soft_fido2/client/
mod.rs

1//! FIDO2 Client API
2//!
3//! High-level, type-safe interface for communicating with FIDO2 authenticators.
4//!
5//! # Architecture
6//!
7//! This module is organized into logical sections:
8//! - **Core Operations**: makeCredential, getAssertion, getInfo
9//! - **Credential Management**: enumerate/delete/update credentials and RPs
10//! - **Authentication Helpers**: PIN/UV token acquisition
11//! - **CBOR Helpers**: Low-level encoding utilities
12//!
13//! # Design Principles
14//!
15//! - **Zero-Copy**: Uses `SmallVec` and stack buffers to minimize allocations
16//! - **Type Safety**: Builder patterns prevent invalid request construction
17//! - **Performance**: Canonical CBOR encoding, pre-allocated vectors
18//! - **Ergonomics**: High-level convenience methods with sensible defaults
19
20mod auth;
21mod cbor_helpers;
22pub mod credential_mgmt;
23
24use crate::error::{Error, Result};
25use crate::request::{GetAssertionRequest, MakeCredentialRequest};
26use crate::transport::Transport;
27
28use soft_fido2_ctap::cbor::{MapBuilder, Value};
29
30use serde::Serialize;
31use sha2::{Digest, Sha256};
32use smallvec::SmallVec;
33
34/// Client for communicating with FIDO2 authenticators
35///
36/// All methods are stateless and require an active `Transport` connection.
37pub struct Client;
38
39impl Client {
40    /// Create a new credential (WebAuthn registration)
41    ///
42    /// Uses the builder pattern for type-safe, ergonomic credential creation.
43    pub fn make_credential(
44        transport: &mut Transport,
45        request: MakeCredentialRequest,
46    ) -> Result<Vec<u8>> {
47        let mut builder = MapBuilder::new();
48
49        builder = builder
50            .insert_bytes(1, request.client_data_hash().as_slice())
51            .map_err(|_| Error::Other)?;
52
53        let mut rp_fields: SmallVec<[(&str, &str); 2]> = SmallVec::new();
54        rp_fields.push(("id", request.rp().id.as_str()));
55        if let Some(name) = &request.rp().name {
56            rp_fields.push(("name", name.as_str()));
57        }
58        builder = builder
59            .insert_text_map(2, &rp_fields)
60            .map_err(|_| Error::Other)?;
61
62        let user_cbor = soft_fido2_ctap::cbor::encode(&request.user()).map_err(|_| Error::Other)?;
63        builder = builder
64            .insert(
65                3,
66                &soft_fido2_ctap::cbor::decode::<Value>(&user_cbor).map_err(|_| Error::Other)?,
67            )
68            .map_err(|_| Error::Other)?;
69
70        #[derive(Serialize)]
71        struct PubKeyCredParam {
72            alg: i32,
73            #[serde(rename = "type")]
74            cred_type: &'static str,
75        }
76
77        let alg_param = PubKeyCredParam {
78            alg: -7,
79            cred_type: "public-key",
80        };
81        let alg_params: SmallVec<[PubKeyCredParam; 1]> = SmallVec::from_buf([alg_param]);
82        builder = builder.insert(4, alg_params).map_err(|_| Error::Other)?;
83
84        if request.resident_key.is_some() || request.user_verification.is_some() {
85            #[derive(Serialize)]
86            struct Options {
87                #[serde(skip_serializing_if = "Option::is_none")]
88                rk: Option<bool>,
89                #[serde(skip_serializing_if = "Option::is_none")]
90                uv: Option<bool>,
91            }
92
93            let options = Options {
94                rk: request.resident_key,
95                uv: request.user_verification,
96            };
97
98            builder = builder.insert(7, &options).map_err(|_| Error::Other)?;
99        }
100
101        if let Some(pin_auth) = request.pin_uv_auth() {
102            builder = builder
103                .insert_bytes(8, pin_auth.param())
104                .map_err(|_| Error::Other)?;
105
106            builder = builder
107                .insert(9, pin_auth.protocol_u8())
108                .map_err(|_| Error::Other)?;
109        }
110
111        let request_bytes = builder.build().map_err(|_| Error::Other)?;
112        let response = transport.send_ctap_command(0x01, &request_bytes, request.timeout_ms)?;
113
114        Ok(response)
115    }
116
117    /// Get an assertion (WebAuthn authentication)
118    ///
119    /// Uses the builder pattern for type-safe, ergonomic assertion retrieval.
120    pub fn get_assertion(
121        transport: &mut Transport,
122        request: GetAssertionRequest,
123    ) -> Result<Vec<u8>> {
124        let mut builder = MapBuilder::new();
125
126        builder = builder
127            .insert(1, request.rp_id())
128            .map_err(|_| Error::Other)?;
129
130        builder = builder
131            .insert_bytes(2, request.client_data_hash().as_slice())
132            .map_err(|_| Error::Other)?;
133
134        if !request.allow_list().is_empty() {
135            #[derive(Serialize)]
136            struct Credential<'a> {
137                id: &'a [u8],
138                #[serde(rename = "type")]
139                credential_type: &'a str,
140            }
141
142            let allow_list: SmallVec<[Credential; 4]> = request
143                .allow_list()
144                .iter()
145                .map(|cred| Credential {
146                    id: cred.id.as_slice(),
147                    credential_type: cred.credential_type.as_str(),
148                })
149                .collect();
150
151            builder = builder.insert(3, &allow_list).map_err(|_| Error::Other)?;
152        }
153
154        if request.user_verification.is_some() {
155            #[derive(Serialize)]
156            struct Options {
157                #[serde(skip_serializing_if = "Option::is_none")]
158                uv: Option<bool>,
159            }
160
161            let options = Options {
162                uv: request.user_verification,
163            };
164
165            builder = builder.insert(5, options).map_err(|_| Error::Other)?;
166        }
167
168        if let Some(pin_auth) = request.pin_uv_auth() {
169            builder = builder
170                .insert_bytes(6, pin_auth.param())
171                .map_err(|_| Error::Other)?;
172
173            builder = builder
174                .insert(7, pin_auth.protocol_u8())
175                .map_err(|_| Error::Other)?;
176        }
177
178        let request_bytes = builder.build().map_err(|_| Error::Other)?;
179        let response = transport.send_ctap_command(0x02, &request_bytes, request.timeout_ms)?;
180
181        Ok(response)
182    }
183
184    /// Send authenticatorGetInfo command
185    pub fn authenticator_get_info(transport: &mut Transport) -> Result<Vec<u8>> {
186        let response = transport.send_ctap_command(0x04, &[], 30000)?;
187        Ok(response)
188    }
189
190    /// Create a new credential (zero-allocation variant)
191    ///
192    /// The caller provides a buffer to write the response into.
193    /// Returns the number of bytes written.
194    pub fn make_credential_buf(
195        transport: &mut Transport,
196        request: MakeCredentialRequest,
197        response: &mut [u8],
198    ) -> Result<usize> {
199        let mut builder = MapBuilder::new();
200
201        builder = builder
202            .insert_bytes(1, request.client_data_hash().as_slice())
203            .map_err(|_| Error::Other)?;
204
205        let mut rp_fields: SmallVec<[(&str, &str); 2]> = SmallVec::new();
206        rp_fields.push(("id", request.rp().id.as_str()));
207        if let Some(name) = &request.rp().name {
208            rp_fields.push(("name", name.as_str()));
209        }
210        builder = builder
211            .insert_text_map(2, &rp_fields)
212            .map_err(|_| Error::Other)?;
213
214        let user_cbor = soft_fido2_ctap::cbor::encode(&request.user()).map_err(|_| Error::Other)?;
215
216        builder = builder
217            .insert(
218                3,
219                &soft_fido2_ctap::cbor::decode::<Value>(&user_cbor).map_err(|_| Error::Other)?,
220            )
221            .map_err(|_| Error::Other)?;
222
223        #[derive(Serialize)]
224        struct PubKeyCredParam {
225            alg: i32,
226            #[serde(rename = "type")]
227            cred_type: &'static str,
228        }
229
230        let alg_param = PubKeyCredParam {
231            alg: -7,
232            cred_type: "public-key",
233        };
234        let alg_params: SmallVec<[PubKeyCredParam; 1]> = SmallVec::from_buf([alg_param]);
235        builder = builder.insert(4, alg_params).map_err(|_| Error::Other)?;
236
237        if request.resident_key.is_some() || request.user_verification.is_some() {
238            #[derive(Serialize)]
239            struct Options {
240                #[serde(skip_serializing_if = "Option::is_none")]
241                rk: Option<bool>,
242                #[serde(skip_serializing_if = "Option::is_none")]
243                uv: Option<bool>,
244            }
245
246            let options = Options {
247                rk: request.resident_key,
248                uv: request.user_verification,
249            };
250
251            builder = builder.insert(7, &options).map_err(|_| Error::Other)?;
252        }
253
254        if let Some(pin_auth) = request.pin_uv_auth() {
255            builder = builder
256                .insert_bytes(8, pin_auth.param())
257                .map_err(|_| Error::Other)?;
258
259            builder = builder
260                .insert(9, pin_auth.protocol_u8())
261                .map_err(|_| Error::Other)?;
262        }
263
264        let request_bytes = builder.build().map_err(|_| Error::Other)?;
265        transport.send_ctap_command_buf(0x01, &request_bytes, response, request.timeout_ms)
266    }
267
268    /// Get an assertion (zero-allocation variant)
269    ///
270    /// The caller provides a buffer to write the response into.
271    /// Returns the number of bytes written.
272    pub fn get_assertion_buf(
273        transport: &mut Transport,
274        request: GetAssertionRequest,
275        response: &mut [u8],
276    ) -> Result<usize> {
277        let mut builder = MapBuilder::new();
278
279        builder = builder
280            .insert(1, request.rp_id())
281            .map_err(|_| Error::Other)?;
282
283        builder = builder
284            .insert_bytes(2, request.client_data_hash().as_slice())
285            .map_err(|_| Error::Other)?;
286
287        if !request.allow_list().is_empty() {
288            #[derive(Serialize)]
289            struct Credential<'a> {
290                id: &'a [u8],
291                #[serde(rename = "type")]
292                credential_type: &'a str,
293            }
294
295            let allow_list: SmallVec<[Credential; 4]> = request
296                .allow_list()
297                .iter()
298                .map(|cred| Credential {
299                    id: cred.id.as_slice(),
300                    credential_type: cred.credential_type.as_str(),
301                })
302                .collect();
303
304            builder = builder.insert(3, &allow_list).map_err(|_| Error::Other)?;
305        }
306
307        if request.user_verification.is_some() {
308            #[derive(Serialize)]
309            struct Options {
310                #[serde(skip_serializing_if = "Option::is_none")]
311                uv: Option<bool>,
312            }
313
314            let options = Options {
315                uv: request.user_verification,
316            };
317
318            builder = builder.insert(5, options).map_err(|_| Error::Other)?;
319        }
320
321        if let Some(pin_auth) = request.pin_uv_auth() {
322            builder = builder
323                .insert_bytes(6, pin_auth.param())
324                .map_err(|_| Error::Other)?;
325
326            builder = builder
327                .insert(7, pin_auth.protocol_u8())
328                .map_err(|_| Error::Other)?;
329        }
330
331        let request_bytes = builder.build().map_err(|_| Error::Other)?;
332        transport.send_ctap_command_buf(0x02, &request_bytes, response, request.timeout_ms)
333    }
334
335    /// Send authenticatorGetInfo command (zero-allocation variant)
336    ///
337    /// The caller provides a buffer to write the response into.
338    /// Returns the number of bytes written.
339    pub fn authenticator_get_info_buf(
340        transport: &mut Transport,
341        response: &mut [u8],
342    ) -> Result<usize> {
343        transport.send_ctap_command_buf(0x04, &[], response, 30000)
344    }
345
346    /// Get credentials metadata (wrapper for credential_mgmt module)
347    pub fn get_credentials_metadata(
348        transport: &mut Transport,
349        request: crate::request::CredentialManagementRequest,
350    ) -> Result<crate::response::CredentialsMetadata> {
351        credential_mgmt::get_credentials_metadata(transport, request)
352    }
353
354    /// Begin RP enumeration (wrapper for credential_mgmt module)
355    pub fn enumerate_rps_begin(
356        transport: &mut Transport,
357        request: crate::request::CredentialManagementRequest,
358    ) -> Result<crate::response::RpEnumerationBeginResponse> {
359        credential_mgmt::enumerate_rps_begin(transport, request)
360    }
361
362    /// Get next RP in enumeration (wrapper for credential_mgmt module)
363    pub fn enumerate_rps_get_next(transport: &mut Transport) -> Result<crate::response::RpInfo> {
364        credential_mgmt::enumerate_rps_get_next(transport)
365    }
366
367    /// Enumerate all RPs (wrapper for credential_mgmt module)
368    pub fn enumerate_rps(
369        transport: &mut Transport,
370        request: crate::request::CredentialManagementRequest,
371    ) -> Result<Vec<crate::response::RpInfo>> {
372        credential_mgmt::enumerate_rps(transport, request)
373    }
374
375    /// Begin credential enumeration (wrapper for credential_mgmt module)
376    pub fn enumerate_credentials_begin(
377        transport: &mut Transport,
378        request: crate::request::EnumerateCredentialsRequest,
379    ) -> Result<crate::response::CredentialEnumerationBeginResponse> {
380        credential_mgmt::enumerate_credentials_begin(transport, request)
381    }
382
383    /// Get next credential (wrapper for credential_mgmt module)
384    pub fn enumerate_credentials_get_next(
385        transport: &mut Transport,
386    ) -> Result<crate::response::CredentialInfo> {
387        credential_mgmt::enumerate_credentials_get_next(transport)
388    }
389
390    /// Enumerate all credentials (wrapper for credential_mgmt module)
391    pub fn enumerate_credentials(
392        transport: &mut Transport,
393        request: crate::request::EnumerateCredentialsRequest,
394    ) -> Result<Vec<crate::response::CredentialInfo>> {
395        credential_mgmt::enumerate_credentials(transport, request)
396    }
397
398    /// Delete a credential (wrapper for credential_mgmt module)
399    pub fn delete_credential(
400        transport: &mut Transport,
401        request: crate::request::DeleteCredentialRequest,
402    ) -> Result<()> {
403        credential_mgmt::delete_credential(transport, request)
404    }
405
406    /// Update user information (wrapper for credential_mgmt module)
407    pub fn update_user_information(
408        transport: &mut Transport,
409        request: crate::request::UpdateUserRequest,
410    ) -> Result<()> {
411        credential_mgmt::update_user_information(transport, request)
412    }
413
414    /// Get a PIN/UV auth token for credential management operations
415    ///
416    /// Handles the complete PIN authentication flow and returns a ready-to-use token.
417    pub fn get_pin_token_for_credential_management(
418        transport: &mut Transport,
419        pin: &str,
420        protocol: crate::pin::PinProtocol,
421    ) -> Result<crate::request::PinUvAuth> {
422        use crate::pin::PinUvAuthEncapsulation;
423        use crate::request::Permission;
424
425        let mut encapsulation = PinUvAuthEncapsulation::new(transport, protocol)?;
426
427        let permissions = Permission::CredentialManagement as u8;
428        let pin_token = encapsulation.get_pin_uv_auth_token_using_pin_with_permissions(
429            transport,
430            pin,
431            permissions,
432            None,
433        )?;
434
435        Ok(crate::request::PinUvAuth::new(pin_token, protocol.into()))
436    }
437
438    /// Get a PIN/UV auth token using user verification (biometric/platform auth)
439    ///
440    /// Attempts to get a PIN token using built-in user verification
441    /// instead of PIN. Not all authenticators support this.
442    pub fn get_uv_token_for_credential_management(
443        transport: &mut Transport,
444        protocol: crate::pin::PinProtocol,
445    ) -> Result<crate::request::PinUvAuth> {
446        use crate::pin::PinUvAuthEncapsulation;
447        use crate::request::Permission;
448
449        let mut encapsulation = PinUvAuthEncapsulation::new(transport, protocol)?;
450
451        let permissions = Permission::CredentialManagement as u8;
452        let uv_token = encapsulation.get_pin_uv_auth_token_using_uv_with_permissions(
453            transport,
454            permissions,
455            None,
456        )?;
457
458        Ok(crate::request::PinUvAuth::new(uv_token, protocol.into()))
459    }
460}
461
462/// Compute SHA-256 hash of RP ID
463pub fn compute_rp_id_hash(rp_id: &str) -> [u8; 32] {
464    let mut hasher = Sha256::new();
465    hasher.update(rp_id.as_bytes());
466    hasher.finalize().into()
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472
473    use crate::request::ClientDataHash;
474
475    use soft_fido2_ctap::types::{RelyingParty, User};
476
477    #[test]
478    fn test_make_credential_request_encoding() {
479        let hash = ClientDataHash::new([0u8; 32]);
480        let rp = RelyingParty {
481            id: "example.com".to_string(),
482            name: Some("Example Corp".to_string()),
483        };
484        let user = User {
485            id: vec![1, 2, 3, 4],
486            name: Some("alice@example.com".to_string()),
487            display_name: Some("Alice".to_string()),
488        };
489
490        let request = MakeCredentialRequest::new(hash, rp, user);
491
492        assert_eq!(request.rp().id, "example.com");
493        assert_eq!(request.user().id, vec![1, 2, 3, 4]);
494    }
495
496    #[test]
497    fn test_get_assertion_request_encoding() {
498        let hash = ClientDataHash::new([0u8; 32]);
499        let request = GetAssertionRequest::new(hash, "example.com".to_string());
500
501        assert_eq!(request.rp_id(), "example.com");
502    }
503
504    #[test]
505    fn test_compute_rp_id_hash() {
506        let hash = compute_rp_id_hash("example.com");
507        assert_eq!(hash.len(), 32);
508    }
509}