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, ®_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}