Skip to main content

webauthn/
credential.rs

1//! Core domain types for stored credentials, challenges, and ceremony results.
2
3use std::time::{Duration, SystemTime};
4
5use ring::rand::{SecureRandom, SystemRandom};
6
7use crate::error::{Result, WebAuthnError};
8
9// ─── Public key ───────────────────────────────────────────────────────────────
10
11/// The public key extracted from a COSE key structure during registration.
12///
13/// Four algorithms are supported:
14/// - **ES256** — ECDSA P-256 with SHA-256 (COSE alg `-7`). Most common.
15/// - **ES384** — ECDSA P-384 with SHA-384 (COSE alg `-35`).
16/// - **EdDSA** — Ed25519 (COSE alg `-8`). Used by newer FIDO2 authenticators.
17/// - **RS256** — RSA PKCS#1 v1.5 with SHA-256 (COSE alg `-257`). Used by
18///   older YubiKey 4-series devices and Windows Hello.
19#[derive(Debug, Clone)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub enum PublicKey {
22    /// P-256 ECDSA public key (COSE alg `-7`, kty `2`).
23    ///
24    /// `x` and `y` are the 32-byte affine coordinates of the public point.
25    /// To obtain the 65-byte uncompressed point for ring, prepend `0x04`:
26    /// `0x04 || x (32 bytes) || y (32 bytes)`.
27    ES256 {
28        #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
29        x: Vec<u8>,
30        #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
31        y: Vec<u8>,
32    },
33
34    /// P-384 ECDSA public key (COSE alg `-35`, kty `2`).
35    ///
36    /// `x` and `y` are the 48-byte affine coordinates of the public point.
37    /// To obtain the 97-byte uncompressed point for ring, prepend `0x04`:
38    /// `0x04 || x (48 bytes) || y (48 bytes)`.
39    ES384 {
40        #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
41        x: Vec<u8>,
42        #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
43        y: Vec<u8>,
44    },
45
46    /// Ed25519 EdDSA public key (COSE alg `-8`, kty `1` OKP).
47    ///
48    /// The inner `Vec<u8>` is the raw 32-byte Ed25519 public key,
49    /// as encoded in COSE OKP key parameter `-2` (`x`).
50    EdDSA(#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))] Vec<u8>),
51
52    /// RSA PKCS#1 v1.5 SHA-256 public key (COSE alg `-257`, kty `3`).
53    ///
54    /// `n` is the big-endian modulus (256 bytes for a 2048-bit key).
55    /// `e` is the big-endian public exponent (typically `[0x01, 0x00, 0x01]`).
56    RS256 {
57        #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
58        n: Vec<u8>,
59        #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
60        e: Vec<u8>,
61    },
62}
63
64impl PublicKey {
65    /// Return the COSE algorithm identifier for this key.
66    pub fn algorithm(&self) -> i64 {
67        match self {
68            PublicKey::ES256 { .. } => crate::algorithm::COSE_ES256,
69            PublicKey::ES384 { .. } => crate::algorithm::COSE_ES384,
70            PublicKey::EdDSA(_) => crate::algorithm::COSE_EDDSA,
71            PublicKey::RS256 { .. } => crate::algorithm::COSE_RS256,
72        }
73    }
74
75    /// Return a human-readable description of the key type.
76    pub fn key_type(&self) -> &'static str {
77        match self {
78            PublicKey::ES256 { .. } => "EC2 P-256",
79            PublicKey::ES384 { .. } => "EC2 P-384",
80            PublicKey::EdDSA(_) => "OKP Ed25519",
81            PublicKey::RS256 { .. } => "RSA 2048",
82        }
83    }
84}
85
86// ─── Stored credential ────────────────────────────────────────────────────────
87
88/// A registered credential persisted on the relying-party side after a
89/// successful registration ceremony.
90///
91/// The caller is responsible for storing this in a durable, server-side store
92/// keyed by `id` (the credential ID) and associated with `user_id`.
93#[derive(Debug, Clone)]
94#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
95pub struct Credential {
96    /// Opaque byte string that uniquely identifies this credential.
97    /// Produced by the authenticator during registration.
98    #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
99    pub id: Vec<u8>,
100
101    /// The authenticator's public key in the format signalled during registration.
102    pub public_key: PublicKey,
103
104    /// Monotonically increasing counter maintained by the authenticator.
105    /// Used to detect cloned authenticators.
106    pub sign_count: u32,
107
108    /// Application-defined identifier for the user this credential belongs to.
109    #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
110    pub user_id: Vec<u8>,
111
112    /// Relying party ID (e.g. `"example.com"`).
113    /// Stored so authentication can verify the credential is bound to this RP.
114    pub rp_id: String,
115
116    /// When this credential was first registered.
117    pub created_at: SystemTime,
118
119    /// Whether this credential is eligible for backup to a platform sync
120    /// service (BE flag, §6.1 bit 3).
121    ///
122    /// This value is immutable per spec — the authenticator sets it once at
123    /// registration and it must not change in subsequent ceremonies. A change
124    /// in `backup_eligible` between registration and authentication is treated
125    /// as a credential substitution attempt and rejected with
126    /// [`crate::error::WebAuthnError::BackupEligibilityChanged`].
127    pub backup_eligible: bool,
128}
129
130// ─── Wire-format input types ──────────────────────────────────────────────────
131
132/// The response produced by the authenticator after `navigator.credentials.create()`.
133///
134/// Both fields carry the **raw decoded bytes** — base64url decoding happens
135/// outside the library before constructing this struct. This matches the
136/// ArrayBuffer values you get after calling `response.clientDataJSON` in JS.
137#[derive(Debug, Clone)]
138pub struct AuthenticatorAttestationResponse {
139    /// Raw UTF-8 bytes of the `clientDataJSON` object.
140    pub client_data_json: Vec<u8>,
141
142    /// Raw CBOR bytes of the `attestationObject`.
143    pub attestation_object: Vec<u8>,
144}
145
146// ─── Challenge ────────────────────────────────────────────────────────────────
147
148/// A single-use challenge issued by the relying party before a ceremony.
149///
150/// **Security contract**: each `Challenge` must be used at most once and must
151/// expire after a short window (typically 60–300 seconds). The caller is
152/// responsible for enforcing both properties.
153#[derive(Debug, Clone)]
154#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
155pub struct Challenge {
156    /// 32 cryptographically random bytes.
157    #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
158    pub bytes: Vec<u8>,
159
160    /// When this challenge was generated — used for expiry checks.
161    pub created_at: SystemTime,
162}
163
164impl Challenge {
165    /// Generate a fresh 32-byte challenge using the OS cryptographic RNG.
166    ///
167    /// 32 bytes provides 256 bits of entropy — far beyond any brute-force threat.
168    ///
169    /// # Errors
170    /// Returns [`WebAuthnError::InvalidClientData`] if the system RNG fails
171    /// (extremely unlikely; would indicate a kernel-level failure).
172    pub fn new() -> Result<Self> {
173        let rng = SystemRandom::new();
174        let mut bytes = vec![0u8; 32];
175        rng.fill(&mut bytes).map_err(|_| {
176            WebAuthnError::InvalidClientData(
177                "system random number generator failed to produce bytes".to_string(),
178            )
179        })?;
180        Ok(Self {
181            bytes,
182            created_at: SystemTime::now(),
183        })
184    }
185
186    /// Returns `true` if this challenge is older than `ttl_secs` seconds.
187    ///
188    /// Returns `true` if the system clock has gone backwards since the challenge
189    /// was created — treating an unverifiable age as expired is the safe default.
190    pub fn is_expired(&self, ttl_secs: u64) -> bool {
191        self.created_at
192            .elapsed()
193            .map(|age| age >= Duration::from_secs(ttl_secs))
194            .unwrap_or(true)
195    }
196}
197
198// ─── Ceremony result types ────────────────────────────────────────────────────
199
200/// Successful outcome of a registration ceremony.
201#[derive(Debug)]
202#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
203pub struct RegistrationResult {
204    /// The newly registered credential — persist this in your database.
205    pub credential: Credential,
206
207    /// What kind of attestation the authenticator provided.
208    pub attestation_type: AttestationType,
209
210    /// Whether the registered credential is eligible for backup to a platform
211    /// sync service (BE flag). This value is set by the authenticator and is
212    /// immutable — it will not change in future ceremonies for this credential.
213    pub backup_eligible: bool,
214
215    /// Whether the credential was backed up at the time of registration (BS flag).
216    pub backup_state: bool,
217}
218
219/// Successful outcome of an authentication ceremony.
220#[derive(Debug)]
221#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
222pub struct AuthenticationResult {
223    /// The credential ID used to authenticate.
224    #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
225    pub credential_id: Vec<u8>,
226
227    /// The sign count returned by the authenticator this ceremony.
228    /// Update the stored credential's `sign_count` to this value after success.
229    pub new_sign_count: u32,
230
231    /// Whether the User Present (UP) flag was set — the authenticator confirmed
232    /// that a human was at the device (button press, touch, etc.).
233    pub user_present: bool,
234
235    /// Whether the authenticator signalled that the user was verified
236    /// (biometric check, PIN, etc.) — corresponds to the UV flag.
237    pub user_verified: bool,
238
239    /// Whether the credential is eligible for backup (BE flag).
240    /// Should remain constant across authentications for a given credential.
241    pub backup_eligible: bool,
242
243    /// Whether the credential is currently backed up (BS flag).
244    /// May change between ceremonies as backup state varies.
245    pub backup_state: bool,
246}
247
248/// The level of attestation the authenticator provided.
249#[derive(Debug, PartialEq, Eq)]
250#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
251pub enum AttestationType {
252    /// The authenticator explicitly provided no attestation (`"fmt": "none"`).
253    /// The credential is still usable, but device provenance cannot be verified.
254    None,
255
256    /// The attestation was signed by the same key used for authentication
257    /// (self-attestation). Proves the credential is fresh but not the device model.
258    SelfAttestation,
259
260    /// The attestation was signed by a separate attestation key with an `x5c`
261    /// certificate chain present. The certificate chain is **not** verified by
262    /// this library (no trust-anchor set or FIDO MDS integration). The credential
263    /// is accepted, but device provenance is unverified.
264    Basic,
265}
266
267// ─── Serde round-trip tests ───────────────────────────────────────────────────
268
269#[cfg(all(test, feature = "serde"))]
270mod serde_tests {
271    use std::time::{Duration, SystemTime};
272
273    use super::*;
274
275    fn epoch_plus(secs: u64) -> SystemTime {
276        SystemTime::UNIX_EPOCH + Duration::from_secs(secs)
277    }
278
279    #[test]
280    fn challenge_round_trips() {
281        let c = Challenge {
282            bytes: vec![0xDE, 0xAD, 0xBE, 0xEF],
283            created_at: epoch_plus(1_700_000_000),
284        };
285        let json = serde_json::to_string(&c).unwrap();
286        let back: Challenge = serde_json::from_str(&json).unwrap();
287        assert_eq!(back.bytes, c.bytes);
288        assert_eq!(back.created_at, c.created_at);
289    }
290
291    #[test]
292    fn public_key_es256_round_trips() {
293        let key = PublicKey::ES256 {
294            x: vec![0x01u8; 32],
295            y: vec![0x02u8; 32],
296        };
297        let json = serde_json::to_string(&key).unwrap();
298        let back: PublicKey = serde_json::from_str(&json).unwrap();
299        match back {
300            PublicKey::ES256 { x, y } => {
301                assert_eq!(x, vec![0x01u8; 32]);
302                assert_eq!(y, vec![0x02u8; 32]);
303            }
304            _ => panic!("wrong variant"),
305        }
306    }
307
308    #[test]
309    fn public_key_eddsa_round_trips() {
310        let key = PublicKey::EdDSA(vec![0x03u8; 32]);
311        let json = serde_json::to_string(&key).unwrap();
312        let back: PublicKey = serde_json::from_str(&json).unwrap();
313        match back {
314            PublicKey::EdDSA(bytes) => assert_eq!(bytes, vec![0x03u8; 32]),
315            _ => panic!("wrong variant"),
316        }
317    }
318
319    #[test]
320    fn public_key_rs256_round_trips() {
321        let key = PublicKey::RS256 {
322            n: vec![0x04u8; 256],
323            e: vec![0x01, 0x00, 0x01],
324        };
325        let json = serde_json::to_string(&key).unwrap();
326        let back: PublicKey = serde_json::from_str(&json).unwrap();
327        match back {
328            PublicKey::RS256 { n, e } => {
329                assert_eq!(n, vec![0x04u8; 256]);
330                assert_eq!(e, vec![0x01, 0x00, 0x01]);
331            }
332            _ => panic!("wrong variant"),
333        }
334    }
335
336    #[test]
337    fn credential_round_trips() {
338        let cred = Credential {
339            id: vec![0xAAu8; 16],
340            public_key: PublicKey::ES256 {
341                x: vec![0x01u8; 32],
342                y: vec![0x02u8; 32],
343            },
344            sign_count: 42,
345            user_id: vec![0xBBu8; 8],
346            rp_id: "example.com".to_string(),
347            created_at: epoch_plus(1_700_000_000),
348            backup_eligible: true,
349        };
350        let json = serde_json::to_string(&cred).unwrap();
351        let back: Credential = serde_json::from_str(&json).unwrap();
352        assert_eq!(back.id, cred.id);
353        assert_eq!(back.sign_count, cred.sign_count);
354        assert_eq!(back.user_id, cred.user_id);
355        assert_eq!(back.rp_id, cred.rp_id);
356        assert_eq!(back.created_at, cred.created_at);
357        assert_eq!(back.backup_eligible, cred.backup_eligible);
358    }
359}