webauthn_authenticator_rs/
softtoken.rs

1#[cfg(doc)]
2use crate::stubs::*;
3
4use crate::{
5    authenticator_hashed::AuthenticatorBackendHashedClientData,
6    crypto::{compute_sha256, get_group},
7    ctap2::commands::{value_to_vec_u8, GetInfoResponse},
8    error::WebauthnCError,
9    BASE64_ENGINE,
10};
11use base64::Engine;
12use openssl::x509::{
13    extension::{AuthorityKeyIdentifier, BasicConstraints, KeyUsage, SubjectKeyIdentifier},
14    X509NameBuilder, X509Ref, X509ReqBuilder, X509,
15};
16use openssl::{asn1, bn, ec, hash, pkey, rand, sign};
17use serde::{Deserialize, Serialize};
18use serde_cbor_2::value::Value;
19use std::collections::HashMap;
20use std::iter;
21use std::{collections::BTreeMap, fs::File, io::Read};
22use std::{
23    collections::BTreeSet,
24    io::{Seek, Write},
25};
26use uuid::Uuid;
27
28use base64urlsafedata::Base64UrlSafeData;
29
30use webauthn_rs_proto::{
31    AllowCredentials, AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw,
32    AuthenticatorAttachment, AuthenticatorAttestationResponseRaw, AuthenticatorTransport,
33    PublicKeyCredential, PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions,
34    RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs, UserVerificationPolicy,
35};
36
37pub const AAGUID: Uuid = uuid::uuid!("0fb9bcbc-a0d4-4042-bbb0-559bc1631e28");
38
39#[derive(Serialize, Deserialize)]
40pub struct SoftToken {
41    #[serde(with = "PKeyPrivateDef")]
42    _ca_key: pkey::PKey<pkey::Private>,
43    #[serde(with = "X509Def")]
44    ca_cert: X509,
45    #[serde(with = "PKeyPrivateDef")]
46    intermediate_key: pkey::PKey<pkey::Private>,
47    #[serde(with = "X509Def")]
48    intermediate_cert: X509,
49    tokens: HashMap<Vec<u8>, Vec<u8>>,
50    counter: u32,
51    falsify_uv: bool,
52}
53
54#[derive(Serialize, Deserialize)]
55#[serde(remote = "pkey::PKey<pkey::Private>")]
56struct PKeyPrivateDef {
57    #[serde(getter = "private_key_to_der")]
58    der: Value,
59}
60
61fn private_key_to_der(k: &pkey::PKeyRef<pkey::Private>) -> Value {
62    Value::Bytes(
63        k.private_key_to_der()
64            .expect("Cannot convert private key to DER"),
65    )
66}
67
68impl From<PKeyPrivateDef> for pkey::PKey<pkey::Private> {
69    fn from(def: PKeyPrivateDef) -> Self {
70        let b = value_to_vec_u8(def.der, "der").expect("Cannot deserialise private key");
71        Self::private_key_from_der(&b).expect("Cannot read private key as DER")
72    }
73}
74
75#[derive(Serialize, Deserialize)]
76#[serde(remote = "X509")]
77struct X509Def {
78    #[serde(getter = "x509_to_der")]
79    der: Value,
80}
81
82fn x509_to_der(k: &X509Ref) -> Value {
83    Value::Bytes(k.to_der().expect("Cannot convert certificate to DER"))
84}
85
86impl From<X509Def> for X509 {
87    fn from(def: X509Def) -> Self {
88        let b = value_to_vec_u8(def.der, "der").expect("Cannot deserialise certificate");
89        Self::from_der(&b).expect("Cannot read certificate as DER")
90    }
91}
92
93fn build_ca() -> Result<(pkey::PKey<pkey::Private>, X509), WebauthnCError> {
94    let ecgroup = get_group()?;
95    let eckey = ec::EcKey::generate(&ecgroup)?;
96    let ca_key = pkey::PKey::from_ec_key(eckey)?;
97    let mut x509_name = X509NameBuilder::new()?;
98
99    x509_name.append_entry_by_text("C", "AU")?;
100    x509_name.append_entry_by_text("ST", "QLD")?;
101    x509_name.append_entry_by_text("O", "Webauthn Authenticator RS")?;
102    x509_name.append_entry_by_text("CN", "Dynamic Softtoken CA")?;
103    let x509_name = x509_name.build();
104
105    let mut cert_builder = X509::builder()?;
106    // Yes, 2 actually means 3 here ...
107    cert_builder.set_version(2)?;
108
109    let serial_number = bn::BigNum::from_u32(1).and_then(|serial| serial.to_asn1_integer())?;
110
111    cert_builder.set_serial_number(&serial_number)?;
112    cert_builder.set_subject_name(&x509_name)?;
113    cert_builder.set_issuer_name(&x509_name)?;
114
115    let not_before = asn1::Asn1Time::days_from_now(0)?;
116    cert_builder.set_not_before(&not_before)?;
117    let not_after = asn1::Asn1Time::days_from_now(1)?;
118    cert_builder.set_not_after(&not_after)?;
119
120    cert_builder.append_extension(BasicConstraints::new().critical().ca().build()?)?;
121    cert_builder.append_extension(
122        KeyUsage::new()
123            .critical()
124            .key_cert_sign()
125            .crl_sign()
126            .build()?,
127    )?;
128
129    let subject_key_identifier =
130        SubjectKeyIdentifier::new().build(&cert_builder.x509v3_context(None, None))?;
131    cert_builder.append_extension(subject_key_identifier)?;
132
133    cert_builder.set_pubkey(&ca_key)?;
134
135    cert_builder.sign(&ca_key, hash::MessageDigest::sha256())?;
136    let ca_cert = cert_builder.build();
137
138    Ok((ca_key, ca_cert))
139}
140
141fn build_intermediate(
142    ca_key: &pkey::PKeyRef<pkey::Private>,
143    ca_cert: &X509Ref,
144) -> Result<(pkey::PKey<pkey::Private>, X509), WebauthnCError> {
145    let ecgroup = get_group()?;
146    let eckey = ec::EcKey::generate(&ecgroup)?;
147    let int_key = pkey::PKey::from_ec_key(eckey)?;
148
149    //
150    let mut req_builder = X509ReqBuilder::new()?;
151    req_builder.set_pubkey(&int_key)?;
152
153    let mut x509_name = X509NameBuilder::new()?;
154    x509_name.append_entry_by_text("C", "AU")?;
155    x509_name.append_entry_by_text("ST", "QLD")?;
156    x509_name.append_entry_by_text("O", "Webauthn Authenticator RS")?;
157    x509_name.append_entry_by_text("CN", "Dynamic Softtoken Leaf Certificate")?;
158    // Requirement of packed attestation.
159    x509_name.append_entry_by_text("OU", "Authenticator Attestation")?;
160    let x509_name = x509_name.build();
161
162    req_builder.set_subject_name(&x509_name)?;
163    req_builder.sign(&int_key, hash::MessageDigest::sha256())?;
164    let req = req_builder.build();
165    // ==
166
167    let mut cert_builder = X509::builder()?;
168    // Yes, 2 actually means 3 here ...
169    cert_builder.set_version(2)?;
170    let serial_number = bn::BigNum::from_u32(2).and_then(|serial| serial.to_asn1_integer())?;
171
172    cert_builder.set_pubkey(&int_key)?;
173
174    cert_builder.set_serial_number(&serial_number)?;
175    cert_builder.set_subject_name(req.subject_name())?;
176    cert_builder.set_issuer_name(ca_cert.subject_name())?;
177
178    let not_before = asn1::Asn1Time::days_from_now(0)?;
179    cert_builder.set_not_before(&not_before)?;
180    let not_after = asn1::Asn1Time::days_from_now(1)?;
181    cert_builder.set_not_after(&not_after)?;
182
183    cert_builder.append_extension(BasicConstraints::new().build()?)?;
184
185    cert_builder.append_extension(
186        KeyUsage::new()
187            .critical()
188            .non_repudiation()
189            .digital_signature()
190            .key_encipherment()
191            .build()?,
192    )?;
193
194    let subject_key_identifier =
195        SubjectKeyIdentifier::new().build(&cert_builder.x509v3_context(Some(ca_cert), None))?;
196    cert_builder.append_extension(subject_key_identifier)?;
197
198    let auth_key_identifier = AuthorityKeyIdentifier::new()
199        .keyid(false)
200        .issuer(false)
201        .build(&cert_builder.x509v3_context(Some(ca_cert), None))?;
202    cert_builder.append_extension(auth_key_identifier)?;
203
204    /*
205    let subject_alt_name = SubjectAlternativeName::new()
206        .dns("*.example.com")
207        .dns("hello.com")
208        .build(&cert_builder.x509v3_context(Some(ca_cert), None))?;
209    cert_builder.append_extension(subject_alt_name)?;
210    */
211
212    cert_builder.sign(ca_key, hash::MessageDigest::sha256())?;
213    let int_cert = cert_builder.build();
214
215    Ok((int_key, int_cert))
216}
217
218impl SoftToken {
219    pub fn new(falsify_uv: bool) -> Result<(Self, X509), WebauthnCError> {
220        let (ca_key, ca_cert) = build_ca()?;
221
222        let ca = ca_cert.clone();
223        /*
224        // Disabled as older openssl versions can't provide this.
225        trace!(
226            "{}",
227            String::from_utf8_lossy(&ca.to_text().map_err(|e| {
228                error!("OpenSSL Error -> {:?}", e);
229                WebauthnCError::OpenSSL
230            })?)
231        );
232        */
233
234        let (intermediate_key, intermediate_cert) = build_intermediate(&ca_key, &ca_cert)?;
235
236        /*
237        // Disabled as older openssl versions can't provide this.
238        trace!(
239            "{}",
240            String::from_utf8_lossy(&intermediate_cert.to_text().map_err(|e| {
241                error!("OpenSSL Error -> {:?}", e);
242                WebauthnCError::OpenSSL
243            })?)
244        );
245        */
246
247        Ok((
248            SoftToken {
249                // We could consider throwing these away?
250                _ca_key: ca_key,
251                ca_cert,
252                intermediate_key,
253                intermediate_cert,
254                tokens: HashMap::new(),
255                counter: 0,
256                falsify_uv,
257            },
258            ca,
259        ))
260    }
261
262    pub fn get_info(&self) -> GetInfoResponse {
263        GetInfoResponse {
264            versions: BTreeSet::from(["FIDO_2_0".to_string()]),
265            aaguid: Some(AAGUID),
266            transports: Some(vec!["internal".to_string()]),
267            ..Default::default()
268        }
269    }
270
271    pub fn to_cbor(&self) -> Result<Vec<u8>, WebauthnCError> {
272        serde_cbor_2::ser::to_vec(self).map_err(|e| {
273            error!("SoftToken.to_cbor: {:?}", e);
274            WebauthnCError::Cbor
275        })
276    }
277
278    pub fn from_cbor(v: &[u8]) -> Result<Self, WebauthnCError> {
279        serde_cbor_2::from_slice(v).map_err(|e| {
280            error!("SoftToken::from_cbor: {:?}", e);
281            WebauthnCError::Cbor
282        })
283    }
284}
285
286#[derive(Debug)]
287pub struct U2FSignData {
288    key_handle: Vec<u8>,
289    counter: u32,
290    signature: Vec<u8>,
291    flags: u8,
292}
293
294impl AuthenticatorBackendHashedClientData for SoftToken {
295    fn perform_register(
296        &mut self,
297        client_data_json_hash: Vec<u8>,
298        options: PublicKeyCredentialCreationOptions,
299        _timeout_ms: u32,
300    ) -> Result<RegisterPublicKeyCredential, WebauthnCError> {
301        // Let credTypesAndPubKeyAlgs be a new list whose items are pairs of PublicKeyCredentialType and a COSEAlgorithmIdentifier.
302        // Done in rust types.
303
304        // For each current of options.pubKeyCredParams:
305        //     If current.type does not contain a PublicKeyCredentialType supported by this implementation, then continue.
306        //     Let alg be current.alg.
307        //     Append the pair of current.type and alg to credTypesAndPubKeyAlgs.
308        let cred_types_and_pub_key_algs: Vec<_> = options
309            .pub_key_cred_params
310            .iter()
311            .filter_map(|param| {
312                if param.type_ != "public-key" {
313                    None
314                } else {
315                    Some((param.type_.clone(), param.alg))
316                }
317            })
318            .collect();
319
320        trace!("Found -> {:x?}", cred_types_and_pub_key_algs);
321
322        // If credTypesAndPubKeyAlgs is empty and options.pubKeyCredParams is not empty, return a DOMException whose name is "NotSupportedError", and terminate this algorithm.
323        if cred_types_and_pub_key_algs.is_empty() {
324            return Err(WebauthnCError::NotSupported);
325        }
326
327        /*
328            // Let clientExtensions be a new map and let authenticatorExtensions be a new map.
329
330            // If the extensions member of options is present, then for each extensionId → clientExtensionInput of options.extensions:
331            //     If extensionId is not supported by this client platform or is not a registration extension, then continue.
332            //     Set clientExtensions[extensionId] to clientExtensionInput.
333            //     If extensionId is not an authenticator extension, then continue.
334            //     Let authenticatorExtensionInput be the (CBOR) result of running extensionId’s client extension processing algorithm on clientExtensionInput. If the algorithm returned an error, continue.
335            //     Set authenticatorExtensions[extensionId] to the base64url encoding of authenticatorExtensionInput.
336        */
337
338        // Not required.
339        // 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.
340
341        // Let issuedRequests be a new ordered set.
342
343        // 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.
344
345        // Start lifetimeTimer.
346
347        // While lifetimeTimer has not expired, perform the following actions depending upon lifetimeTimer, and the state and response for each authenticator in authenticators:
348
349        //    If lifetimeTimer expires,
350        //        For each authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove authenticator from issuedRequests.
351
352        //    If the user exercises a user agent user-interface option to cancel the process,
353        //        For each authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove authenticator from issuedRequests. Return a DOMException whose name is "NotAllowedError".
354
355        //    If the options.signal is present and its aborted flag is set to true,
356        //        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.
357
358        //    If an authenticator becomes available on this client device,
359        //         If options.authenticatorSelection is present:
360        //             If options.authenticatorSelection.authenticatorAttachment is present and its value is not equal to authenticator’s authenticator attachment modality, continue.
361        //             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.
362        //             If options.authenticatorSelection.userVerification is set to required and the authenticator is not capable of performing user verification, continue.
363        //          Let userVerification be the effective user verification requirement for credential creation, a Boolean value, as follows. If options.authenticatorSelection.userVerification
364        //              is set to required -> Let userVerification be true.
365        //              is set to preferred
366        //                  If the authenticator
367        //                      is capable of user verification -> Let userVerification be true.
368        //                      is not capable of user verification -> Let userVerification be false.
369        //              is set to discouraged -> Let userVerification be false.
370        //          Let userPresence be a Boolean value set to the inverse of userVerification.
371        //          Let excludeCredentialDescriptorList be a new list.
372        //          For each credential descriptor C in options.excludeCredentials:
373        //              If C.transports is not empty, and authenticator is connected over a transport not mentioned in C.transports, the client MAY continue.
374        //              Otherwise, Append C to excludeCredentialDescriptorList.
375        //          Invoke the authenticatorMakeCredential operation on authenticator with clientDataHash, options.rp, options.user, options.authenticatorSelection.requireResidentKey, userPresence, userVerification, credTypesAndPubKeyAlgs, excludeCredentialDescriptorList, and authenticatorExtensions as parameters.
376
377        //          Append authenticator to issuedRequests.
378
379        //    If an authenticator ceases to be available on this client device,
380        //         Remove authenticator from issuedRequests.
381
382        //    If any authenticator returns a status indicating that the user cancelled the operation,
383        //         Remove authenticator from issuedRequests.
384        //         For each remaining authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove it from issuedRequests.
385
386        //    If any authenticator returns an error status equivalent to "InvalidStateError",
387        //         Remove authenticator from issuedRequests.
388        //         For each remaining authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove it from issuedRequests.
389        //         Return a DOMException whose name is "InvalidStateError" and terminate this algorithm.
390
391        //    If any authenticator returns an error status not equivalent to "InvalidStateError",
392        //         Remove authenticator from issuedRequests.
393
394        //    If any authenticator indicates success,
395        //         Remove authenticator from issuedRequests.
396        //         Let credentialCreationData be a struct whose items are:
397        //         Let constructCredentialAlg be an algorithm that takes a global object global, and whose steps are:
398
399        //         Let attestationObject be a new ArrayBuffer, created using global’s %ArrayBuffer%, containing the bytes of credentialCreationData.attestationObjectResult’s value.
400
401        //         Let id be attestationObject.authData.attestedCredentialData.credentialId.
402        //         Let pubKeyCred be a new PublicKeyCredential object associated with global whose fields are:
403        //         For each remaining authenticator in issuedRequests invoke the authenticatorCancel operation on authenticator and remove it from issuedRequests.
404        //         Return constructCredentialAlg and terminate this algorithm.
405
406        // For our needs, we let the u2f auth library handle the above, but currently it can't accept
407        // verified devices for u2f with ctap1/2. We may need to change u2f/authenticator library in the future.
408        // As a result this really limits our usage to certain device classes. This is why we implement
409        // this section in a seperate function call.
410
411        let (platform_attached, resident_key, user_verification) =
412            match &options.authenticator_selection {
413                Some(auth_sel) => {
414                    let pa = auth_sel
415                        .authenticator_attachment
416                        .as_ref()
417                        .map(|v| v == &AuthenticatorAttachment::Platform)
418                        .unwrap_or(false);
419                    let uv = auth_sel.user_verification == UserVerificationPolicy::Required;
420                    (pa, auth_sel.require_resident_key, uv)
421                }
422                None => (false, false, false),
423            };
424
425        let rp_id_hash = compute_sha256(options.rp.id.as_bytes()).to_vec();
426
427        // =====
428
429        if user_verification && !self.falsify_uv {
430            error!("User Verification not supported by softtoken");
431            return Err(WebauthnCError::NotSupported);
432        }
433
434        if platform_attached {
435            error!("Platform Attachement not supported by softtoken");
436            return Err(WebauthnCError::NotSupported);
437        }
438
439        if resident_key {
440            error!("Resident Keys not supported by softtoken");
441            // These will be supported in future :)
442            return Err(WebauthnCError::NotSupported);
443        }
444
445        // Generate a random credential id
446        let mut key_handle: Vec<u8> = Vec::with_capacity(32);
447        key_handle.resize_with(32, Default::default);
448        rand::rand_bytes(key_handle.as_mut_slice())?;
449
450        // Create a new key.
451        let ecgroup = get_group()?;
452
453        let eckey = ec::EcKey::generate(&ecgroup)?;
454
455        // Extract the public x and y coords.
456        let ecpub_points = eckey.public_key();
457
458        let mut bnctx = bn::BigNumContext::new()?;
459
460        let mut xbn = bn::BigNum::new()?;
461
462        let mut ybn = bn::BigNum::new()?;
463
464        ecpub_points.affine_coordinates_gfp(&ecgroup, &mut xbn, &mut ybn, &mut bnctx)?;
465
466        let mut public_key_x = Vec::with_capacity(32);
467        let mut public_key_y = Vec::with_capacity(32);
468
469        public_key_x.resize(32, 0);
470        public_key_y.resize(32, 0);
471
472        let xbnv = xbn.to_vec();
473        let ybnv = ybn.to_vec();
474
475        let (_pad, x_fill) = public_key_x.split_at_mut(32 - xbnv.len());
476        x_fill.copy_from_slice(&xbnv);
477
478        let (_pad, y_fill) = public_key_y.split_at_mut(32 - ybnv.len());
479        y_fill.copy_from_slice(&ybnv);
480
481        // Extract the DER cert for later
482        let ecpriv_der = eckey.private_key_to_der()?;
483
484        // =====
485
486        // From the u2f response, we now need to assemble the attestation object now.
487
488        // cbor encode the public key. We already decomposed this, so just create
489        // the correct bytes.
490        let mut map = BTreeMap::new();
491        // KeyType -> EC2
492        map.insert(Value::Integer(1), Value::Integer(2));
493        // Alg -> ES256
494        map.insert(Value::Integer(3), Value::Integer(-7));
495
496        // Curve -> P-256
497        map.insert(Value::Integer(-1), Value::Integer(1));
498        // EC X coord
499        map.insert(Value::Integer(-2), Value::Bytes(public_key_x));
500        // EC Y coord
501        map.insert(Value::Integer(-3), Value::Bytes(public_key_y));
502
503        let pk_cbor = Value::Map(map);
504        let pk_cbor_bytes = serde_cbor_2::to_vec(&pk_cbor).map_err(|e| {
505            error!("PK CBOR -> {:x?}", e);
506            WebauthnCError::Cbor
507        })?;
508
509        let key_handle_len: u16 = u16::try_from(key_handle.len()).map_err(|e| {
510            error!("CBOR kh len is not u16 -> {:x?}", e);
511            WebauthnCError::Cbor
512        })?;
513
514        // combine aaGuid, KeyHandle, CborPubKey into a AttestedCredentialData. (acd)
515        let khlen_be_bytes = key_handle_len.to_be_bytes();
516        let acd_iter = AAGUID
517            .as_bytes()
518            .iter()
519            .chain(khlen_be_bytes.iter())
520            .copied()
521            .chain(key_handle.iter().copied())
522            .chain(pk_cbor_bytes.iter().copied());
523
524        // set counter to 0 during create
525        // Combine rp_id_hash, flags, counter, acd, into authenticator data.
526        // The flags are always att present, user verified, user present
527        let flags = if user_verification {
528            0b01000101
529        } else {
530            0b01000001
531        };
532
533        let authdata: Vec<u8> = rp_id_hash
534            .iter()
535            .copied()
536            .chain(iter::once(flags))
537            .chain(
538                // A 0 u32 counter
539                iter::repeat(0).take(4),
540            )
541            .chain(acd_iter)
542            .collect();
543
544        // 4.b. Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash using the credential public key with alg.
545
546        let verification_data: Vec<u8> = authdata
547            .iter()
548            .chain(client_data_json_hash.iter())
549            .copied()
550            .collect();
551
552        // Now setup to sign.
553        // NOTE: for the token version we use the intermediate!
554        let mut signer = sign::Signer::new(hash::MessageDigest::sha256(), &self.intermediate_key)?;
555
556        // Do the signature
557        let signature = signer
558            .update(verification_data.as_slice())
559            .and_then(|_| signer.sign_to_vec())?;
560
561        let mut attest_map = BTreeMap::new();
562
563        attest_map.insert(
564            Value::Text("fmt".to_string()),
565            Value::Text("packed".to_string()),
566        );
567        let mut att_stmt_map = BTreeMap::new();
568        att_stmt_map.insert(Value::Text("alg".to_string()), Value::Integer(-7));
569
570        let x509_bytes = Value::Bytes(self.intermediate_cert.to_der()?);
571
572        att_stmt_map.insert(
573            Value::Text("x5c".to_string()),
574            Value::Array(vec![x509_bytes]),
575        );
576        att_stmt_map.insert(Value::Text("sig".to_string()), Value::Bytes(signature));
577
578        attest_map.insert(Value::Text("attStmt".to_string()), Value::Map(att_stmt_map));
579        attest_map.insert(Value::Text("authData".to_string()), Value::Bytes(authdata));
580
581        let ao = Value::Map(attest_map);
582
583        let ao_bytes = serde_cbor_2::to_vec(&ao).map_err(|e| {
584            error!("AO CBOR -> {:x?}", e);
585            WebauthnCError::Cbor
586        })?;
587
588        // 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.
589
590        // Okay, now persist the token. We shouldn't fail from here.
591        self.tokens.insert(key_handle.clone(), ecpriv_der);
592
593        let rego = RegisterPublicKeyCredential {
594            id: BASE64_ENGINE.encode(&key_handle),
595            raw_id: key_handle.into(),
596            response: AuthenticatorAttestationResponseRaw {
597                attestation_object: ao_bytes.into(),
598                client_data_json: Base64UrlSafeData::new(),
599                transports: Some(vec![AuthenticatorTransport::Internal]),
600            },
601            type_: "public-key".to_string(),
602            extensions: RegistrationExtensionsClientOutputs::default(),
603        };
604
605        trace!("rego  -> {:x?}", rego);
606        Ok(rego)
607    }
608
609    fn perform_auth(
610        &mut self,
611        client_data_json_hash: Vec<u8>,
612        options: PublicKeyCredentialRequestOptions,
613        timeout_ms: u32,
614    ) -> Result<PublicKeyCredential, WebauthnCError> {
615        // Let clientExtensions be a new map and let authenticatorExtensions be a new map.
616
617        // If the extensions member of options is present, then for each extensionId → clientExtensionInput of options.extensions:
618        // ...
619
620        // This is where we deviate from the spec, since we aren't a browser.
621
622        let user_verification = options.user_verification == UserVerificationPolicy::Required;
623
624        let rp_id_hash = compute_sha256(options.rp_id.as_bytes()).to_vec();
625
626        let u2sd = self.perform_u2f_sign(
627            rp_id_hash.clone(),
628            client_data_json_hash,
629            timeout_ms.into(),
630            options.allow_credentials.as_slice(),
631            user_verification && self.falsify_uv,
632        )?;
633
634        trace!("u2sd -> {:x?}", u2sd);
635        // Transform the result to webauthn
636
637        // The flags are set from the device.
638
639        let authdata: Vec<u8> = rp_id_hash
640            .iter()
641            .copied()
642            .chain(iter::once(u2sd.flags))
643            .chain(
644                // A 0 u32 counter
645                u2sd.counter.to_be_bytes().iter().copied(),
646            )
647            .collect();
648
649        Ok(PublicKeyCredential {
650            id: BASE64_ENGINE.encode(&u2sd.key_handle),
651            raw_id: u2sd.key_handle.into(),
652            response: AuthenticatorAssertionResponseRaw {
653                authenticator_data: authdata.into(),
654                client_data_json: Base64UrlSafeData::new(),
655                signature: u2sd.signature.into(),
656                user_handle: None,
657            },
658            type_: "public-key".to_string(),
659            extensions: AuthenticationExtensionsClientOutputs::default(),
660        })
661    }
662}
663
664pub trait U2FToken {
665    fn perform_u2f_sign(
666        &mut self,
667        // This is rp.id_hash
668        app_bytes: Vec<u8>,
669        // This is client_data_json_hash
670        chal_bytes: Vec<u8>,
671        // timeout from options
672        timeout_ms: u64,
673        // list of creds
674        allowed_credentials: &[AllowCredentials],
675        user_verification: bool,
676    ) -> Result<U2FSignData, WebauthnCError>;
677}
678
679impl U2FToken for SoftToken {
680    fn perform_u2f_sign(
681        &mut self,
682        // This is rp.id_hash
683        app_bytes: Vec<u8>,
684        // This is client_data_json_hash
685        chal_bytes: Vec<u8>,
686        // timeout from options
687        _timeout_ms: u64,
688        // list of creds
689        allowed_credentials: &[AllowCredentials],
690        user_verification: bool,
691    ) -> Result<U2FSignData, WebauthnCError> {
692        if user_verification && !self.falsify_uv {
693            error!("User Verification not supported by softtoken");
694            return Err(WebauthnCError::NotSupported);
695        }
696
697        let cred = allowed_credentials
698            .iter()
699            .filter_map(|ac| {
700                self.tokens
701                    .get(ac.id.as_ref())
702                    .map(|v| (ac.id.clone().into(), v.clone()))
703            })
704            .take(1)
705            .next();
706
707        let (key_handle, pkder) = if let Some((key_handle, pkder)) = cred {
708            (key_handle, pkder)
709        } else {
710            error!("Credential ID not found");
711            return Err(WebauthnCError::Internal);
712        };
713
714        debug!("Using -> {:?}", key_handle);
715
716        let eckey = ec::EcKey::private_key_from_der(pkder.as_slice())?;
717
718        let pkey = pkey::PKey::from_ec_key(eckey)?;
719
720        let mut signer = sign::Signer::new(hash::MessageDigest::sha256(), &pkey)?;
721
722        // Increment the counter.
723        self.counter += 1;
724        let counter = self.counter;
725
726        let flags = if user_verification {
727            0b00000101
728        } else {
729            0b00000001
730        };
731
732        let verification_data: Vec<u8> = app_bytes
733            .iter()
734            .chain(iter::once(&flags))
735            .chain(counter.to_be_bytes().iter())
736            .chain(chal_bytes.iter())
737            .copied()
738            .collect();
739
740        trace!("Signing: {:?}", verification_data.as_slice());
741        let signature = signer
742            .update(verification_data.as_slice())
743            .and_then(|_| signer.sign_to_vec())?;
744
745        Ok(U2FSignData {
746            key_handle,
747            counter,
748            signature,
749            flags,
750        })
751    }
752}
753
754/// [SoftToken] which is read form, and automatically saved to a [File] when
755/// dropped.
756///
757/// ## Warning
758///
759/// **[SoftTokenFile] is intended for testing purposes only, and is not intended
760/// for production or long-term usage.**
761///
762/// [SoftTokenFile] stores private key material insecurely with no protection
763/// of any kind. Its serialisation format is subject to change in the future
764/// *without warning*, which may prevent loading or saving [SoftTokenFile]s
765/// created with other versions of this code.
766pub struct SoftTokenFile {
767    token: SoftToken,
768    file: File,
769}
770
771impl SoftTokenFile {
772    /// Creates a new [SoftTokenFile] which will be saved when dropped.
773    pub fn new(token: SoftToken, file: File) -> Self {
774        Self { token, file }
775    }
776
777    /// Reads a [SoftToken] from a [File].
778    pub fn open(mut file: File) -> Result<Self, WebauthnCError> {
779        let mut buf = Vec::new();
780        file.read_to_end(&mut buf)?;
781
782        let token: SoftToken = serde_cbor_2::from_slice(&buf).map_err(|e| {
783            error!("Error reading SoftToken: {:?}", e);
784            WebauthnCError::Cbor
785        })?;
786
787        Ok(Self { token, file })
788    }
789
790    /// Saves the [SoftToken] to a [File].
791    fn save(&mut self) -> Result<(), WebauthnCError> {
792        trace!("Saving SoftToken to {:?}", self.file);
793        let d = self.token.to_cbor()?;
794        self.file.set_len(0)?;
795        self.file.rewind()?;
796        self.file.write_all(&d)?;
797        self.file.flush()?;
798        Ok(())
799    }
800}
801
802/// Extracts the [File] handle from this [SoftTokenFile], dropping (and saving)
803/// the [SoftTokenFile] in the process.
804impl TryFrom<SoftTokenFile> for File {
805    type Error = WebauthnCError;
806    fn try_from(value: SoftTokenFile) -> Result<Self, Self::Error> {
807        Ok(value.file.try_clone()?)
808    }
809}
810
811impl AsRef<SoftToken> for SoftTokenFile {
812    fn as_ref(&self) -> &SoftToken {
813        &self.token
814    }
815}
816
817/// Drops the [SoftTokenFile], automatically saving it to disk.
818impl Drop for SoftTokenFile {
819    fn drop(&mut self) {
820        self.save().unwrap_or_else(|e| {
821            error!("Error saving SoftToken: {:?}", e);
822        });
823    }
824}
825
826impl AuthenticatorBackendHashedClientData for SoftTokenFile {
827    fn perform_register(
828        &mut self,
829        client_data_hash: Vec<u8>,
830        options: PublicKeyCredentialCreationOptions,
831        timeout_ms: u32,
832    ) -> Result<RegisterPublicKeyCredential, WebauthnCError> {
833        self.token
834            .perform_register(client_data_hash, options, timeout_ms)
835    }
836
837    fn perform_auth(
838        &mut self,
839        client_data_hash: Vec<u8>,
840        options: PublicKeyCredentialRequestOptions,
841        timeout_ms: u32,
842    ) -> Result<PublicKeyCredential, WebauthnCError> {
843        self.token
844            .perform_auth(client_data_hash, options, timeout_ms)
845    }
846}
847
848#[cfg(test)]
849#[allow(clippy::panic)]
850mod tests {
851    use super::*;
852    use openssl::{hash::MessageDigest, rand::rand_bytes, sign::Verifier, x509::X509};
853    use std::time::Duration;
854    use tempfile::tempfile;
855    use webauthn_rs_core::{
856        proto::{AttestationCaList, AttestationCaListBuilder, COSEKey},
857        WebauthnCore as Webauthn,
858    };
859    use webauthn_rs_proto::{
860        AllowCredentials, AttestationConveyancePreference, COSEAlgorithm, PubKeyCredParams,
861        RelyingParty, User, UserVerificationPolicy,
862    };
863
864    use crate::{
865        ctap2::{
866            commands::{
867                value_to_vec_u8, GetAssertionRequest, GetAssertionResponse, MakeCredentialRequest,
868                MakeCredentialResponse,
869            },
870            CBORResponse,
871        },
872        perform_auth_with_request, perform_register_with_request,
873        prelude::{Url, WebauthnAuthenticator},
874        softtoken::SoftToken,
875    };
876
877    const AUTHENTICATOR_TIMEOUT: Duration = Duration::from_secs(60);
878
879    #[test]
880    fn webauthn_authenticator_wan_softtoken_direct_attest() {
881        let _ = tracing_subscriber::fmt::try_init();
882        let wan = Webauthn::new_unsafe_experts_only(
883            "https://localhost:8080/auth",
884            "localhost",
885            vec![url::Url::parse("https://localhost:8080").unwrap()],
886            AUTHENTICATOR_TIMEOUT,
887            None,
888            None,
889        );
890
891        let (soft_token, ca_root) = SoftToken::new(true).unwrap();
892
893        let mut wa = WebauthnAuthenticator::new(soft_token);
894
895        let unique_id = [
896            158, 170, 228, 89, 68, 28, 73, 194, 134, 19, 227, 153, 107, 220, 150, 238,
897        ];
898        let name = "william";
899
900        let builder = wan
901            .new_challenge_register_builder(&unique_id, name, name)
902            .unwrap()
903            .attestation(AttestationConveyancePreference::Direct)
904            .user_verification_policy(UserVerificationPolicy::Preferred);
905
906        let (chal, reg_state) = wan.generate_challenge_register(builder).unwrap();
907
908        info!("🍿 challenge -> {:x?}", chal);
909
910        let r = wa
911            .do_registration(Url::parse("https://localhost:8080").unwrap(), chal)
912            .map_err(|e| {
913                error!("Error -> {:x?}", e);
914                e
915            })
916            .expect("Failed to register");
917
918        let mut att_ca_builder = AttestationCaListBuilder::new();
919        att_ca_builder
920            .insert_device_x509(ca_root, AAGUID, "softtoken".to_string(), Default::default())
921            .expect("Failed to build att ca list");
922        let att_ca_list: AttestationCaList = att_ca_builder.build();
923
924        let cred = wan
925            .register_credential(&r, &reg_state, Some(&att_ca_list))
926            .unwrap();
927
928        info!("Credential -> {:?}", cred);
929
930        let (chal, auth_state) = wan
931            .new_challenge_authenticate_builder(vec![cred], None)
932            .and_then(|b| wan.generate_challenge_authenticate(b))
933            .unwrap();
934
935        let r = wa
936            .do_authentication(Url::parse("https://localhost:8080").unwrap(), chal)
937            .map_err(|e| {
938                error!("Error -> {:x?}", e);
939                e
940            })
941            .expect("Failed to auth");
942
943        let auth_res = wan
944            .authenticate_credential(&r, &auth_state)
945            .expect("webauth authentication denied");
946        info!("auth_res -> {:x?}", auth_res);
947    }
948
949    #[test]
950    fn softtoken_persistence() {
951        let _ = tracing_subscriber::fmt::try_init();
952        let wan = Webauthn::new_unsafe_experts_only(
953            "https://localhost:8080/auth",
954            "localhost",
955            vec![url::Url::parse("https://localhost:8080").unwrap()],
956            AUTHENTICATOR_TIMEOUT,
957            None,
958            None,
959        );
960
961        let (soft_token, ca_root) = SoftToken::new(true).unwrap();
962        let file = tempfile().unwrap();
963        let soft_token = SoftTokenFile::new(soft_token, file);
964        assert_eq!(soft_token.token.tokens.len(), 0);
965
966        let mut wa = WebauthnAuthenticator::new(soft_token);
967
968        let unique_id = [
969            158, 170, 228, 89, 68, 28, 73, 194, 134, 19, 227, 153, 107, 220, 150, 238,
970        ];
971        let name = "william";
972
973        let builder = wan
974            .new_challenge_register_builder(&unique_id, name, name)
975            .unwrap()
976            .attestation(AttestationConveyancePreference::Direct)
977            .user_verification_policy(UserVerificationPolicy::Preferred);
978
979        let (chal, reg_state) = wan.generate_challenge_register(builder).unwrap();
980
981        info!("🍿 challenge -> {:x?}", chal);
982
983        let r = wa
984            .do_registration(Url::parse("https://localhost:8080").unwrap(), chal)
985            .map_err(|e| {
986                error!("Error -> {:x?}", e);
987                e
988            })
989            .expect("Failed to register");
990
991        let mut att_ca_builder = AttestationCaListBuilder::new();
992        att_ca_builder
993            .insert_device_x509(ca_root, AAGUID, "softtoken".to_string(), Default::default())
994            .expect("Failed to build att ca list");
995        let att_ca_list: AttestationCaList = att_ca_builder.build();
996
997        let cred = wan
998            .register_credential(&r, &reg_state, Some(&att_ca_list))
999            .unwrap();
1000
1001        info!("Credential -> {:?}", cred);
1002
1003        assert_eq!(wa.backend.token.tokens.len(), 1);
1004
1005        // Save the credential to disk
1006        let mut file: File = wa.backend.try_into().unwrap();
1007        assert!(file.stream_position().unwrap() > 0);
1008
1009        // Rewind and reload
1010        file.rewind().unwrap();
1011
1012        let soft_token = SoftTokenFile::open(file).unwrap();
1013        assert_eq!(soft_token.token.tokens.len(), 1);
1014
1015        let mut wa = WebauthnAuthenticator::new(soft_token);
1016
1017        let (chal, auth_state) = wan
1018            .new_challenge_authenticate_builder(vec![cred], None)
1019            .and_then(|b| wan.generate_challenge_authenticate(b))
1020            .unwrap();
1021
1022        let r = wa
1023            .do_authentication(Url::parse("https://localhost:8080").unwrap(), chal)
1024            .map_err(|e| {
1025                error!("Error -> {:x?}", e);
1026                e
1027            })
1028            .expect("Failed to auth");
1029
1030        let auth_res = wan
1031            .authenticate_credential(&r, &auth_state)
1032            .expect("webauth authentication denied");
1033        info!("auth_res -> {:x?}", auth_res);
1034    }
1035
1036    #[test]
1037    fn perform_register_auth_with_command() {
1038        let _ = tracing_subscriber::fmt::try_init();
1039        let (mut soft_token, _) = SoftToken::new(true).unwrap();
1040        let mut client_data_hash = vec![0; 32];
1041        let mut user_id = vec![0; 16];
1042        rand_bytes(&mut client_data_hash).unwrap();
1043        rand_bytes(&mut user_id).unwrap();
1044
1045        let request = MakeCredentialRequest {
1046            client_data_hash: client_data_hash.clone(),
1047            rp: RelyingParty {
1048                name: "example.com".to_string(),
1049                id: "example.com".to_string(),
1050            },
1051            user: User {
1052                id: Base64UrlSafeData::from(user_id),
1053                name: "sampleuser".to_string(),
1054                display_name: "Sample User".to_string(),
1055            },
1056            pub_key_cred_params: vec![
1057                PubKeyCredParams {
1058                    type_: "public-key".to_string(),
1059                    alg: -7,
1060                },
1061                PubKeyCredParams {
1062                    type_: "public-key".to_string(),
1063                    alg: -257,
1064                },
1065            ],
1066            exclude_list: vec![],
1067            options: None,
1068            pin_uv_auth_param: None,
1069            pin_uv_auth_proto: None,
1070            enterprise_attest: None,
1071        };
1072
1073        let response = perform_register_with_request(&mut soft_token, request, 10000).unwrap();
1074
1075        // All keys should be ints
1076        let m: Value = serde_cbor_2::from_slice(response.as_slice()).unwrap();
1077        let m = if let Value::Map(m) = m {
1078            m
1079        } else {
1080            panic!("unexpected type")
1081        };
1082        assert!(m.keys().all(|k| matches!(k, Value::Integer(_))));
1083
1084        // Try to deserialise the MakeCredentialResponse again
1085        let response =
1086            <MakeCredentialResponse as CBORResponse>::try_from(response.as_slice()).unwrap();
1087        trace!(?response);
1088
1089        // Run packed attestation verification
1090        // https://www.w3.org/TR/webauthn-2/#sctn-packed-attestation
1091        let mut att_stmt = if let Value::Map(m) = response.att_stmt.unwrap() {
1092            m
1093        } else {
1094            panic!("unexpected type");
1095        };
1096        trace!(?att_stmt);
1097        let signature = value_to_vec_u8(
1098            att_stmt.remove(&Value::Text("sig".to_string())).unwrap(),
1099            "att_stmt.sig",
1100        )
1101        .unwrap();
1102
1103        // Extract attestation certificate
1104        let x5c = if let Value::Array(v) = att_stmt.remove(&Value::Text("x5c".to_string())).unwrap()
1105        {
1106            v
1107        } else {
1108            panic!("Unexpected type");
1109        };
1110        let x5c = value_to_vec_u8(x5c[0].to_owned(), "x5c[0]").unwrap();
1111        let verification_cert = X509::from_der(&x5c).unwrap();
1112        let pubkey = verification_cert.public_key().unwrap();
1113
1114        // Reconstruct verification data (auth_data + client_data_hash)
1115        let mut verification_data =
1116            value_to_vec_u8(response.auth_data.unwrap(), "verification_data").unwrap();
1117        let auth_data_len = verification_data.len();
1118        verification_data.reserve(client_data_hash.len());
1119        verification_data.extend_from_slice(&client_data_hash);
1120
1121        let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey).unwrap();
1122        assert!(verifier
1123            .verify_oneshot(&signature, &verification_data)
1124            .unwrap());
1125
1126        // https://www.w3.org/TR/webauthn-2/#attestation-object
1127        let cred_id_off = /* rp_id_hash */ 32 + /* flags */ 1 + /* counter */ 4 + /* aaguid */ 16;
1128        let cred_id_len = u16::from_be_bytes(
1129            (&verification_data[cred_id_off..cred_id_off + 2])
1130                .try_into()
1131                .unwrap(),
1132        ) as usize;
1133        let cred_id = Base64UrlSafeData::from(
1134            (verification_data[cred_id_off + 2..cred_id_off + 2 + cred_id_len]).to_vec(),
1135        );
1136
1137        // Future assertions are signed with this COSEKey
1138        let cose_key: Value = serde_cbor_2::from_slice(
1139            &verification_data[cred_id_off + 2 + cred_id_len..auth_data_len],
1140        )
1141        .unwrap();
1142        let cose_key = COSEKey::try_from(&cose_key).unwrap();
1143
1144        rand_bytes(&mut client_data_hash).unwrap();
1145        let request = GetAssertionRequest {
1146            client_data_hash: client_data_hash.clone(),
1147            rp_id: "example.com".to_string(),
1148            allow_list: vec![AllowCredentials {
1149                type_: "public-key".to_string(),
1150                id: cred_id.to_owned(),
1151                transports: None,
1152            }],
1153            options: None,
1154            pin_uv_auth_param: None,
1155            pin_uv_auth_proto: None,
1156        };
1157        trace!(?request);
1158
1159        let response = perform_auth_with_request(&mut soft_token, request, 10000).unwrap();
1160        let response =
1161            <GetAssertionResponse as CBORResponse>::try_from(response.as_slice()).unwrap();
1162        trace!(?response);
1163
1164        // Check correct matching credential
1165        assert_eq!(response.credential.unwrap().id, cred_id);
1166
1167        // Check the signature
1168        let signature = response.signature.unwrap();
1169        let mut verification_data = response.auth_data.unwrap();
1170        verification_data.reserve(client_data_hash.len());
1171        verification_data.extend_from_slice(&client_data_hash);
1172
1173        assert!(cose_key
1174            .verify_signature(&signature, &verification_data)
1175            .unwrap());
1176    }
1177}