Skip to main content

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}