webauthn_authenticator_rs/ctap2/
ctap21_cred.rs

1//! CTAP 2.1-PRE / 2.1 Credential Management functionality.
2#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
3use std::ops::{Deref, DerefMut};
4
5#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
6use async_trait::async_trait;
7#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
8use webauthn_rs_proto::UserVerificationPolicy;
9
10use crate::ui::UiCallback;
11
12#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
13use crate::{
14    crypto::SHA256Hash,
15    ctap2::{
16        commands::{
17            CredSubCommand, CredentialManagementRequestTrait, CredentialManagementResponse,
18            CredentialStorageMetadata, DiscoverableCredential, Permissions,
19            PublicKeyCredentialDescriptorCM, RelyingPartyCM,
20        },
21        ctap20::AuthSession,
22        Ctap20Authenticator,
23    },
24    error::{CtapError, WebauthnCError},
25    transport::Token,
26};
27
28/// Trait to provide a [CredentialManagementAuthenticator] implementation.
29pub trait CredentialManagementAuthenticatorInfo<U: UiCallback>: Sync + Send {
30    #[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
31    /// Request type for credential management commands.
32    type RequestType: CredentialManagementRequestTrait;
33
34    /// Checks if the authenticator supports credential management commands.
35    fn supports_credential_management(&self) -> bool;
36}
37
38#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
39/// Internal support methods for credential management.
40#[async_trait]
41pub(super) trait CredentialManagementAuthenticatorSupport<T, U, R>
42where
43    T: CredentialManagementAuthenticatorInfo<U, RequestType = R>,
44    U: UiCallback,
45    R: CredentialManagementRequestTrait,
46{
47    async fn cred_mgmt(
48        &mut self,
49        sub_command: CredSubCommand,
50    ) -> Result<CredentialManagementResponse, WebauthnCError>;
51
52    /// Send a [CredSubCommand] using a provided `pin_uv_auth_token` session.
53    #[allow(dead_code)]
54    async fn cred_mgmt_with_session(
55        &mut self,
56        sub_command: CredSubCommand,
57        auth_session: &AuthSession,
58    ) -> Result<CredentialManagementResponse, WebauthnCError>;
59}
60
61#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
62#[async_trait]
63impl<'a, K, T, U, R> CredentialManagementAuthenticatorSupport<T, U, R> for T
64where
65    K: Token,
66    T: CredentialManagementAuthenticatorInfo<U, RequestType = R>
67        + Deref<Target = Ctap20Authenticator<'a, K, U>>
68        + DerefMut<Target = Ctap20Authenticator<'a, K, U>>,
69    U: UiCallback + 'a,
70    R: CredentialManagementRequestTrait,
71{
72    async fn cred_mgmt(
73        &mut self,
74        sub_command: CredSubCommand,
75    ) -> Result<CredentialManagementResponse, WebauthnCError> {
76        let (pin_uv_auth_proto, pin_uv_auth_param) = self
77            .get_pin_uv_auth_token(
78                sub_command.prf().as_slice(),
79                Permissions::CREDENTIAL_MANAGEMENT,
80                None,
81                UserVerificationPolicy::Required,
82            )
83            .await?
84            .into_pin_uv_params();
85
86        let ui = self.ui_callback;
87        let r = self
88            .token
89            .transmit(
90                T::RequestType::new(sub_command, pin_uv_auth_proto, pin_uv_auth_param),
91                ui,
92            )
93            .await?;
94
95        Ok(r)
96    }
97
98    async fn cred_mgmt_with_session(
99        &mut self,
100        sub_command: CredSubCommand,
101        auth_session: &AuthSession,
102    ) -> Result<CredentialManagementResponse, WebauthnCError> {
103        let (pin_uv_protocol, pin_uv_auth_param) = match auth_session {
104            AuthSession::InterfaceToken(iface, pin_token) => {
105                let mut pin_uv_auth_param =
106                    iface.authenticate(pin_token, sub_command.prf().as_slice())?;
107                pin_uv_auth_param.truncate(16);
108
109                (Some(iface.get_pin_uv_protocol()), Some(pin_uv_auth_param))
110            }
111
112            _ => (None, None),
113        };
114
115        let ui = self.ui_callback;
116        self.token
117            .transmit(
118                T::RequestType::new(sub_command, pin_uv_protocol, pin_uv_auth_param),
119                ui,
120            )
121            .await
122    }
123}
124
125#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
126/// [CTAP 2.1] and [2.1-PRE] discoverable credential management commands.
127///
128/// All methods return [`WebauthnCError::NotSupported`] if the authenticator
129/// does not support credential management.
130///
131/// ## See also
132///
133/// * [`Ctap21Authenticator::update_credential_user()`][update]
134///
135/// [CTAP 2.1]: super::Ctap21Authenticator
136/// [2.1-PRE]: super::Ctap21PreAuthenticator
137/// [update]: super::Ctap21Authenticator::update_credential_user
138#[async_trait]
139pub trait CredentialManagementAuthenticator {
140    /// Checks that the device supports credential management.
141    ///
142    /// Returns [WebauthnCError::NotSupported] if the token does not support
143    /// credential management.
144    fn check_credential_management_support(&self) -> Result<(), WebauthnCError>;
145
146    /// Gets metadata about the authenticator's discoverable credential storage.
147    ///
148    /// See [CredentialStorageMetadata] for more details.
149    async fn get_credentials_metadata(
150        &mut self,
151    ) -> Result<CredentialStorageMetadata, WebauthnCError>;
152
153    /// Enumerates a list of all relying parties with discoverable credentials
154    /// stored on this authenticator.
155    ///
156    /// ## Note
157    ///
158    /// To iterate over all credentials for a relying party, pass the
159    /// [`RelyingPartyCM::hash`] to [`enumerate_credentials_by_hash`][0].
160    ///
161    /// [`RelyingPartyCM::id`] [might be truncated][1] by the authenticator.
162    ///
163    /// [0]: CredentialManagementAuthenticator::enumerate_credentials_by_hash
164    /// [1]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#rpid-truncation
165    async fn enumerate_rps(&mut self) -> Result<Vec<RelyingPartyCM>, WebauthnCError>;
166
167    /// Enumerates all discoverable credentials on the authenticator for a
168    /// relying party, by the SHA-256 hash of the relying party ID.
169    ///
170    /// ## Note
171    ///
172    /// This does not provide a "permissions RP ID" with the request, as it only
173    /// works correctly with authenticators supporting the `pinUvAuthToken`
174    /// feature.
175    async fn enumerate_credentials_by_hash(
176        &mut self,
177        rp_id_hash: SHA256Hash,
178    ) -> Result<Vec<DiscoverableCredential>, WebauthnCError>;
179
180    /// Enumerates all discoverable credentials on the authenticator for a
181    /// relying party, by the relying party ID.
182    ///
183    /// ## Note
184    ///
185    /// This does not provide a "permissions RP ID" with the request, as it only
186    /// works correctly with authenticators supporting the `pinUvAuthToken`
187    /// feature.
188    async fn enumerate_credentials_by_rpid(
189        &mut self,
190        rpid: &str,
191    ) -> Result<Vec<DiscoverableCredential>, WebauthnCError>;
192
193    /// Deletes a discoverable credential from the authenticator.
194    ///
195    /// ## Note
196    ///
197    /// This does not provide a "permissions RP ID" with the request, as it only
198    /// works correctly with authenticators supporting the `pinUvAuthToken`
199    /// feature.
200    ///
201    /// ## Warning
202    ///
203    /// This does not garbage-collect associated large blob storage.
204    async fn delete_credential(
205        &mut self,
206        credential_id: PublicKeyCredentialDescriptorCM,
207    ) -> Result<(), WebauthnCError>;
208}
209
210#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
211/// Implementation of credential management commands for [Ctap21Authenticator][]
212/// and [Ctap21PreAuthenticator][].
213///
214/// [Ctap21Authenticator]: super::Ctap21Authenticator
215/// [Ctap21PreAuthenticator]: super::Ctap21PreAuthenticator
216#[async_trait]
217impl<'a, K, T, U, R> CredentialManagementAuthenticator for T
218where
219    K: Token,
220    T: CredentialManagementAuthenticatorInfo<U, RequestType = R>
221        + Deref<Target = Ctap20Authenticator<'a, K, U>>
222        + DerefMut<Target = Ctap20Authenticator<'a, K, U>>,
223    U: UiCallback + 'a,
224    R: CredentialManagementRequestTrait,
225{
226    fn check_credential_management_support(&self) -> Result<(), WebauthnCError> {
227        if !self.supports_credential_management() {
228            return Err(WebauthnCError::NotSupported);
229        }
230
231        Ok(())
232    }
233
234    async fn get_credentials_metadata(
235        &mut self,
236    ) -> Result<CredentialStorageMetadata, WebauthnCError> {
237        self.check_credential_management_support()?;
238
239        let r = self.cred_mgmt(CredSubCommand::GetCredsMetadata).await?;
240
241        r.storage_metadata
242            .ok_or(WebauthnCError::MissingRequiredField)
243    }
244
245    async fn enumerate_rps(&mut self) -> Result<Vec<RelyingPartyCM>, WebauthnCError> {
246        self.check_credential_management_support()?;
247        let r = self.cred_mgmt(CredSubCommand::EnumerateRPsBegin).await;
248
249        // "If no discoverable credentials exist on the authenticator, return
250        // CTAP2_ERR_NO_CREDENTIALS."
251        if matches!(r, Err(WebauthnCError::Ctap(CtapError::Ctap2NoCredentials))) {
252            return Ok(Vec::new());
253        }
254        let r = r?;
255
256        // Feitian doesn't return an error when there are zero keys, and instead
257        // sends an empty CredentialManagementResponse (ie: no fields set), so
258        // we can't require that total_rps is set.
259        //
260        // Token2 and Yubikey also doesn't return an error when there are zero
261        // keys, but at least sets `total_rps = 0`.
262        let total_rps = r.total_rps.unwrap_or_default();
263        let mut o = Vec::with_capacity(total_rps as usize);
264
265        if total_rps == 0 {
266            return Ok(o);
267        }
268
269        if let Some(rp) = r.rp {
270            o.push(rp);
271        } else {
272            return Err(WebauthnCError::MissingRequiredField);
273        };
274
275        let ui = self.ui_callback;
276        for _ in 1..total_rps {
277            let r = self.token.transmit(R::ENUMERATE_RPS_GET_NEXT, ui).await?;
278            if let Some(rp) = r.rp {
279                o.push(rp);
280            } else {
281                break;
282            }
283        }
284
285        Ok(o)
286    }
287
288    async fn enumerate_credentials_by_hash(
289        &mut self,
290        rp_id_hash: SHA256Hash,
291    ) -> Result<Vec<DiscoverableCredential>, WebauthnCError> {
292        self.check_credential_management_support()?;
293        enumerate_credentials_impl(self, CredSubCommand::EnumerateCredentialsBegin(rp_id_hash))
294            .await
295    }
296
297    async fn enumerate_credentials_by_rpid(
298        &mut self,
299        rp_id: &str,
300    ) -> Result<Vec<DiscoverableCredential>, WebauthnCError> {
301        self.check_credential_management_support()?;
302        enumerate_credentials_impl(self, CredSubCommand::enumerate_credentials_by_rpid(rp_id)).await
303    }
304
305    async fn delete_credential(
306        &mut self,
307        credential_id: PublicKeyCredentialDescriptorCM,
308    ) -> Result<(), WebauthnCError> {
309        self.check_credential_management_support()?;
310
311        self.cred_mgmt(CredSubCommand::DeleteCredential(credential_id))
312            .await
313            .map(|_| ())
314    }
315}
316
317#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
318#[doc(hidden)]
319async fn enumerate_credentials_impl<'a, K, T, U, R>(
320    self_: &mut T,
321    sub_command: CredSubCommand,
322) -> Result<Vec<DiscoverableCredential>, WebauthnCError>
323where
324    K: Token,
325    T: CredentialManagementAuthenticatorInfo<U, RequestType = R>
326        + Deref<Target = Ctap20Authenticator<'a, K, U>>
327        + DerefMut<Target = Ctap20Authenticator<'a, K, U>>,
328    U: UiCallback + 'a,
329    R: CredentialManagementRequestTrait,
330{
331    let r = self_.cred_mgmt(sub_command).await;
332
333    // "If no discoverable credentials for this RP ID hash exist on this
334    // authenticator, return CTAP2_ERR_NO_CREDENTIALS."
335    if matches!(r, Err(WebauthnCError::Ctap(CtapError::Ctap2NoCredentials))) {
336        return Ok(Vec::new());
337    }
338    let r = r?;
339
340    let total_creds = r.total_credentials.unwrap_or_default();
341    let mut o = Vec::with_capacity(total_creds as usize);
342
343    if total_creds == 0 {
344        return Ok(o);
345    }
346
347    o.push(r.discoverable_credential);
348
349    let ui = self_.ui_callback;
350    for _ in 1..total_creds {
351        let r = self_
352            .token
353            .transmit(R::ENUMERATE_CREDENTIALS_GET_NEXT, ui)
354            .await?;
355        o.push(r.discoverable_credential);
356    }
357
358    Ok(o)
359}