Skip to main content

auth_framework/protocols/
fido1.rs

1//! FIDO U2F (Universal 2nd Factor) protocol support.
2//!
3//! Provides U2F registration and authentication challenge/response data
4//! structures and verification logic following the FIDO U2F specification.
5
6use crate::errors::{AuthError, Result};
7use base64::Engine;
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10use std::collections::HashMap;
11
12/// U2F application identity (facet).
13const U2F_VERSION: &str = "U2F_V2";
14
15/// U2F registration request sent to the authenticator.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct U2fRegistrationRequest {
18    pub app_id: String,
19    pub challenge: String,
20}
21
22/// U2F registration response from the authenticator.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct U2fRegistrationResponse {
25    pub registration_data: Vec<u8>,
26    pub client_data: Vec<u8>,
27}
28
29/// Parsed U2F registration result containing the key handle and public key.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct U2fRegistration {
32    pub key_handle: Vec<u8>,
33    pub public_key: Vec<u8>,
34    pub attestation_cert: Vec<u8>,
35}
36
37/// U2F authentication (sign) request.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct U2fSignRequest {
40    pub app_id: String,
41    pub challenge: String,
42    pub key_handle: Vec<u8>,
43}
44
45/// U2F authentication (sign) response from the authenticator.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct U2fSignResponse {
48    pub signature_data: Vec<u8>,
49    pub client_data: Vec<u8>,
50    pub key_handle: Vec<u8>,
51}
52
53/// Parsed client data from U2F operations.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct U2fClientData {
56    pub typ: String,
57    pub challenge: String,
58    pub origin: String,
59}
60
61/// Manager for U2F registration and authentication flows.
62pub struct U2fManager {
63    app_id: String,
64    registrations: HashMap<String, Vec<U2fRegistration>>,
65}
66
67impl U2fManager {
68    /// Create a new U2F manager for the given application ID.
69    ///
70    /// The `app_id` should be the application's origin (e.g., `https://example.com`).
71    pub fn new(app_id: &str) -> Result<Self> {
72        if app_id.is_empty() {
73            return Err(AuthError::validation("App ID cannot be empty"));
74        }
75        Ok(Self {
76            app_id: app_id.to_string(),
77            registrations: HashMap::new(),
78        })
79    }
80
81    /// Generate a registration challenge for a new U2F device.
82    pub fn generate_registration_challenge(&self) -> Result<U2fRegistrationRequest> {
83        let challenge = generate_challenge()?;
84        Ok(U2fRegistrationRequest {
85            app_id: self.app_id.clone(),
86            challenge,
87        })
88    }
89
90    /// Verify a U2F registration response and store the credential.
91    ///
92    /// Per the U2F spec, the registration response contains:
93    /// - 1 byte: reserved (0x05)
94    /// - 65 bytes: user public key (uncompressed P-256)
95    /// - 1 byte: key handle length
96    /// - N bytes: key handle
97    /// - remaining: attestation certificate + signature
98    ///
99    /// # Example
100    /// ```rust,ignore
101    /// let request = authenticator.generate_registration_challenge()?;
102    /// // … user interacts with the U2F device …
103    /// let registration = authenticator.verify_registration("user1", &request, &response)?;
104    /// ```
105    pub fn verify_registration(
106        &mut self,
107        user_id: &str,
108        request: &U2fRegistrationRequest,
109        response: &U2fRegistrationResponse,
110    ) -> Result<U2fRegistration> {
111        // Validate client data
112        let client_data: U2fClientData = serde_json::from_slice(&response.client_data)
113            .map_err(|e| AuthError::validation(format!("Invalid client data: {e}")))?;
114
115        if client_data.typ != "navigator.id.finishEnrollment" {
116            return Err(AuthError::validation(
117                "Invalid client data type for registration",
118            ));
119        }
120        if client_data.challenge != request.challenge {
121            return Err(AuthError::validation("Challenge mismatch"));
122        }
123
124        // Parse registration data
125        let data = &response.registration_data;
126        if data.len() < 67 {
127            return Err(AuthError::validation("Registration data too short"));
128        }
129        if data[0] != 0x05 {
130            return Err(AuthError::validation(
131                "Invalid reserved byte (expected 0x05)",
132            ));
133        }
134
135        let public_key = data[1..66].to_vec();
136        let key_handle_len = data[66] as usize;
137
138        if data.len() < 67 + key_handle_len {
139            return Err(AuthError::validation("Registration data truncated"));
140        }
141
142        let key_handle = data[67..67 + key_handle_len].to_vec();
143        let attestation_cert = data[67 + key_handle_len..].to_vec();
144
145        let registration = U2fRegistration {
146            key_handle,
147            public_key,
148            attestation_cert,
149        };
150
151        self.registrations
152            .entry(user_id.to_string())
153            .or_default()
154            .push(registration.clone());
155
156        Ok(registration)
157    }
158
159    /// Generate an authentication challenge for a registered user.
160    pub fn generate_sign_challenge(&self, user_id: &str) -> Result<Vec<U2fSignRequest>> {
161        let regs = self
162            .registrations
163            .get(user_id)
164            .ok_or_else(|| AuthError::validation("No registrations found for user"))?;
165
166        let challenge = generate_challenge()?;
167
168        Ok(regs
169            .iter()
170            .map(|reg| U2fSignRequest {
171                app_id: self.app_id.clone(),
172                challenge: challenge.clone(),
173                key_handle: reg.key_handle.clone(),
174            })
175            .collect())
176    }
177
178    /// Verify a U2F authentication response.
179    ///
180    /// The signature data contains:
181    /// - 1 byte: user presence
182    /// - 4 bytes: counter (big-endian)
183    /// - remaining: ECDSA signature
184    pub fn verify_authentication(
185        &self,
186        user_id: &str,
187        request: &U2fSignRequest,
188        response: &U2fSignResponse,
189    ) -> Result<u32> {
190        // Validate client data
191        let client_data: U2fClientData = serde_json::from_slice(&response.client_data)
192            .map_err(|e| AuthError::validation(format!("Invalid client data: {e}")))?;
193
194        if client_data.typ != "navigator.id.getAssertion" {
195            return Err(AuthError::validation(
196                "Invalid client data type for authentication",
197            ));
198        }
199        if client_data.challenge != request.challenge {
200            return Err(AuthError::validation("Challenge mismatch"));
201        }
202
203        // Find matching registration
204        let regs = self
205            .registrations
206            .get(user_id)
207            .ok_or_else(|| AuthError::validation("No registrations found"))?;
208
209        let _reg = regs
210            .iter()
211            .find(|r| r.key_handle == response.key_handle)
212            .ok_or_else(|| AuthError::validation("Unknown key handle"))?;
213
214        // Parse signature data
215        if response.signature_data.len() < 5 {
216            return Err(AuthError::validation("Signature data too short"));
217        }
218
219        let user_presence = response.signature_data[0];
220        if user_presence & 0x01 == 0 {
221            return Err(AuthError::validation("User presence not asserted"));
222        }
223
224        let counter = u32::from_be_bytes([
225            response.signature_data[1],
226            response.signature_data[2],
227            response.signature_data[3],
228            response.signature_data[4],
229        ]);
230
231        // Verify ECDSA P-256 signature over the signed data.
232        // Per the U2F spec, the signed data is:
233        //   application_parameter (32) | user_presence (1) | counter (4) | client_data_hash (32)
234        let app_param = self.app_param();
235        let client_data_hash: [u8; 32] = {
236            let mut hasher = Sha256::new();
237            hasher.update(&response.client_data);
238            hasher.finalize().into()
239        };
240
241        let mut signed_data = Vec::with_capacity(69);
242        signed_data.extend_from_slice(&app_param);
243        signed_data.push(user_presence);
244        signed_data.extend_from_slice(&response.signature_data[1..5]);
245        signed_data.extend_from_slice(&client_data_hash);
246
247        let signature = &response.signature_data[5..];
248        let public_key = ring::signature::UnparsedPublicKey::new(
249            &ring::signature::ECDSA_P256_SHA256_ASN1,
250            &_reg.public_key,
251        );
252        public_key
253            .verify(&signed_data, signature)
254            .map_err(|_| AuthError::crypto("ECDSA P-256 signature verification failed"))?;
255
256        Ok(counter)
257    }
258
259    /// Get all registrations for a user.
260    pub fn get_registrations(&self, user_id: &str) -> Option<&Vec<U2fRegistration>> {
261        self.registrations.get(user_id)
262    }
263
264    /// Remove a specific key handle registration.
265    pub fn remove_registration(&mut self, user_id: &str, key_handle: &[u8]) -> bool {
266        if let Some(regs) = self.registrations.get_mut(user_id) {
267            let before = regs.len();
268            regs.retain(|r| r.key_handle != key_handle);
269            regs.len() < before
270        } else {
271            false
272        }
273    }
274
275    /// Compute the application parameter (SHA-256 of app_id).
276    pub fn app_param(&self) -> [u8; 32] {
277        let mut hasher = Sha256::new();
278        hasher.update(self.app_id.as_bytes());
279        hasher.finalize().into()
280    }
281
282    /// Get the U2F version string.
283    pub fn version(&self) -> &'static str {
284        U2F_VERSION
285    }
286}
287
288/// Generate a cryptographically random challenge (32 bytes, base64url-encoded).
289fn generate_challenge() -> Result<String> {
290    use ring::rand::{SecureRandom, SystemRandom};
291    let rng = SystemRandom::new();
292    let mut buf = [0u8; 32];
293    rng.fill(&mut buf)
294        .map_err(|_| AuthError::crypto("Failed to generate challenge".to_string()))?;
295    Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(buf))
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn test_u2f_manager_creation() {
304        let mgr = U2fManager::new("https://example.com").unwrap();
305        assert_eq!(mgr.app_id, "https://example.com");
306        assert_eq!(mgr.version(), "U2F_V2");
307    }
308
309    #[test]
310    fn test_empty_app_id_rejected() {
311        assert!(U2fManager::new("").is_err());
312    }
313
314    #[test]
315    fn test_registration_challenge_generation() {
316        let mgr = U2fManager::new("https://example.com").unwrap();
317        let req = mgr.generate_registration_challenge().unwrap();
318        assert_eq!(req.app_id, "https://example.com");
319        assert!(!req.challenge.is_empty());
320    }
321
322    #[test]
323    fn test_challenge_uniqueness() {
324        let mgr = U2fManager::new("https://example.com").unwrap();
325        let c1 = mgr.generate_registration_challenge().unwrap();
326        let c2 = mgr.generate_registration_challenge().unwrap();
327        assert_ne!(c1.challenge, c2.challenge);
328    }
329
330    #[test]
331    fn test_verify_registration_invalid_reserved_byte() {
332        let mut mgr = U2fManager::new("https://example.com").unwrap();
333        let req = mgr.generate_registration_challenge().unwrap();
334        let client_data = serde_json::json!({
335            "typ": "navigator.id.finishEnrollment",
336            "challenge": req.challenge,
337            "origin": "https://example.com"
338        });
339        let response = U2fRegistrationResponse {
340            registration_data: vec![0x04; 100], // wrong reserved byte
341            client_data: serde_json::to_vec(&client_data).unwrap(),
342        };
343        assert!(mgr.verify_registration("user1", &req, &response).is_err());
344    }
345
346    #[test]
347    fn test_verify_registration_valid() {
348        let mut mgr = U2fManager::new("https://example.com").unwrap();
349        let req = mgr.generate_registration_challenge().unwrap();
350
351        // Build minimal valid registration data
352        let mut reg_data = vec![0x05]; // reserved
353        reg_data.extend_from_slice(&[0xAA; 65]); // public key (65 bytes)
354        reg_data.push(4); // key handle length
355        reg_data.extend_from_slice(&[0xBB; 4]); // key handle
356        reg_data.extend_from_slice(&[0xCC; 10]); // attestation cert stub
357
358        let client_data = serde_json::json!({
359            "typ": "navigator.id.finishEnrollment",
360            "challenge": req.challenge,
361            "origin": "https://example.com"
362        });
363
364        let response = U2fRegistrationResponse {
365            registration_data: reg_data,
366            client_data: serde_json::to_vec(&client_data).unwrap(),
367        };
368
369        let reg = mgr.verify_registration("user1", &req, &response).unwrap();
370        assert_eq!(reg.key_handle, vec![0xBB; 4]);
371        assert_eq!(reg.public_key.len(), 65);
372        assert!(mgr.get_registrations("user1").is_some());
373    }
374
375    #[test]
376    fn test_sign_challenge_no_registrations() {
377        let mgr = U2fManager::new("https://example.com").unwrap();
378        assert!(mgr.generate_sign_challenge("unknown").is_err());
379    }
380
381    #[test]
382    fn test_verify_auth_user_presence() {
383        let mut mgr = U2fManager::new("https://example.com").unwrap();
384        let req = mgr.generate_registration_challenge().unwrap();
385
386        // Register first
387        let mut reg_data = vec![0x05];
388        reg_data.extend_from_slice(&[0xAA; 65]);
389        reg_data.push(4);
390        reg_data.extend_from_slice(&[0xBB; 4]);
391        reg_data.extend_from_slice(&[0xCC; 10]);
392
393        let client_data = serde_json::json!({
394            "typ": "navigator.id.finishEnrollment",
395            "challenge": req.challenge,
396            "origin": "https://example.com"
397        });
398        let reg_response = U2fRegistrationResponse {
399            registration_data: reg_data,
400            client_data: serde_json::to_vec(&client_data).unwrap(),
401        };
402        mgr.verify_registration("user1", &req, &reg_response)
403            .unwrap();
404
405        // Now test authentication
406        let sign_reqs = mgr.generate_sign_challenge("user1").unwrap();
407        let sign_req = &sign_reqs[0];
408
409        // No user presence (byte 0 = 0x00)
410        let mut sig_data = vec![0x00]; // no user presence
411        sig_data.extend_from_slice(&[0, 0, 0, 1]); // counter=1
412        sig_data.extend_from_slice(&[0xFF; 10]); // signature stub
413
414        let auth_client_data = serde_json::json!({
415            "typ": "navigator.id.getAssertion",
416            "challenge": sign_req.challenge,
417            "origin": "https://example.com"
418        });
419        let sign_response = U2fSignResponse {
420            signature_data: sig_data,
421            client_data: serde_json::to_vec(&auth_client_data).unwrap(),
422            key_handle: vec![0xBB; 4],
423        };
424
425        assert!(
426            mgr.verify_authentication("user1", sign_req, &sign_response)
427                .is_err()
428        );
429    }
430
431    #[test]
432    fn test_verify_auth_success() {
433        use ring::rand::SystemRandom;
434        use ring::signature::{ECDSA_P256_SHA256_ASN1_SIGNING, EcdsaKeyPair, KeyPair};
435
436        let rng = SystemRandom::new();
437        let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
438        let key_pair =
439            EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, pkcs8.as_ref(), &rng)
440                .unwrap();
441        let public_key_bytes = key_pair.public_key().as_ref().to_vec(); // 65 bytes
442
443        let mut mgr = U2fManager::new("https://example.com").unwrap();
444        let req = mgr.generate_registration_challenge().unwrap();
445
446        let mut reg_data = vec![0x05];
447        reg_data.extend_from_slice(&public_key_bytes);
448        reg_data.push(4);
449        reg_data.extend_from_slice(&[0xBB; 4]);
450        reg_data.extend_from_slice(&[0xCC; 10]);
451
452        let client_data = serde_json::json!({
453            "typ": "navigator.id.finishEnrollment",
454            "challenge": req.challenge,
455            "origin": "https://example.com"
456        });
457        let reg_resp = U2fRegistrationResponse {
458            registration_data: reg_data,
459            client_data: serde_json::to_vec(&client_data).unwrap(),
460        };
461        mgr.verify_registration("user1", &req, &reg_resp).unwrap();
462
463        let sign_reqs = mgr.generate_sign_challenge("user1").unwrap();
464        let sign_req = &sign_reqs[0];
465
466        // Build the signed data the same way verify_authentication does
467        let app_param = mgr.app_param();
468        let auth_client = serde_json::json!({
469            "typ": "navigator.id.getAssertion",
470            "challenge": sign_req.challenge,
471            "origin": "https://example.com"
472        });
473        let auth_client_bytes = serde_json::to_vec(&auth_client).unwrap();
474        let client_data_hash: [u8; 32] = {
475            let mut hasher = Sha256::new();
476            hasher.update(&auth_client_bytes);
477            hasher.finalize().into()
478        };
479
480        let user_presence: u8 = 0x01;
481        let counter_bytes: [u8; 4] = 5u32.to_be_bytes();
482
483        let mut signed_data = Vec::with_capacity(69);
484        signed_data.extend_from_slice(&app_param);
485        signed_data.push(user_presence);
486        signed_data.extend_from_slice(&counter_bytes);
487        signed_data.extend_from_slice(&client_data_hash);
488
489        let signature = key_pair.sign(&rng, &signed_data).unwrap();
490
491        let mut sig_data = vec![user_presence];
492        sig_data.extend_from_slice(&counter_bytes);
493        sig_data.extend_from_slice(signature.as_ref());
494
495        let sign_resp = U2fSignResponse {
496            signature_data: sig_data,
497            client_data: auth_client_bytes,
498            key_handle: vec![0xBB; 4],
499        };
500
501        let counter = mgr
502            .verify_authentication("user1", sign_req, &sign_resp)
503            .unwrap();
504        assert_eq!(counter, 5);
505    }
506
507    #[test]
508    fn test_remove_registration() {
509        let mut mgr = U2fManager::new("https://example.com").unwrap();
510        let req = mgr.generate_registration_challenge().unwrap();
511
512        let mut reg_data = vec![0x05];
513        reg_data.extend_from_slice(&[0xAA; 65]);
514        reg_data.push(4);
515        reg_data.extend_from_slice(&[0xBB; 4]);
516        reg_data.extend_from_slice(&[0xCC; 10]);
517
518        let client_data = serde_json::json!({
519            "typ": "navigator.id.finishEnrollment",
520            "challenge": req.challenge,
521            "origin": "https://example.com"
522        });
523        let resp = U2fRegistrationResponse {
524            registration_data: reg_data,
525            client_data: serde_json::to_vec(&client_data).unwrap(),
526        };
527        mgr.verify_registration("user1", &req, &resp).unwrap();
528
529        assert!(mgr.remove_registration("user1", &[0xBB; 4]));
530        assert_eq!(mgr.get_registrations("user1").unwrap().len(), 0);
531    }
532
533    #[test]
534    fn test_app_param() {
535        let mgr = U2fManager::new("https://example.com").unwrap();
536        let param = mgr.app_param();
537        assert_eq!(param.len(), 32);
538        // Same app_id should yield same param
539        let param2 = mgr.app_param();
540        assert_eq!(param, param2);
541    }
542}