soft_fido2_ctap/
types.rs

1//! CTAP data types
2//!
3//! Core data structures used in CTAP protocol messages.
4//! All types support CBOR serialization as required by the FIDO2 spec.
5
6use crate::sec_bytes::{SecBytes, SecPinHash};
7
8use alloc::string::{String, ToString};
9use alloc::vec::Vec;
10
11#[cfg(feature = "std")]
12use std::time::{SystemTime, UNIX_EPOCH};
13
14use serde::{Deserialize, Serialize};
15
16/// Relying Party information
17///
18/// Represents a web service that uses FIDO2 for authentication.
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct RelyingParty {
21    /// Relying party identifier (e.g., "example.com")
22    pub id: String,
23
24    /// Human-readable name (optional)
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub name: Option<String>,
27}
28
29impl RelyingParty {
30    /// Create a new RelyingParty with just an ID
31    pub fn new(id: String) -> Self {
32        Self { id, name: None }
33    }
34
35    /// Create a new RelyingParty with ID and name
36    pub fn with_name(id: String, name: String) -> Self {
37        Self {
38            id,
39            name: Some(name),
40        }
41    }
42}
43
44/// User information
45///
46/// Represents the user account being registered or authenticated.
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub struct User {
50    /// User handle - opaque identifier for the user
51    #[serde(with = "serde_bytes")]
52    pub id: Vec<u8>,
53
54    /// Human-readable username (optional in some contexts)
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub name: Option<String>,
57
58    /// Human-readable display name (optional)
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub display_name: Option<String>,
61}
62
63impl User {
64    /// Create a new User with just an ID
65    pub fn new(id: Vec<u8>) -> Self {
66        Self {
67            id,
68            name: None,
69            display_name: None,
70        }
71    }
72
73    /// Create a new User with all fields
74    pub fn with_details(id: Vec<u8>, name: String, display_name: String) -> Self {
75        Self {
76            id,
77            name: Some(name),
78            display_name: Some(display_name),
79        }
80    }
81}
82
83/// Public key credential descriptor
84///
85/// Identifies a credential by its type and ID.
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct PublicKeyCredentialDescriptor {
88    /// Credential ID
89    #[serde(with = "serde_bytes")]
90    pub id: Vec<u8>,
91
92    /// Credential type (always "public-key" for FIDO2)
93    pub r#type: String,
94
95    /// Supported transports (optional)
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub transports: Option<Vec<String>>,
98}
99
100impl PublicKeyCredentialDescriptor {
101    /// Create a new public-key credential descriptor
102    pub fn new(id: Vec<u8>) -> Self {
103        Self {
104            r#type: "public-key".to_string(),
105            id,
106            transports: None,
107        }
108    }
109
110    /// Create with specific transports
111    pub fn with_transports(id: Vec<u8>, transports: Vec<String>) -> Self {
112        Self {
113            r#type: "public-key".to_string(),
114            id,
115            transports: Some(transports),
116        }
117    }
118}
119
120/// Public key credential parameters
121///
122/// Specifies an acceptable credential type and algorithm.
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124pub struct PublicKeyCredentialParameters {
125    /// Credential type
126    #[serde(rename = "type")]
127    pub cred_type: String,
128
129    /// COSE algorithm identifier
130    pub alg: i32,
131}
132
133impl PublicKeyCredentialParameters {
134    /// ES256 algorithm (P-256 + SHA-256)
135    pub fn es256() -> Self {
136        Self {
137            cred_type: "public-key".to_string(),
138            alg: -7,
139        }
140    }
141
142    /// Create new credential parameters
143    pub fn new(cred_type: String, alg: i32) -> Self {
144        Self { cred_type, alg }
145    }
146}
147
148/// COSE algorithm identifiers
149///
150/// Common COSE algorithm identifiers used in FIDO2.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152#[repr(i32)]
153pub enum CoseAlgorithm {
154    /// ES256 (ECDSA with P-256 and SHA-256)
155    ES256 = -7,
156    /// EdDSA
157    EdDSA = -8,
158    /// ES384 (ECDSA with P-384 and SHA-384)
159    ES384 = -35,
160    /// ES512 (ECDSA with P-521 and SHA-512)
161    ES512 = -36,
162    /// RS256 (RSASSA-PKCS1-v1_5 with SHA-256)
163    RS256 = -257,
164}
165
166impl CoseAlgorithm {
167    /// Convert to i32 value
168    pub fn to_i32(self) -> i32 {
169        self as i32
170    }
171
172    /// Create from i32 value
173    pub fn from_i32(value: i32) -> Option<Self> {
174        match value {
175            -7 => Some(Self::ES256),
176            -8 => Some(Self::EdDSA),
177            -35 => Some(Self::ES384),
178            -36 => Some(Self::ES512),
179            -257 => Some(Self::RS256),
180            _ => None,
181        }
182    }
183}
184
185/// Authenticator options
186///
187/// Boolean options that control authenticator behavior.
188#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
189pub struct AuthenticatorOptions {
190    /// Resident key (discoverable credential)
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub rk: Option<bool>,
193
194    /// User presence
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub up: Option<bool>,
197
198    /// User verification
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub uv: Option<bool>,
201}
202
203impl AuthenticatorOptions {
204    /// Create new empty options
205    pub fn new() -> Self {
206        Self::default()
207    }
208
209    /// Set resident key option
210    pub fn with_rk(mut self, rk: bool) -> Self {
211        self.rk = Some(rk);
212        self
213    }
214
215    /// Set user presence option
216    pub fn with_up(mut self, up: bool) -> Self {
217        self.up = Some(up);
218        self
219    }
220
221    /// Set user verification option
222    pub fn with_uv(mut self, uv: bool) -> Self {
223        self.uv = Some(uv);
224        self
225    }
226}
227
228/// Credential protection policy
229///
230/// Defines the level of protection for a credential.
231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
232#[repr(u8)]
233pub enum CredProtect {
234    /// User verification optional
235    UserVerificationOptional = 0x01,
236    /// User verification optional with credential ID list
237    UserVerificationOptionalWithCredentialIdList = 0x02,
238    /// User verification required
239    UserVerificationRequired = 0x03,
240}
241
242impl CredProtect {
243    /// Convert to u8 value
244    pub fn to_u8(self) -> u8 {
245        self as u8
246    }
247
248    /// Create from u8 value
249    pub fn from_u8(value: u8) -> Option<Self> {
250        match value {
251            0x01 => Some(Self::UserVerificationOptional),
252            0x02 => Some(Self::UserVerificationOptionalWithCredentialIdList),
253            0x03 => Some(Self::UserVerificationRequired),
254            _ => None,
255        }
256    }
257}
258
259/// Credential data stored by authenticator
260///
261/// Internal representation of a credential with all metadata.
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
263pub struct Credential {
264    /// Credential ID
265    #[serde(with = "serde_bytes")]
266    pub id: Vec<u8>,
267
268    /// Relying party identifier
269    pub rp_id: String,
270
271    /// Creation timestamp (Unix timestamp)
272    pub created: i64,
273
274    /// Relying party name
275    pub rp_name: Option<String>,
276
277    /// User handle
278    #[serde(with = "serde_bytes")]
279    pub user_id: Vec<u8>,
280
281    /// COSE algorithm identifier
282    pub algorithm: i32,
283
284    /// User name
285    pub user_name: Option<String>,
286
287    /// Signature counter
288    pub sign_count: u32,
289
290    /// Private key (32 bytes for P-256)
291    ///
292    /// Protected using `SecBytes` which:
293    /// - Zeros memory on drop (prevents heap retention attacks)
294    /// - Uses mlock in std builds (prevents swapping to disk)
295    /// - Provides constant-time equality
296    pub private_key: SecBytes,
297
298    /// Credential protection level
299    pub cred_protect: u8,
300
301    /// Whether this is a discoverable credential
302    pub discoverable: bool,
303
304    /// User display name
305    pub user_display_name: Option<String>,
306}
307
308impl Credential {
309    /// Create a new credential
310    #[allow(clippy::too_many_arguments)]
311    pub fn new(
312        id: Vec<u8>,
313        rp_id: String,
314        rp_name: Option<String>,
315        user_id: Vec<u8>,
316        user_name: Option<String>,
317        user_display_name: Option<String>,
318        algorithm: i32,
319        private_key: SecBytes,
320        discoverable: bool,
321    ) -> Self {
322        Self {
323            id,
324            rp_id,
325            created: current_timestamp(),
326            rp_name,
327            user_id,
328            algorithm,
329            user_name,
330            sign_count: 0,
331            private_key,
332            cred_protect: CredProtect::UserVerificationOptional.to_u8(),
333            discoverable,
334            user_display_name,
335        }
336    }
337}
338
339/// Get current Unix timestamp in seconds
340#[cfg(feature = "std")]
341fn current_timestamp() -> i64 {
342    SystemTime::now()
343        .duration_since(UNIX_EPOCH)
344        .unwrap_or_default()
345        .as_secs() as i64
346}
347
348/// Get current Unix timestamp in seconds (no_std fallback)
349#[cfg(not(feature = "std"))]
350fn current_timestamp() -> i64 {
351    // In no_std, return 0. Applications can override this by providing
352    // their own time source.
353    0
354}
355
356/// Authenticator transport types
357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358pub enum AuthenticatorTransport {
359    /// USB
360    Usb,
361    /// NFC
362    Nfc,
363    /// Bluetooth Low Energy
364    Ble,
365    /// Internal (platform authenticator)
366    Internal,
367}
368
369impl AuthenticatorTransport {
370    /// Convert to string
371    pub fn as_str(&self) -> &'static str {
372        match self {
373            Self::Usb => "usb",
374            Self::Nfc => "nfc",
375            Self::Ble => "ble",
376            Self::Internal => "internal",
377        }
378    }
379
380    /// Parse from string
381    pub fn parse(s: &str) -> Option<Self> {
382        match s {
383            "usb" => Some(Self::Usb),
384            "nfc" => Some(Self::Nfc),
385            "ble" => Some(Self::Ble),
386            "internal" => Some(Self::Internal),
387            _ => None,
388        }
389    }
390}
391
392/// Maximum PIN retry attempts before blocking
393pub const MAX_PIN_RETRIES: u8 = 8;
394
395/// Maximum UV retry attempts before blocking
396pub const MAX_UV_RETRIES: u8 = 3;
397
398/// Default minimum PIN length (Unicode code points)
399pub const DEFAULT_MIN_PIN_LENGTH: u8 = 4;
400
401/// Persistent PIN state for secure storage
402#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct PinState {
404    /// SHA-256 hash of the PIN (None if no PIN set)
405    pub pin_hash: Option<SecPinHash>,
406
407    /// Remaining PIN retry attempts (0-8)
408    pub retries: u8,
409
410    /// Remaining UV retry attempts (0-3)
411    ///
412    /// This is persisted to prevent brute-force bypass via device restart.
413    /// Uses serde default for backwards compatibility with existing stored state.
414    #[serde(default = "default_uv_retries")]
415    pub uv_retries: u8,
416
417    /// Minimum PIN length in Unicode code points (4-63)
418    pub min_pin_length: u8,
419
420    /// State version for rollback detection
421    pub version: u64,
422
423    /// Force PIN change flag
424    pub force_pin_change: bool,
425}
426
427/// Default value for UV retries for serde deserialization
428fn default_uv_retries() -> u8 {
429    MAX_UV_RETRIES
430}
431
432impl Default for PinState {
433    fn default() -> Self {
434        Self::new()
435    }
436}
437
438impl PinState {
439    /// Create a new PIN state with no PIN set
440    pub fn new() -> Self {
441        Self {
442            pin_hash: None,
443            retries: MAX_PIN_RETRIES,
444            uv_retries: MAX_UV_RETRIES,
445            min_pin_length: DEFAULT_MIN_PIN_LENGTH,
446            version: 0,
447            force_pin_change: false,
448        }
449    }
450
451    /// Check if a PIN has been set
452    pub fn is_pin_set(&self) -> bool {
453        self.pin_hash.is_some()
454    }
455
456    /// Check if PIN is blocked (no retries remaining)
457    pub fn is_blocked(&self) -> bool {
458        self.retries == 0
459    }
460
461    /// Check if UV is blocked (no UV retries remaining)
462    pub fn is_uv_blocked(&self) -> bool {
463        self.uv_retries == 0
464    }
465
466    /// Increment version for state change
467    pub fn increment_version(&mut self) {
468        self.version = self.version.saturating_add(1);
469    }
470}
471
472#[cfg(test)]
473mod tests {
474    use super::*;
475
476    #[test]
477    fn test_relying_party() {
478        let rp = RelyingParty::new("example.com".to_string());
479        assert_eq!(rp.id, "example.com");
480        assert_eq!(rp.name, None);
481
482        let rp = RelyingParty::with_name("example.com".to_string(), "Example".to_string());
483        assert_eq!(rp.name, Some("Example".to_string()));
484    }
485
486    #[test]
487    fn test_user() {
488        let user = User::new(vec![1, 2, 3, 4]);
489        assert_eq!(user.id, vec![1, 2, 3, 4]);
490        assert_eq!(user.name, None);
491
492        let user = User::with_details(
493            vec![1, 2, 3, 4],
494            "john@example.com".to_string(),
495            "John Doe".to_string(),
496        );
497        assert_eq!(user.name, Some("john@example.com".to_string()));
498        assert_eq!(user.display_name, Some("John Doe".to_string()));
499    }
500
501    #[test]
502    fn test_credential_descriptor() {
503        let desc = PublicKeyCredentialDescriptor::new(vec![1, 2, 3]);
504        assert_eq!(desc.r#type, "public-key");
505        assert_eq!(desc.id, vec![1, 2, 3]);
506        assert_eq!(desc.transports, None);
507
508        let desc =
509            PublicKeyCredentialDescriptor::with_transports(vec![1, 2, 3], vec!["usb".to_string()]);
510        assert_eq!(desc.transports, Some(vec!["usb".to_string()]));
511    }
512
513    #[test]
514    fn test_cose_algorithm() {
515        assert_eq!(CoseAlgorithm::ES256.to_i32(), -7);
516        assert_eq!(CoseAlgorithm::from_i32(-7), Some(CoseAlgorithm::ES256));
517        assert_eq!(CoseAlgorithm::from_i32(999), None);
518    }
519
520    #[test]
521    fn test_cred_protect() {
522        assert_eq!(CredProtect::UserVerificationRequired.to_u8(), 0x03);
523        assert_eq!(
524            CredProtect::from_u8(0x03),
525            Some(CredProtect::UserVerificationRequired)
526        );
527        assert_eq!(CredProtect::from_u8(0xFF), None);
528    }
529
530    #[test]
531    fn test_authenticator_options() {
532        let opts = AuthenticatorOptions::new().with_rk(true).with_uv(true);
533        assert_eq!(opts.rk, Some(true));
534        assert_eq!(opts.uv, Some(true));
535        assert_eq!(opts.up, None);
536    }
537
538    #[test]
539    fn test_credential_creation() {
540        let cred = Credential::new(
541            vec![1, 2, 3],
542            "example.com".to_string(),
543            Some("Example".to_string()),
544            vec![4, 5, 6],
545            Some("user@example.com".to_string()),
546            Some("User Name".to_string()),
547            -7,
548            SecBytes::new(vec![0u8; 32]),
549            true,
550        );
551
552        assert_eq!(cred.id, vec![1, 2, 3]);
553        assert_eq!(cred.rp_id, "example.com");
554        assert_eq!(cred.sign_count, 0);
555        assert!(cred.discoverable);
556    }
557
558    #[test]
559    fn test_authenticator_transport() {
560        assert_eq!(AuthenticatorTransport::Usb.as_str(), "usb");
561        assert_eq!(
562            AuthenticatorTransport::parse("usb"),
563            Some(AuthenticatorTransport::Usb)
564        );
565        assert_eq!(AuthenticatorTransport::parse("invalid"), None);
566    }
567
568    #[test]
569    fn test_cbor_serialization() {
570        let rp = RelyingParty::with_name("example.com".to_string(), "Example".to_string());
571        let mut buf = Vec::new();
572        let result = crate::cbor::into_writer(&rp, &mut buf);
573        assert!(result.is_ok());
574    }
575
576    #[test]
577    fn test_pin_state_uv_retries() {
578        // Default state should have MAX_UV_RETRIES
579        let state = PinState::new();
580        assert_eq!(state.uv_retries, MAX_UV_RETRIES);
581        assert!(!state.is_uv_blocked());
582
583        // Test UV blocking
584        let mut state = PinState::new();
585        state.uv_retries = 0;
586        assert!(state.is_uv_blocked());
587    }
588
589    #[test]
590    fn test_pin_state_cbor_round_trip() {
591        // Create state with specific UV retries
592        let mut state = PinState::new();
593        state.uv_retries = 1; // Only 1 retry left
594        state.retries = 5;
595        state.version = 42;
596
597        // Serialize to CBOR
598        let mut buf = Vec::new();
599        crate::cbor::into_writer(&state, &mut buf).expect("CBOR serialization failed");
600
601        // Deserialize back
602        let restored: PinState = crate::cbor::decode(&buf).expect("CBOR deserialization failed");
603
604        assert_eq!(restored.uv_retries, 1, "UV retries should be preserved");
605        assert_eq!(restored.retries, 5);
606        assert_eq!(restored.version, 42);
607    }
608}