webauthn_authenticator_rs/
softpasskey.rs

1#[cfg(doc)]
2use crate::stubs::*;
3
4use crate::authenticator_hashed::AuthenticatorBackendHashedClientData;
5use crate::crypto::{compute_sha256, get_group};
6use crate::error::WebauthnCError;
7use crate::BASE64_ENGINE;
8use base64::Engine;
9use openssl::{bn, ec, hash, pkey, rand, sign};
10use serde_cbor_2::value::Value;
11use std::collections::BTreeMap;
12use std::collections::HashMap;
13use std::iter;
14
15use base64urlsafedata::Base64UrlSafeData;
16
17use webauthn_rs_proto::{
18    AllowCredentials, AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw,
19    AuthenticatorAttachment, AuthenticatorAttestationResponseRaw, PublicKeyCredential,
20    PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions,
21    RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs, UserVerificationPolicy,
22};
23
24pub struct SoftPasskey {
25    tokens: HashMap<Vec<u8>, Vec<u8>>,
26    counter: u32,
27    falsify_uv: bool,
28}
29
30impl SoftPasskey {
31    pub fn new(falsify_uv: bool) -> Self {
32        SoftPasskey {
33            tokens: HashMap::new(),
34            counter: 0,
35            falsify_uv,
36        }
37    }
38}
39
40impl Default for SoftPasskey {
41    fn default() -> Self {
42        Self::new(false)
43    }
44}
45
46#[derive(Debug)]
47pub struct U2FSignData {
48    key_handle: Vec<u8>,
49    counter: u32,
50    signature: Vec<u8>,
51    flags: u8,
52}
53
54impl AuthenticatorBackendHashedClientData for SoftPasskey {
55    fn perform_register(
56        &mut self,
57        client_data_json_hash: Vec<u8>,
58        options: PublicKeyCredentialCreationOptions,
59        _timeout_ms: u32,
60    ) -> Result<RegisterPublicKeyCredential, WebauthnCError> {
61        // Let credTypesAndPubKeyAlgs be a new list whose items are pairs of PublicKeyCredentialType and a COSEAlgorithmIdentifier.
62        // Done in rust types.
63
64        // For each current of options.pubKeyCredParams:
65        //     If current.type does not contain a PublicKeyCredentialType supported by this implementation, then continue.
66        //     Let alg be current.alg.
67        //     Append the pair of current.type and alg to credTypesAndPubKeyAlgs.
68        let cred_types_and_pub_key_algs: Vec<_> = options
69            .pub_key_cred_params
70            .iter()
71            .filter_map(|param| {
72                if param.type_ != "public-key" {
73                    None
74                } else {
75                    Some((param.type_.clone(), param.alg))
76                }
77            })
78            .collect();
79
80        trace!("Found -> {:x?}", cred_types_and_pub_key_algs);
81
82        // If credTypesAndPubKeyAlgs is empty and options.pubKeyCredParams is not empty, return a DOMException whose name is "NotSupportedError", and terminate this algorithm.
83        if cred_types_and_pub_key_algs.is_empty() {
84            return Err(WebauthnCError::NotSupported);
85        }
86
87        // Webauthn-rs doesn't support this yet.
88        /*
89            // Let clientExtensions be a new map and let authenticatorExtensions be a new map.
90
91            // If the extensions member of options is present, then for each extensionId → clientExtensionInput of options.extensions:
92            //     If extensionId is not supported by this client platform or is not a registration extension, then continue.
93            //     Set clientExtensions[extensionId] to clientExtensionInput.
94            //     If extensionId is not an authenticator extension, then continue.
95            //     Let authenticatorExtensionInput be the (CBOR) result of running extensionId’s client extension processing algorithm on clientExtensionInput. If the algorithm returned an error, continue.
96            //     Set authenticatorExtensions[extensionId] to the base64url encoding of authenticatorExtensionInput.
97        */
98
99        // Not required.
100        // If the options.signal is present and its aborted flag is set to true, return a DOMException whose name is "AbortError" and terminate this algorithm.
101
102        // Let issuedRequests be a new ordered set.
103
104        // Let authenticators represent a value which at any given instant is a set of client platform-specific handles, where each item identifies an authenticator presently available on this client platform at that instant.
105
106        // Start lifetimeTimer.
107
108        // While lifetimeTimer has not expired, perform the following actions depending upon lifetimeTimer, and the state and response for each authenticator in authenticators:
109
110        //    If lifetimeTimer expires,
111        //        For each authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove authenticator from issuedRequests.
112
113        //    If the user exercises a user agent user-interface option to cancel the process,
114        //        For each authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove authenticator from issuedRequests. Return a DOMException whose name is "NotAllowedError".
115
116        //    If the options.signal is present and its aborted flag is set to true,
117        //        For each authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove authenticator from issuedRequests. Then return a DOMException whose name is "AbortError" and terminate this algorithm.
118
119        //    If an authenticator becomes available on this client device,
120        //         If options.authenticatorSelection is present:
121        //             If options.authenticatorSelection.authenticatorAttachment is present and its value is not equal to authenticator’s authenticator attachment modality, continue.
122        //             If options.authenticatorSelection.requireResidentKey is set to true and the authenticator is not capable of storing a client-side-resident public key credential source, continue.
123        //             If options.authenticatorSelection.userVerification is set to required and the authenticator is not capable of performing user verification, continue.
124        //          Let userVerification be the effective user verification requirement for credential creation, a Boolean value, as follows. If options.authenticatorSelection.userVerification
125        //              is set to required -> Let userVerification be true.
126        //              is set to preferred
127        //                  If the authenticator
128        //                      is capable of user verification -> Let userVerification be true.
129        //                      is not capable of user verification -> Let userVerification be false.
130        //              is set to discouraged -> Let userVerification be false.
131        //          Let userPresence be a Boolean value set to the inverse of userVerification.
132        //          Let excludeCredentialDescriptorList be a new list.
133        //          For each credential descriptor C in options.excludeCredentials:
134        //              If C.transports is not empty, and authenticator is connected over a transport not mentioned in C.transports, the client MAY continue.
135        //              Otherwise, Append C to excludeCredentialDescriptorList.
136        //          Invoke the authenticatorMakeCredential operation on authenticator with clientDataHash, options.rp, options.user, options.authenticatorSelection.requireResidentKey, userPresence, userVerification, credTypesAndPubKeyAlgs, excludeCredentialDescriptorList, and authenticatorExtensions as parameters.
137
138        //          Append authenticator to issuedRequests.
139
140        //    If an authenticator ceases to be available on this client device,
141        //         Remove authenticator from issuedRequests.
142
143        //    If any authenticator returns a status indicating that the user cancelled the operation,
144        //         Remove authenticator from issuedRequests.
145        //         For each remaining authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove it from issuedRequests.
146
147        //    If any authenticator returns an error status equivalent to "InvalidStateError",
148        //         Remove authenticator from issuedRequests.
149        //         For each remaining authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove it from issuedRequests.
150        //         Return a DOMException whose name is "InvalidStateError" and terminate this algorithm.
151
152        //    If any authenticator returns an error status not equivalent to "InvalidStateError",
153        //         Remove authenticator from issuedRequests.
154
155        //    If any authenticator indicates success,
156        //         Remove authenticator from issuedRequests.
157        //         Let credentialCreationData be a struct whose items are:
158        //         Let constructCredentialAlg be an algorithm that takes a global object global, and whose steps are:
159
160        //         Let attestationObject be a new ArrayBuffer, created using global’s %ArrayBuffer%, containing the bytes of credentialCreationData.attestationObjectResult’s value.
161
162        //         Let id be attestationObject.authData.attestedCredentialData.credentialId.
163        //         Let pubKeyCred be a new PublicKeyCredential object associated with global whose fields are:
164        //         For each remaining authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove it from issuedRequests.
165        //         Return constructCredentialAlg and terminate this algorithm.
166
167        // For our needs, we let the u2f auth library handle the above, but currently it can't accept
168        // verified devices for u2f with ctap1/2. We may need to change u2f/authenticator library in the future.
169        // As a result this really limits our usage to certain device classes. This is why we implement
170        // this section in a seperate function call.
171
172        let (platform_attached, resident_key, user_verification) =
173            match &options.authenticator_selection {
174                Some(auth_sel) => {
175                    let pa = auth_sel
176                        .authenticator_attachment
177                        .as_ref()
178                        .map(|v| v == &AuthenticatorAttachment::Platform)
179                        .unwrap_or(false);
180                    let uv = auth_sel.user_verification == UserVerificationPolicy::Required;
181                    (pa, auth_sel.require_resident_key, uv)
182                }
183                None => (false, false, false),
184            };
185
186        let rp_id_hash = compute_sha256(options.rp.id.as_bytes()).to_vec();
187
188        // =====
189
190        if user_verification && !self.falsify_uv {
191            error!("User Verification not supported by softtoken");
192            return Err(WebauthnCError::NotSupported);
193        }
194
195        if platform_attached {
196            error!("Platform Attachement not supported by softtoken");
197            return Err(WebauthnCError::NotSupported);
198        }
199
200        if resident_key {
201            error!("Resident Keys not supported by softtoken");
202            return Err(WebauthnCError::NotSupported);
203        }
204
205        // Generate a random credential id
206        let mut key_handle: Vec<u8> = Vec::with_capacity(32);
207        key_handle.resize_with(32, Default::default);
208        rand::rand_bytes(key_handle.as_mut_slice())?;
209
210        // Create a new key.
211        let ecgroup = get_group()?;
212
213        let eckey = ec::EcKey::generate(&ecgroup)?;
214
215        // Extract the public x and y coords.
216        let ecpub_points = eckey.public_key();
217
218        let mut bnctx = bn::BigNumContext::new()?;
219
220        let mut xbn = bn::BigNum::new()?;
221
222        let mut ybn = bn::BigNum::new()?;
223
224        ecpub_points.affine_coordinates_gfp(&ecgroup, &mut xbn, &mut ybn, &mut bnctx)?;
225
226        let mut public_key_x = Vec::with_capacity(32);
227        let mut public_key_y = Vec::with_capacity(32);
228
229        public_key_x.resize(32, 0);
230        public_key_y.resize(32, 0);
231
232        let xbnv = xbn.to_vec();
233        let ybnv = ybn.to_vec();
234
235        let (_pad, x_fill) = public_key_x.split_at_mut(32 - xbnv.len());
236        x_fill.copy_from_slice(&xbnv);
237
238        let (_pad, y_fill) = public_key_y.split_at_mut(32 - ybnv.len());
239        y_fill.copy_from_slice(&ybnv);
240
241        // Extract the DER cert for later
242        let ecpriv_der = eckey.private_key_to_der()?;
243
244        // Now setup to sign.
245        let pkey = pkey::PKey::from_ec_key(eckey)?;
246
247        let mut signer = sign::Signer::new(hash::MessageDigest::sha256(), &pkey)?;
248
249        // =====
250
251        // From the u2f response, we now need to assemble the attestation object now.
252
253        // cbor encode the public key. We already decomposed this, so just create
254        // the correct bytes.
255        let mut map = BTreeMap::new();
256        // KeyType -> EC2
257        map.insert(Value::Integer(1), Value::Integer(2));
258        // Alg -> ES256
259        map.insert(Value::Integer(3), Value::Integer(-7));
260
261        // Curve -> P-256
262        map.insert(Value::Integer(-1), Value::Integer(1));
263        // EC X coord
264        map.insert(Value::Integer(-2), Value::Bytes(public_key_x));
265        // EC Y coord
266        map.insert(Value::Integer(-3), Value::Bytes(public_key_y));
267
268        let pk_cbor = Value::Map(map);
269        let pk_cbor_bytes = serde_cbor_2::to_vec(&pk_cbor).map_err(|e| {
270            error!("PK CBOR -> {:x?}", e);
271            WebauthnCError::Cbor
272        })?;
273
274        let key_handle_len: u16 = u16::try_from(key_handle.len()).map_err(|e| {
275            error!("CBOR kh len is not u16 -> {:x?}", e);
276            WebauthnCError::Cbor
277        })?;
278
279        // combine aaGuid, KeyHandle, CborPubKey into a AttestedCredentialData. (acd)
280        let aaguid: [u8; 16] = [0; 16];
281
282        // make a 00 aaguid
283        let khlen_be_bytes = key_handle_len.to_be_bytes();
284        let acd_iter = aaguid
285            .iter()
286            .chain(khlen_be_bytes.iter())
287            .copied()
288            .chain(key_handle.iter().copied())
289            .chain(pk_cbor_bytes.iter().copied());
290
291        // set counter to 0 during create
292        // Combine rp_id_hash, flags, counter, acd, into authenticator data.
293        // The flags are always user_present, att present
294        let flags = if user_verification {
295            0b01000101
296        } else {
297            0b01000001
298        };
299
300        let authdata: Vec<u8> = rp_id_hash
301            .iter()
302            .copied()
303            .chain(iter::once(flags))
304            .chain(
305                // A 0 u32 counter
306                iter::repeat(0).take(4),
307            )
308            .chain(acd_iter)
309            .collect();
310
311        // 4.b. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash using the credential public key with alg.
312
313        let verification_data: Vec<u8> = authdata
314            .iter()
315            .chain(client_data_json_hash.iter())
316            .copied()
317            .collect();
318
319        // Do the signature
320        let signature = signer
321            .update(verification_data.as_slice())
322            .and_then(|_| signer.sign_to_vec())?;
323
324        let mut attest_map = BTreeMap::new();
325
326        /*
327        match options.attestation {
328            None | Some(AttestationConveyancePreference::None) => {
329            }
330            Some(AttestationConveyancePreference::Indirect)
331            | Some(AttestationConveyancePreference::Direct) => {
332                todo!();
333            }
334        }
335        */
336
337        attest_map.insert(
338            Value::Text("fmt".to_string()),
339            Value::Text("packed".to_string()),
340        );
341        let mut att_stmt_map = BTreeMap::new();
342        att_stmt_map.insert(Value::Text("alg".to_string()), Value::Integer(-7));
343        att_stmt_map.insert(Value::Text("sig".to_string()), Value::Bytes(signature));
344
345        attest_map.insert(Value::Text("attStmt".to_string()), Value::Map(att_stmt_map));
346        attest_map.insert(Value::Text("authData".to_string()), Value::Bytes(authdata));
347
348        let ao = Value::Map(attest_map);
349
350        let ao_bytes = serde_cbor_2::to_vec(&ao).map_err(|e| {
351            error!("AO CBOR -> {:x?}", e);
352            WebauthnCError::Cbor
353        })?;
354
355        // Return a DOMException whose name is "NotAllowedError". In order to prevent information leak that could identify the user without consent, this step MUST NOT be executed before lifetimeTimer has expired. See §14.5 Registration Ceremony Privacy for details.
356
357        // Okay, now persist the token. We shouldn't fail from here.
358        self.tokens.insert(key_handle.clone(), ecpriv_der);
359
360        let rego = RegisterPublicKeyCredential {
361            id: BASE64_ENGINE.encode(&key_handle),
362            raw_id: key_handle.into(),
363            response: AuthenticatorAttestationResponseRaw {
364                attestation_object: ao_bytes.into(),
365                client_data_json: Base64UrlSafeData::new(),
366                transports: None,
367            },
368            type_: "public-key".to_string(),
369            extensions: RegistrationExtensionsClientOutputs::default(),
370        };
371
372        trace!("rego  -> {:x?}", rego);
373        Ok(rego)
374    }
375
376    fn perform_auth(
377        &mut self,
378        client_data_json_hash: Vec<u8>,
379        options: PublicKeyCredentialRequestOptions,
380        timeout_ms: u32,
381    ) -> Result<PublicKeyCredential, WebauthnCError> {
382        // Let clientExtensions be a new map and let authenticatorExtensions be a new map.
383
384        // If the extensions member of options is present, then for each extensionId → clientExtensionInput of options.extensions:
385        // ...
386
387        // This is where we deviate from the spec, since we aren't a browser.
388
389        let user_verification = options.user_verification == UserVerificationPolicy::Required;
390
391        let rp_id_hash = compute_sha256(options.rp_id.as_bytes()).to_vec();
392
393        let u2sd = self.perform_u2f_sign(
394            rp_id_hash.clone(),
395            client_data_json_hash,
396            timeout_ms.into(),
397            options.allow_credentials.as_slice(),
398            user_verification,
399        )?;
400
401        trace!("u2sd -> {:x?}", u2sd);
402        // Transform the result to webauthn
403
404        // The flags are set from the device.
405
406        let authdata: Vec<u8> = rp_id_hash
407            .iter()
408            .copied()
409            .chain(iter::once(u2sd.flags))
410            .chain(
411                // A 0 u32 counter
412                u2sd.counter.to_be_bytes().iter().copied(),
413            )
414            .collect();
415
416        Ok(PublicKeyCredential {
417            id: BASE64_ENGINE.encode(&u2sd.key_handle),
418            raw_id: u2sd.key_handle.into(),
419            response: AuthenticatorAssertionResponseRaw {
420                authenticator_data: authdata.into(),
421                client_data_json: Base64UrlSafeData::new(),
422                signature: u2sd.signature.into(),
423                user_handle: None,
424            },
425            type_: "public-key".to_string(),
426            extensions: AuthenticationExtensionsClientOutputs::default(),
427        })
428    }
429}
430
431pub trait U2FToken {
432    fn perform_u2f_sign(
433        &mut self,
434        // This is rp.id_hash
435        app_bytes: Vec<u8>,
436        // This is client_data_json_hash
437        chal_bytes: Vec<u8>,
438        // timeout from options
439        timeout_ms: u64,
440        // list of creds
441        allowed_credentials: &[AllowCredentials],
442        user_verification: bool,
443    ) -> Result<U2FSignData, WebauthnCError>;
444}
445
446impl U2FToken for SoftPasskey {
447    fn perform_u2f_sign(
448        &mut self,
449        // This is rp.id_hash
450        app_bytes: Vec<u8>,
451        // This is client_data_json_hash
452        chal_bytes: Vec<u8>,
453        // timeout from options
454        _timeout_ms: u64,
455        // list of creds
456        allowed_credentials: &[AllowCredentials],
457        user_verification: bool,
458    ) -> Result<U2FSignData, WebauthnCError> {
459        if user_verification && !self.falsify_uv {
460            error!("User Verification not supported by softtoken");
461            return Err(WebauthnCError::NotSupported);
462        }
463
464        let cred = allowed_credentials
465            .iter()
466            .filter_map(|ac| {
467                self.tokens
468                    .get(ac.id.as_ref())
469                    .map(|v| (ac.id.clone().into(), v.clone()))
470            })
471            .take(1)
472            .next();
473
474        let (key_handle, pkder) = if let Some((key_handle, pkder)) = cred {
475            (key_handle, pkder)
476        } else {
477            error!("Credential ID not found");
478            return Err(WebauthnCError::Internal);
479        };
480
481        debug!("Using -> {:?}", key_handle);
482
483        let eckey = ec::EcKey::private_key_from_der(pkder.as_slice())?;
484
485        let pkey = pkey::PKey::from_ec_key(eckey)?;
486
487        let mut signer = sign::Signer::new(hash::MessageDigest::sha256(), &pkey)?;
488
489        // Increment the counter.
490        self.counter += 1;
491        let counter = self.counter;
492
493        let flags = if user_verification {
494            0b00000101
495        } else {
496            0b00000001
497        };
498
499        let verification_data: Vec<u8> = app_bytes
500            .iter()
501            .chain(iter::once(&flags))
502            .chain(counter.to_be_bytes().iter())
503            .chain(chal_bytes.iter())
504            .copied()
505            .collect();
506
507        let signature = signer
508            .update(verification_data.as_slice())
509            .and_then(|_| signer.sign_to_vec())?;
510
511        Ok(U2FSignData {
512            key_handle,
513            counter,
514            signature,
515            flags,
516        })
517    }
518}
519
520#[cfg(test)]
521mod tests {
522    use super::SoftPasskey;
523    use crate::prelude::{Url, WebauthnAuthenticator};
524    use std::time::Duration;
525    use webauthn_rs_core::WebauthnCore as Webauthn;
526    use webauthn_rs_proto::{
527        AttestationConveyancePreference, COSEAlgorithm, UserVerificationPolicy,
528    };
529
530    const AUTHENTICATOR_TIMEOUT: Duration = Duration::from_secs(60);
531
532    #[test]
533    fn webauthn_authenticator_wan_softpasskey_self_attest() {
534        let _ = tracing_subscriber::fmt::try_init();
535        let wan = Webauthn::new_unsafe_experts_only(
536            "https://localhost:8080/auth",
537            "localhost",
538            vec![url::Url::parse("https://localhost:8080").unwrap()],
539            AUTHENTICATOR_TIMEOUT,
540            None,
541            None,
542        );
543
544        let unique_id = [
545            158, 170, 228, 89, 68, 28, 73, 194, 134, 19, 227, 153, 107, 220, 150, 238,
546        ];
547        let name = "william";
548
549        let builder = wan
550            .new_challenge_register_builder(&unique_id, name, name)
551            .unwrap()
552            .attestation(AttestationConveyancePreference::Direct)
553            .user_verification_policy(UserVerificationPolicy::Preferred);
554
555        let (chal, reg_state) = wan.generate_challenge_register(builder).unwrap();
556
557        info!("🍿 challenge -> {:x?}", chal);
558
559        let mut wa = WebauthnAuthenticator::new(SoftPasskey::new(true));
560        let r = wa
561            .do_registration(Url::parse("https://localhost:8080").unwrap(), chal)
562            .map_err(|e| {
563                error!("Error -> {:x?}", e);
564                e
565            })
566            .expect("Failed to register");
567
568        let cred = wan.register_credential(&r, &reg_state, None).unwrap();
569
570        let (chal, auth_state) = wan
571            .new_challenge_authenticate_builder(vec![cred], None)
572            .and_then(|b| wan.generate_challenge_authenticate(b))
573            .unwrap();
574
575        let r = wa
576            .do_authentication(Url::parse("https://localhost:8080").unwrap(), chal)
577            .map_err(|e| {
578                error!("Error -> {:x?}", e);
579                e
580            })
581            .expect("Failed to auth");
582
583        let auth_res = wan
584            .authenticate_credential(&r, &auth_state)
585            .expect("webauth authentication denied");
586        info!("auth_res -> {:x?}", auth_res);
587    }
588}