ctap_fido2/cmd/mod.rs
1//! Public-facing command API. Submodules carry the wire encoding/parsing.
2
3use ciborium::Value;
4use zeroize::ZeroizeOnDrop;
5
6use crate::{
7 cmd::{
8 get_assertion::{
9 Assertion,
10 HmacSecretRequest,
11 HmacSecretResponse,
12 },
13 make_credential::{
14 CredProtect,
15 Credential,
16 },
17 },
18 device::DeviceInfo,
19 error::Result,
20 hid::Transport,
21 pin::{
22 self,
23 PinSession,
24 PinToken,
25 },
26};
27
28pub mod get_assertion;
29pub mod get_info;
30pub mod get_next_assertion;
31pub mod make_credential;
32pub mod reset;
33
34/// COSE algorithms this crate will ask the authenticator for.
35#[derive(Copy, Clone, Debug, PartialEq, Eq)]
36pub enum Algorithm {
37 /// ECDSA P-256 / SHA-256, COSE `-7`.
38 Es256,
39 /// `EdDSA` Ed25519, COSE `-8`.
40 EdDsa,
41}
42
43impl Algorithm {
44 /// Wire-level COSE algorithm identifier.
45 #[must_use]
46 pub const fn cose_id(self) -> i32 {
47 match self {
48 Self::Es256 => -7,
49 Self::EdDsa => -8,
50 }
51 }
52}
53
54impl From<Algorithm> for i32 {
55 fn from(alg: Algorithm) -> Self {
56 alg.cose_id()
57 }
58}
59
60/// Parsed `authenticatorGetInfo` response.
61///
62/// Fields mirror the CTAP2.1 spec; absent values are `None`/empty.
63#[derive(Clone, Debug, Default)]
64pub struct AuthenticatorInfo {
65 /// `versions` (0x01) — protocol versions, e.g. `"FIDO_2_0"`, `"FIDO_2_1"`.
66 pub versions: Vec<String>,
67 /// `extensions` (0x02) — extension names the device understands.
68 pub extensions: Vec<String>,
69 /// `aaguid` (0x03) — 16-byte authenticator model identifier.
70 pub aaguid: [u8; 16],
71 /// `options` (0x04) — capability flags such as `rk`, `up`, `uv`, `plat`,
72 /// `clientPin`. Missing keys mean the option isn't supported.
73 pub options: Vec<(String, bool)>,
74 /// `maxMsgSize` (0x05).
75 pub max_msg_size: Option<u32>,
76 /// `pinProtocols` (0x06) — PIN protocol versions supported, ordered by
77 /// device preference. Empty if `clientPin` is unsupported.
78 pub pin_protocols: Vec<u8>,
79 /// `maxCredentialCountInList` (0x07).
80 pub max_credential_count_in_list: Option<u32>,
81 /// `maxCredentialIdLength` (0x08).
82 pub max_credential_id_length: Option<u32>,
83 /// `transports` (0x09) — `"usb"`, `"nfc"`, `"ble"`, `"internal"`.
84 pub transports: Vec<String>,
85 /// `algorithms` (0x0A) — COSE algorithm ids the device will sign with.
86 pub algorithms: Vec<i32>,
87 /// `maxSerializedLargeBlobArray` (0x0B).
88 pub max_serialized_large_blob_array: Option<u32>,
89 /// `forcePINChange` (0x0C).
90 pub force_pin_change: bool,
91 /// `minPINLength` (0x0D).
92 pub min_pin_length: Option<u32>,
93 /// `firmwareVersion` (0x0E).
94 pub firmware_version: Option<u32>,
95 /// `maxCredBlobLength` (0x0F).
96 pub max_cred_blob_length: Option<u32>,
97 /// `maxRPIDsForSetMinPINLength` (0x10).
98 pub max_rp_ids_for_set_min_pin_length: Option<u32>,
99 /// `preferredPlatformUvAttempts` (0x11).
100 pub preferred_platform_uv_attempts: Option<u32>,
101 /// `uvModality` (0x12).
102 pub uv_modality: Option<u32>,
103 /// `remainingDiscoverableCredentials` (0x14).
104 pub remaining_discoverable_credentials: Option<u32>,
105}
106
107impl AuthenticatorInfo {
108 /// Read a named option, returning `None` if the device didn't advertise it.
109 #[must_use]
110 pub fn option(&self, name: &str) -> Option<bool> {
111 self
112 .options
113 .iter()
114 .find_map(|&(ref stored_key, value)| (stored_key == name).then_some(value))
115 }
116
117 /// True iff a client PIN is currently set on the device.
118 #[must_use]
119 pub fn client_pin_set(&self) -> bool {
120 self.option("clientPin").unwrap_or(false)
121 }
122
123 /// True iff the device advertises the `hmac-secret` extension.
124 #[must_use]
125 pub fn hmac_secret(&self) -> bool {
126 self.extensions.iter().any(|name| name == "hmac-secret")
127 }
128
129 /// True iff the device accepts PIN protocol v1 commands. CTAP2.0 devices
130 /// that don't advertise `pinProtocols` are treated as v1.
131 #[must_use]
132 pub fn supports_pin_protocol_v1(&self) -> bool {
133 self.pin_protocols.is_empty() || self.pin_protocols.contains(&1)
134 }
135
136 /// True iff the device advertises PIN protocol v2. The crate does not
137 /// implement v2 yet; this is exposed so callers can detect v2-only
138 /// devices and fail early rather than time out on a v1 exchange.
139 #[must_use]
140 pub fn supports_pin_protocol_v2(&self) -> bool {
141 self.pin_protocols.contains(&2)
142 }
143}
144
145/// Options for [`Authenticator::make_credential`].
146#[derive(Clone, Debug)]
147pub struct MakeCredentialOptions<'a> {
148 /// Algorithm to request.
149 pub algorithm: Algorithm,
150 /// `None` disables UV too. UV-scoped hmac-secret outputs would break
151 /// interop with the Go `age-plugin-fido2-hmac`.
152 pub pin: Option<&'a str>,
153 /// Ask the device to create a resident (discoverable) credential.
154 pub resident_key: bool,
155 /// `credProtect` policy level to apply to the credential. `None`
156 /// leaves the device's default in place.
157 pub cred_protect: Option<CredProtect>,
158 /// Up to 32 bytes of arbitrary data to store on the device alongside
159 /// the credential. Retrievable on every assertion without a second
160 /// touch.
161 pub cred_blob: Option<&'a [u8]>,
162 /// Request the per-credential large-blob key from the device. Returned
163 /// in [`Credential::large_blob_key`] when the device supports the
164 /// extension.
165 pub large_blob_key: bool,
166 /// Ask the device to report its current `minPinLength` value in the
167 /// extensions echo.
168 pub min_pin_length: bool,
169}
170
171impl Default for MakeCredentialOptions<'_> {
172 fn default() -> Self {
173 Self {
174 algorithm: Algorithm::Es256,
175 pin: None,
176 resident_key: false,
177 cred_protect: None,
178 cred_blob: None,
179 large_blob_key: false,
180 min_pin_length: false,
181 }
182 }
183}
184
185/// 32-byte hmac-secret output.
186#[derive(ZeroizeOnDrop)]
187pub struct HmacSecret(pub [u8; 32]);
188
189/// Open CTAP2 authenticator handle.
190pub struct Authenticator {
191 pub(crate) transport: Transport,
192 pub(crate) info: Option<AuthenticatorInfo>,
193}
194
195impl Authenticator {
196 /// Open a device returned by
197 /// [`list_devices`](crate::device::list_devices). Runs `CTAPHID_INIT`
198 /// to allocate a channel id.
199 ///
200 /// # Errors
201 ///
202 /// [`Error::Hid`] if `hidapi` can't open the path, [`Error::Parse`] if
203 /// the INIT response is malformed.
204 pub fn open(info: &DeviceInfo) -> Result<Self> {
205 let transport = Transport::open(&info.path)?;
206 Ok(Self {
207 transport,
208 info: None,
209 })
210 }
211
212 /// Borrow the underlying [`Transport`] for raw CTAPHID exchanges.
213 pub const fn transport_mut(&mut self) -> &mut Transport {
214 &mut self.transport
215 }
216
217 /// Firmware version reported in the `CTAPHID_INIT` response that ran
218 /// during [`Self::open`]. Tuple is `(major, minor, build)`.
219 #[must_use]
220 pub const fn firmware_version(&self) -> (u8, u8, u8) {
221 self.transport.firmware_version()
222 }
223
224 /// Cached `authenticatorGetInfo`, fetched on first call.
225 ///
226 /// # Errors
227 ///
228 /// Whatever [`get_info::call`] propagates: [`Error::Ctap`],
229 /// [`Error::Hid`], [`Error::Cbor`].
230 pub fn info(&mut self) -> Result<&AuthenticatorInfo> {
231 if let Some(ref info) = self.info {
232 return Ok(info);
233 }
234 let fresh = get_info::call(&mut self.transport)?;
235 Ok(self.info.insert(fresh))
236 }
237
238 /// Create a non-discoverable credential bound to `hmac-secret`.
239 /// Returns the credential id and public key. Persist both: the public
240 /// key is required to verify assertion signatures via
241 /// [`Self::get_hmac_secret`].
242 ///
243 /// # Errors
244 ///
245 /// PIN/touch/policy failures from CTAP, plus the transport and CBOR
246 /// errors from the lower layers.
247 pub fn make_credential(
248 &mut self,
249 rp_id: &str,
250 client_data_hash: &[u8; 32],
251 opts: &MakeCredentialOptions<'_>,
252 ) -> Result<Credential> {
253 make_credential::call(&mut self.transport, rp_id, client_data_hash, opts)
254 }
255
256 /// Remaining PIN attempts. Does not consume one.
257 ///
258 /// # Errors
259 ///
260 /// [`Error::Ctap`] if the device rejects `clientPIN.getPinRetries`, or
261 /// [`Error::Pin`] if the response is missing the retry count.
262 pub fn pin_retries(&mut self) -> Result<u8> {
263 pin::get_pin_retries(&mut self.transport)
264 }
265
266 /// Return the 32-byte `hmac-secret` output(s) for the given request.
267 /// When `req.salt2` is `Some`, the second slot of the returned tuple
268 /// holds the second output. When `None`, the second slot is `None`.
269 ///
270 /// # Errors
271 ///
272 /// Same as [`Self::make_credential`], plus
273 /// [`CtapStatus::NoCredentials`](crate::CtapStatus::NoCredentials)
274 /// when `req.cred_id` is unknown to the device, and
275 /// [`Error::MissingExtension`] when a salt2 was requested but the
276 /// device returned a single-output response.
277 pub fn get_hmac_secret(&mut self, req: &HmacSecretRequest<'_>) -> Result<HmacSecretResponse> {
278 get_assertion::call_hmac_secret(&mut self.transport, req)
279 }
280
281 /// Fetch the next assertion in a multi-credential sequence. Call once
282 /// per remaining credential after [`Self::get_assertion`] returns an
283 /// [`Assertion`] with `number_of_credentials > 1`.
284 ///
285 /// # Errors
286 ///
287 /// Forwards from [`get_next_assertion::call`].
288 pub fn get_next_assertion(&mut self) -> Result<Assertion> {
289 get_next_assertion::call(&mut self.transport)
290 }
291
292 /// Run `getAssertion`. Empty `allow_list` triggers resident-credential
293 /// discovery. `extensions` is a CBOR map of `{name: input}`.
294 ///
295 /// # Errors
296 ///
297 /// Forwards from [`get_assertion::call`].
298 pub fn get_assertion(
299 &mut self,
300 rp_id: &str,
301 client_data_hash: &[u8; 32],
302 allow_list: &[&[u8]],
303 extensions: Option<Value>,
304 pin: Option<&str>,
305 ) -> Result<Assertion> {
306 let pin_token = match pin {
307 Some(value) => {
308 let session = PinSession::establish(&mut self.transport)?;
309 Some(session.get_pin_token(&mut self.transport, value)?)
310 },
311 None => None,
312 };
313 get_assertion::call(
314 &mut self.transport,
315 rp_id,
316 client_data_hash,
317 allow_list,
318 extensions,
319 pin_token.as_ref(),
320 )
321 }
322
323 /// Silent (`up=false`) allow-list probe: returns the matching
324 /// credential id without touch, or [`None`] when the device has none
325 /// of the candidates. Callers follow up with a touch-requiring
326 /// assertion (e.g. [`Self::get_hmac_secret`]) to derive the secret.
327 ///
328 /// # Errors
329 ///
330 /// CTAP statuses other than `NoCredentials` propagate via
331 /// [`Error::Ctap`]. Older firmware rejects `up=false` outright;
332 /// callers should fall back to [`Self::probe_credential_with_touch`].
333 pub fn probe_credential(
334 &mut self,
335 rp_id: &str,
336 client_data_hash: &[u8; 32],
337 allow_list: &[&[u8]],
338 ) -> Result<Option<Vec<u8>>> {
339 use crate::{
340 cmd::get_assertion::AssertionOptions,
341 error::{
342 CtapStatus,
343 Error,
344 },
345 };
346 match get_assertion::call_with_options(
347 &mut self.transport,
348 rp_id,
349 client_data_hash,
350 allow_list,
351 None,
352 None,
353 AssertionOptions::SILENT,
354 ) {
355 Ok(assertion) => Ok(assertion.credential_id),
356 Err(Error::Ctap(CtapStatus::NoCredentials)) => Ok(None),
357 Err(other) => Err(other),
358 }
359 }
360
361 /// Touch-requiring `up=true` allow-list probe. Fallback for
362 /// firmware that rejects [`Self::probe_credential`].
363 ///
364 /// # Errors
365 ///
366 /// CTAP statuses other than `NoCredentials` propagate via
367 /// [`Error::Ctap`].
368 pub fn probe_credential_with_touch(
369 &mut self,
370 rp_id: &str,
371 client_data_hash: &[u8; 32],
372 allow_list: &[&[u8]],
373 ) -> Result<Option<Vec<u8>>> {
374 use crate::error::{
375 CtapStatus,
376 Error,
377 };
378 match get_assertion::call(
379 &mut self.transport,
380 rp_id,
381 client_data_hash,
382 allow_list,
383 None,
384 None,
385 ) {
386 Ok(assertion) => Ok(assertion.credential_id),
387 Err(Error::Ctap(CtapStatus::NoCredentials)) => Ok(None),
388 Err(other) => Err(other),
389 }
390 }
391
392 /// Establish a PIN session for amortizing one ECDH across multiple commands.
393 ///
394 /// # Errors
395 ///
396 /// [`Error::Pin`] if the authenticator's `COSE_Key` is malformed,
397 /// [`Error::Ctap`] for transport failures.
398 pub fn pin_session(&mut self) -> Result<PinSession> {
399 PinSession::establish(&mut self.transport)
400 }
401
402 /// Exchange a PIN for a `pinUvAuthToken` within an established session.
403 ///
404 /// # Errors
405 ///
406 /// [`Error::Ctap`] with
407 /// [`CtapStatus::PinInvalid`](crate::CtapStatus::PinInvalid)
408 /// or [`PinBlocked`](crate::CtapStatus::PinBlocked).
409 pub fn pin_token(&mut self, session: &PinSession, pin: &str) -> Result<PinToken> {
410 session.get_pin_token(&mut self.transport, pin)
411 }
412
413 /// Blink/buzz the device so the user can identify it. Optional per spec.
414 ///
415 /// # Errors
416 ///
417 /// [`Error::Ctap`] with an invalid-command status on devices that don't
418 /// implement `CTAPHID_WINK`.
419 pub fn wink(&mut self) -> Result<()> {
420 self.transport.wink()
421 }
422
423 /// Fire-and-forget `CTAPHID_CANCEL`. Cannot interrupt your own in-flight
424 /// `transact`. Intended for signal handlers and `Drop` paths.
425 ///
426 /// # Errors
427 ///
428 /// [`Error::Hid`] if the underlying HID write fails.
429 pub fn cancel(&self) -> Result<()> {
430 self.transport.cancel()
431 }
432
433 /// **Destructive: wipes all credentials and the PIN.**
434 ///
435 /// Devices typically require the command within ~10s of insertion and
436 /// touch within ~30s of the command.
437 ///
438 /// # Errors
439 ///
440 /// [`Error::Ctap`] if the device rejects the reset (outside the
441 /// 10s-since-insertion window, or no touch within the 30s grace).
442 pub fn reset(&mut self) -> Result<()> {
443 reset::call(&mut self.transport)
444 }
445}