1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "hasht.h"
8 #include "nsHTMLDocument.h"
9 #include "nsIURIMutator.h"
10 #include "nsThreadUtils.h"
11 #include "WebAuthnCoseIdentifiers.h"
12 #include "mozilla/BasePrincipal.h"
13 #include "mozilla/dom/AuthenticatorAssertionResponse.h"
14 #include "mozilla/dom/AuthenticatorAttestationResponse.h"
15 #include "mozilla/dom/PublicKeyCredential.h"
16 #include "mozilla/dom/Promise.h"
17 #include "mozilla/dom/PWebAuthnTransaction.h"
18 #include "mozilla/dom/WebAuthnManager.h"
19 #include "mozilla/dom/WebAuthnTransactionChild.h"
20 #include "mozilla/dom/WebAuthnUtil.h"
21 #include "mozilla/ipc/BackgroundChild.h"
22 #include "mozilla/ipc/PBackgroundChild.h"
23 #include "authenticator/src/u2fhid-capi.h"
24
25 #ifdef OS_WIN
26 # include "WinWebAuthnManager.h"
27 #endif
28
29 using namespace mozilla::ipc;
30
31 namespace mozilla {
32 namespace dom {
33
34 /***********************************************************************
35 * Statics
36 **********************************************************************/
37
38 namespace {
39 static mozilla::LazyLogModule gWebAuthnManagerLog("webauthnmanager");
40 }
41
42 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(WebAuthnManager,
43 WebAuthnManagerBase)
44
45 NS_IMPL_CYCLE_COLLECTION_CLASS(WebAuthnManager)
46
47 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WebAuthnManager,
48 WebAuthnManagerBase)
49 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransaction)
50 tmp->mTransaction.reset();
51 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
52
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WebAuthnManager,WebAuthnManagerBase)53 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WebAuthnManager,
54 WebAuthnManagerBase)
55 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransaction)
56 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
57
58 /***********************************************************************
59 * Utility Functions
60 **********************************************************************/
61
62 static nsresult AssembleClientData(
63 const nsAString& aOrigin, const CryptoBuffer& aChallenge,
64 const nsAString& aType,
65 const AuthenticationExtensionsClientInputs& aExtensions,
66 /* out */ nsACString& aJsonOut) {
67 MOZ_ASSERT(NS_IsMainThread());
68
69 nsString challengeBase64;
70 nsresult rv = aChallenge.ToJwkBase64(challengeBase64);
71 if (NS_WARN_IF(NS_FAILED(rv))) {
72 return NS_ERROR_FAILURE;
73 }
74
75 CollectedClientData clientDataObject;
76 clientDataObject.mType.Assign(aType);
77 clientDataObject.mChallenge.Assign(challengeBase64);
78 clientDataObject.mOrigin.Assign(aOrigin);
79 clientDataObject.mHashAlgorithm.AssignLiteral(u"SHA-256");
80 clientDataObject.mClientExtensions = aExtensions;
81
82 nsAutoString temp;
83 if (NS_WARN_IF(!clientDataObject.ToJSON(temp))) {
84 return NS_ERROR_FAILURE;
85 }
86
87 aJsonOut.Assign(NS_ConvertUTF16toUTF8(temp));
88 return NS_OK;
89 }
90
GetOrigin(nsPIDOMWindowInner * aParent,nsAString & aOrigin,nsACString & aHost)91 nsresult GetOrigin(nsPIDOMWindowInner* aParent,
92 /*out*/ nsAString& aOrigin, /*out*/ nsACString& aHost) {
93 MOZ_ASSERT(aParent);
94 nsCOMPtr<Document> doc = aParent->GetDoc();
95 MOZ_ASSERT(doc);
96
97 nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
98 nsresult rv = nsContentUtils::GetUTFOrigin(principal, aOrigin);
99 if (NS_WARN_IF(NS_FAILED(rv)) || NS_WARN_IF(aOrigin.IsEmpty())) {
100 return NS_ERROR_FAILURE;
101 }
102
103 if (principal->GetIsIpAddress()) {
104 return NS_ERROR_DOM_SECURITY_ERR;
105 }
106
107 if (aOrigin.EqualsLiteral("null")) {
108 // 4.1.1.3 If callerOrigin is an opaque origin, reject promise with a
109 // DOMException whose name is "NotAllowedError", and terminate this
110 // algorithm
111 MOZ_LOG(gWebAuthnManagerLog, LogLevel::Debug,
112 ("Rejecting due to opaque origin"));
113 return NS_ERROR_DOM_NOT_ALLOWED_ERR;
114 }
115
116 nsCOMPtr<nsIURI> originUri;
117 auto* basePrin = BasePrincipal::Cast(principal);
118 if (NS_FAILED(basePrin->GetURI(getter_AddRefs(originUri)))) {
119 return NS_ERROR_FAILURE;
120 }
121 if (NS_FAILED(originUri->GetAsciiHost(aHost))) {
122 return NS_ERROR_FAILURE;
123 }
124
125 return NS_OK;
126 }
127
RelaxSameOrigin(nsPIDOMWindowInner * aParent,const nsAString & aInputRpId,nsACString & aRelaxedRpId)128 nsresult RelaxSameOrigin(nsPIDOMWindowInner* aParent,
129 const nsAString& aInputRpId,
130 /* out */ nsACString& aRelaxedRpId) {
131 MOZ_ASSERT(aParent);
132 nsCOMPtr<Document> doc = aParent->GetDoc();
133 MOZ_ASSERT(doc);
134
135 nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
136 auto* basePrin = BasePrincipal::Cast(principal);
137 nsCOMPtr<nsIURI> uri;
138
139 if (NS_FAILED(basePrin->GetURI(getter_AddRefs(uri)))) {
140 return NS_ERROR_FAILURE;
141 }
142 nsAutoCString originHost;
143 if (NS_FAILED(uri->GetAsciiHost(originHost))) {
144 return NS_ERROR_FAILURE;
145 }
146 nsCOMPtr<Document> document = aParent->GetDoc();
147 if (!document || !document->IsHTMLDocument()) {
148 return NS_ERROR_FAILURE;
149 }
150 nsHTMLDocument* html = document->AsHTMLDocument();
151 // See if the given RP ID is a valid domain string.
152 // (We use the document's URI here as a template so we don't have to come up
153 // with our own scheme, etc. If we can successfully set the host as the given
154 // RP ID, then it should be a valid domain string.)
155 nsCOMPtr<nsIURI> inputRpIdURI;
156 nsresult rv = NS_MutateURI(uri)
157 .SetHost(NS_ConvertUTF16toUTF8(aInputRpId))
158 .Finalize(inputRpIdURI);
159 if (NS_FAILED(rv)) {
160 return NS_ERROR_DOM_SECURITY_ERR;
161 }
162 nsAutoCString inputRpId;
163 if (NS_FAILED(inputRpIdURI->GetAsciiHost(inputRpId))) {
164 return NS_ERROR_FAILURE;
165 }
166 if (!html->IsRegistrableDomainSuffixOfOrEqualTo(
167 NS_ConvertUTF8toUTF16(inputRpId), originHost)) {
168 return NS_ERROR_DOM_SECURITY_ERR;
169 }
170
171 aRelaxedRpId.Assign(inputRpId);
172 return NS_OK;
173 }
174
175 /***********************************************************************
176 * WebAuthnManager Implementation
177 **********************************************************************/
178
ClearTransaction()179 void WebAuthnManager::ClearTransaction() {
180 if (!mTransaction.isNothing()) {
181 StopListeningForVisibilityEvents();
182 }
183
184 mTransaction.reset();
185 Unfollow();
186 }
187
RejectTransaction(const nsresult & aError)188 void WebAuthnManager::RejectTransaction(const nsresult& aError) {
189 if (!NS_WARN_IF(mTransaction.isNothing())) {
190 mTransaction.ref().mPromise->MaybeReject(aError);
191 }
192
193 ClearTransaction();
194 }
195
CancelTransaction(const nsresult & aError)196 void WebAuthnManager::CancelTransaction(const nsresult& aError) {
197 if (!NS_WARN_IF(!mChild || mTransaction.isNothing())) {
198 mChild->SendRequestCancel(mTransaction.ref().mId);
199 }
200
201 RejectTransaction(aError);
202 }
203
HandleVisibilityChange()204 void WebAuthnManager::HandleVisibilityChange() {
205 if (mTransaction.isSome()) {
206 mTransaction.ref().mVisibilityChanged = true;
207 }
208 }
209
~WebAuthnManager()210 WebAuthnManager::~WebAuthnManager() {
211 MOZ_ASSERT(NS_IsMainThread());
212
213 if (mTransaction.isSome()) {
214 ClearTransaction();
215 }
216
217 if (mChild) {
218 RefPtr<WebAuthnTransactionChild> c;
219 mChild.swap(c);
220 c->Disconnect();
221 }
222 }
223
MakeCredential(const PublicKeyCredentialCreationOptions & aOptions,const Optional<OwningNonNull<AbortSignal>> & aSignal)224 already_AddRefed<Promise> WebAuthnManager::MakeCredential(
225 const PublicKeyCredentialCreationOptions& aOptions,
226 const Optional<OwningNonNull<AbortSignal>>& aSignal) {
227 MOZ_ASSERT(NS_IsMainThread());
228
229 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
230
231 ErrorResult rv;
232 RefPtr<Promise> promise = Promise::Create(global, rv);
233 if (rv.Failed()) {
234 return nullptr;
235 }
236
237 if (mTransaction.isSome()) {
238 // If there hasn't been a visibility change during the current
239 // transaction, then let's let that one complete rather than
240 // cancelling it on a subsequent call.
241 if (!mTransaction.ref().mVisibilityChanged) {
242 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
243 return promise.forget();
244 }
245
246 // Otherwise, the user may well have clicked away, so let's
247 // abort the old transaction and take over control from here.
248 CancelTransaction(NS_ERROR_ABORT);
249 }
250
251 // Abort the request if aborted flag is already set.
252 if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
253 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
254 return promise.forget();
255 }
256
257 nsString origin;
258 nsCString rpId;
259 rv = GetOrigin(mParent, origin, rpId);
260 if (NS_WARN_IF(rv.Failed())) {
261 promise->MaybeReject(std::move(rv));
262 return promise.forget();
263 }
264
265 // Enforce 5.4.3 User Account Parameters for Credential Generation
266 // When we add UX, we'll want to do more with this value, but for now
267 // we just have to verify its correctness.
268
269 CryptoBuffer userId;
270 userId.Assign(aOptions.mUser.mId);
271 if (userId.Length() > 64) {
272 promise->MaybeRejectWithTypeError("user.id is too long");
273 return promise.forget();
274 }
275
276 // If timeoutSeconds was specified, check if its value lies within a
277 // reasonable range as defined by the platform and if not, correct it to the
278 // closest value lying within that range.
279
280 uint32_t adjustedTimeout = 30000;
281 if (aOptions.mTimeout.WasPassed()) {
282 adjustedTimeout = aOptions.mTimeout.Value();
283 adjustedTimeout = std::max(15000u, adjustedTimeout);
284 adjustedTimeout = std::min(120000u, adjustedTimeout);
285 }
286
287 if (aOptions.mRp.mId.WasPassed()) {
288 // If rpId is specified, then invoke the procedure used for relaxing the
289 // same-origin restriction by setting the document.domain attribute, using
290 // rpId as the given value but without changing the current document’s
291 // domain. If no errors are thrown, set rpId to the value of host as
292 // computed by this procedure, and rpIdHash to the SHA-256 hash of rpId.
293 // Otherwise, reject promise with a DOMException whose name is
294 // "SecurityError", and terminate this algorithm.
295
296 if (NS_FAILED(RelaxSameOrigin(mParent, aOptions.mRp.mId.Value(), rpId))) {
297 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
298 return promise.forget();
299 }
300 }
301
302 // <https://w3c.github.io/webauthn/#sctn-appid-extension>
303 if (aOptions.mExtensions.mAppid.WasPassed()) {
304 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
305 return promise.forget();
306 }
307
308 // Process each element of mPubKeyCredParams using the following steps, to
309 // produce a new sequence coseAlgos.
310 nsTArray<CoseAlg> coseAlgos;
311 for (size_t a = 0; a < aOptions.mPubKeyCredParams.Length(); ++a) {
312 // If current.type does not contain a PublicKeyCredentialType
313 // supported by this implementation, then stop processing current and move
314 // on to the next element in mPubKeyCredParams.
315 if (aOptions.mPubKeyCredParams[a].mType !=
316 PublicKeyCredentialType::Public_key) {
317 continue;
318 }
319
320 coseAlgos.AppendElement(aOptions.mPubKeyCredParams[a].mAlg);
321 }
322
323 // If there are algorithms specified, but none are Public_key algorithms,
324 // reject the promise.
325 if (coseAlgos.IsEmpty() && !aOptions.mPubKeyCredParams.IsEmpty()) {
326 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
327 return promise.forget();
328 }
329
330 // If excludeList is undefined, set it to the empty list.
331 //
332 // If extensions was specified, process any extensions supported by this
333 // client platform, to produce the extension data that needs to be sent to the
334 // authenticator. If an error is encountered while processing an extension,
335 // skip that extension and do not produce any extension data for it. Call the
336 // result of this processing clientExtensions.
337 //
338 // Currently no extensions are supported
339 //
340 // Use attestationChallenge, callerOrigin and rpId, along with the token
341 // binding key associated with callerOrigin (if any), to create a ClientData
342 // structure representing this request. Choose a hash algorithm for hashAlg
343 // and compute the clientDataJSON and clientDataHash.
344
345 CryptoBuffer challenge;
346 if (!challenge.Assign(aOptions.mChallenge)) {
347 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
348 return promise.forget();
349 }
350
351 nsAutoCString clientDataJSON;
352 nsresult srv = AssembleClientData(origin, challenge, u"webauthn.create"_ns,
353 aOptions.mExtensions, clientDataJSON);
354 if (NS_WARN_IF(NS_FAILED(srv))) {
355 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
356 return promise.forget();
357 }
358
359 nsTArray<WebAuthnScopedCredential> excludeList;
360 for (const auto& s : aOptions.mExcludeCredentials) {
361 WebAuthnScopedCredential c;
362 CryptoBuffer cb;
363 cb.Assign(s.mId);
364 c.id() = cb;
365 excludeList.AppendElement(c);
366 }
367
368 if (!MaybeCreateBackgroundActor()) {
369 promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
370 return promise.forget();
371 }
372
373 // TODO: Add extension list building
374 nsTArray<WebAuthnExtension> extensions;
375
376 // <https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#sctn-hmac-secret-extension>
377 if (aOptions.mExtensions.mHmacCreateSecret.WasPassed()) {
378 bool hmacCreateSecret = aOptions.mExtensions.mHmacCreateSecret.Value();
379 if (hmacCreateSecret) {
380 extensions.AppendElement(WebAuthnExtensionHmacSecret(hmacCreateSecret));
381 }
382 }
383
384 const auto& selection = aOptions.mAuthenticatorSelection;
385 const auto& attachment = selection.mAuthenticatorAttachment;
386 const AttestationConveyancePreference& attestation = aOptions.mAttestation;
387
388 // Attachment
389 Maybe<AuthenticatorAttachment> authenticatorAttachment;
390 if (attachment.WasPassed()) {
391 authenticatorAttachment.emplace(attachment.Value());
392 }
393
394 // Create and forward authenticator selection criteria.
395 WebAuthnAuthenticatorSelection authSelection(selection.mRequireResidentKey,
396 selection.mUserVerification,
397 authenticatorAttachment);
398
399 nsString rpIcon;
400 if (aOptions.mRp.mIcon.WasPassed()) {
401 rpIcon = aOptions.mRp.mIcon.Value();
402 }
403
404 nsString userIcon;
405 if (aOptions.mUser.mIcon.WasPassed()) {
406 userIcon = aOptions.mUser.mIcon.Value();
407 }
408
409 WebAuthnMakeCredentialRpInfo rpInfo(aOptions.mRp.mName, rpIcon);
410
411 WebAuthnMakeCredentialUserInfo userInfo(
412 userId, aOptions.mUser.mName, userIcon, aOptions.mUser.mDisplayName);
413
414 WebAuthnMakeCredentialExtraInfo extra(rpInfo, userInfo, coseAlgos, extensions,
415 authSelection, attestation);
416
417 BrowsingContext* context = mParent->GetBrowsingContext();
418 if (!context) {
419 promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
420 return promise.forget();
421 }
422
423 WebAuthnMakeCredentialInfo info(
424 origin, NS_ConvertUTF8toUTF16(rpId), challenge, clientDataJSON,
425 adjustedTimeout, excludeList, Some(extra), context->Top()->Id());
426
427 #ifdef OS_WIN
428 if (!WinWebAuthnManager::AreWebAuthNApisAvailable()) {
429 ListenForVisibilityEvents();
430 }
431 #else
432 ListenForVisibilityEvents();
433 #endif
434
435 AbortSignal* signal = nullptr;
436 if (aSignal.WasPassed()) {
437 signal = &aSignal.Value();
438 Follow(signal);
439 }
440
441 MOZ_ASSERT(mTransaction.isNothing());
442 mTransaction = Some(WebAuthnTransaction(promise));
443 mChild->SendRequestRegister(mTransaction.ref().mId, info);
444
445 return promise.forget();
446 }
447
GetAssertion(const PublicKeyCredentialRequestOptions & aOptions,const Optional<OwningNonNull<AbortSignal>> & aSignal)448 already_AddRefed<Promise> WebAuthnManager::GetAssertion(
449 const PublicKeyCredentialRequestOptions& aOptions,
450 const Optional<OwningNonNull<AbortSignal>>& aSignal) {
451 MOZ_ASSERT(NS_IsMainThread());
452
453 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
454
455 ErrorResult rv;
456 RefPtr<Promise> promise = Promise::Create(global, rv);
457 if (rv.Failed()) {
458 return nullptr;
459 }
460
461 if (mTransaction.isSome()) {
462 // If there hasn't been a visibility change during the current
463 // transaction, then let's let that one complete rather than
464 // cancelling it on a subsequent call.
465 if (!mTransaction.ref().mVisibilityChanged) {
466 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
467 return promise.forget();
468 }
469
470 // Otherwise, the user may well have clicked away, so let's
471 // abort the old transaction and take over control from here.
472 CancelTransaction(NS_ERROR_ABORT);
473 }
474
475 // Abort the request if aborted flag is already set.
476 if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
477 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
478 return promise.forget();
479 }
480
481 nsString origin;
482 nsCString rpId;
483 rv = GetOrigin(mParent, origin, rpId);
484 if (NS_WARN_IF(rv.Failed())) {
485 promise->MaybeReject(std::move(rv));
486 return promise.forget();
487 }
488
489 // If timeoutSeconds was specified, check if its value lies within a
490 // reasonable range as defined by the platform and if not, correct it to the
491 // closest value lying within that range.
492
493 uint32_t adjustedTimeout = 30000;
494 if (aOptions.mTimeout.WasPassed()) {
495 adjustedTimeout = aOptions.mTimeout.Value();
496 adjustedTimeout = std::max(15000u, adjustedTimeout);
497 adjustedTimeout = std::min(120000u, adjustedTimeout);
498 }
499
500 if (aOptions.mRpId.WasPassed()) {
501 // If rpId is specified, then invoke the procedure used for relaxing the
502 // same-origin restriction by setting the document.domain attribute, using
503 // rpId as the given value but without changing the current document’s
504 // domain. If no errors are thrown, set rpId to the value of host as
505 // computed by this procedure, and rpIdHash to the SHA-256 hash of rpId.
506 // Otherwise, reject promise with a DOMException whose name is
507 // "SecurityError", and terminate this algorithm.
508
509 if (NS_FAILED(RelaxSameOrigin(mParent, aOptions.mRpId.Value(), rpId))) {
510 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
511 return promise.forget();
512 }
513 }
514
515 CryptoBuffer rpIdHash;
516 if (!rpIdHash.SetLength(SHA256_LENGTH, fallible)) {
517 promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
518 return promise.forget();
519 }
520
521 // Use assertionChallenge, callerOrigin and rpId, along with the token binding
522 // key associated with callerOrigin (if any), to create a ClientData structure
523 // representing this request. Choose a hash algorithm for hashAlg and compute
524 // the clientDataJSON and clientDataHash.
525 CryptoBuffer challenge;
526 if (!challenge.Assign(aOptions.mChallenge)) {
527 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
528 return promise.forget();
529 }
530
531 nsAutoCString clientDataJSON;
532 nsresult srv = AssembleClientData(origin, challenge, u"webauthn.get"_ns,
533 aOptions.mExtensions, clientDataJSON);
534 if (NS_WARN_IF(NS_FAILED(srv))) {
535 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
536 return promise.forget();
537 }
538
539 nsTArray<WebAuthnScopedCredential> allowList;
540 for (const auto& s : aOptions.mAllowCredentials) {
541 if (s.mType == PublicKeyCredentialType::Public_key) {
542 WebAuthnScopedCredential c;
543 CryptoBuffer cb;
544 cb.Assign(s.mId);
545 c.id() = cb;
546
547 // Serialize transports.
548 if (s.mTransports.WasPassed()) {
549 uint8_t transports = 0;
550
551 // Transports is a string, but we match it to an enumeration so
552 // that we have forward-compatibility, ignoring unknown transports.
553 for (const nsAString& str : s.mTransports.Value()) {
554 NS_ConvertUTF16toUTF8 cStr(str);
555 int i = FindEnumStringIndexImpl(
556 cStr.get(), cStr.Length(), AuthenticatorTransportValues::strings);
557 if (i < 0) {
558 continue; // Unknown enum
559 }
560 AuthenticatorTransport t = static_cast<AuthenticatorTransport>(i);
561
562 if (t == AuthenticatorTransport::Usb) {
563 transports |= U2F_AUTHENTICATOR_TRANSPORT_USB;
564 }
565 if (t == AuthenticatorTransport::Nfc) {
566 transports |= U2F_AUTHENTICATOR_TRANSPORT_NFC;
567 }
568 if (t == AuthenticatorTransport::Ble) {
569 transports |= U2F_AUTHENTICATOR_TRANSPORT_BLE;
570 }
571 if (t == AuthenticatorTransport::Internal) {
572 transports |= CTAP_AUTHENTICATOR_TRANSPORT_INTERNAL;
573 }
574 }
575 c.transports() = transports;
576 }
577
578 allowList.AppendElement(c);
579 }
580 }
581
582 if (!MaybeCreateBackgroundActor()) {
583 promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
584 return promise.forget();
585 }
586
587 // If extensions were specified, process any extensions supported by this
588 // client platform, to produce the extension data that needs to be sent to the
589 // authenticator. If an error is encountered while processing an extension,
590 // skip that extension and do not produce any extension data for it. Call the
591 // result of this processing clientExtensions.
592 nsTArray<WebAuthnExtension> extensions;
593
594 // <https://w3c.github.io/webauthn/#sctn-appid-extension>
595 if (aOptions.mExtensions.mAppid.WasPassed()) {
596 nsString appId(aOptions.mExtensions.mAppid.Value());
597
598 // Check that the appId value is allowed.
599 if (!EvaluateAppID(mParent, origin, appId)) {
600 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
601 return promise.forget();
602 }
603
604 CryptoBuffer appIdHash;
605 if (!appIdHash.SetLength(SHA256_LENGTH, fallible)) {
606 promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
607 return promise.forget();
608 }
609
610 // We need the SHA-256 hash of the appId.
611 srv = HashCString(NS_ConvertUTF16toUTF8(appId), appIdHash);
612 if (NS_WARN_IF(NS_FAILED(srv))) {
613 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
614 return promise.forget();
615 }
616
617 // Append the hash and send it to the backend.
618 extensions.AppendElement(WebAuthnExtensionAppId(appIdHash, appId));
619 }
620
621 WebAuthnGetAssertionExtraInfo extra(extensions, aOptions.mUserVerification);
622
623 BrowsingContext* context = mParent->GetBrowsingContext();
624 if (!context) {
625 promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
626 return promise.forget();
627 }
628
629 WebAuthnGetAssertionInfo info(origin, NS_ConvertUTF8toUTF16(rpId), challenge,
630 clientDataJSON, adjustedTimeout, allowList,
631 Some(extra), context->Top()->Id());
632
633 #ifdef OS_WIN
634 if (!WinWebAuthnManager::AreWebAuthNApisAvailable()) {
635 ListenForVisibilityEvents();
636 }
637 #else
638 ListenForVisibilityEvents();
639 #endif
640
641 AbortSignal* signal = nullptr;
642 if (aSignal.WasPassed()) {
643 signal = &aSignal.Value();
644 Follow(signal);
645 }
646
647 MOZ_ASSERT(mTransaction.isNothing());
648 mTransaction = Some(WebAuthnTransaction(promise));
649 mChild->SendRequestSign(mTransaction.ref().mId, info);
650
651 return promise.forget();
652 }
653
Store(const Credential & aCredential)654 already_AddRefed<Promise> WebAuthnManager::Store(
655 const Credential& aCredential) {
656 MOZ_ASSERT(NS_IsMainThread());
657
658 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
659
660 ErrorResult rv;
661 RefPtr<Promise> promise = Promise::Create(global, rv);
662 if (rv.Failed()) {
663 return nullptr;
664 }
665
666 if (mTransaction.isSome()) {
667 // If there hasn't been a visibility change during the current
668 // transaction, then let's let that one complete rather than
669 // cancelling it on a subsequent call.
670 if (!mTransaction.ref().mVisibilityChanged) {
671 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
672 return promise.forget();
673 }
674
675 // Otherwise, the user may well have clicked away, so let's
676 // abort the old transaction and take over control from here.
677 CancelTransaction(NS_ERROR_ABORT);
678 }
679
680 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
681 return promise.forget();
682 }
683
FinishMakeCredential(const uint64_t & aTransactionId,const WebAuthnMakeCredentialResult & aResult)684 void WebAuthnManager::FinishMakeCredential(
685 const uint64_t& aTransactionId,
686 const WebAuthnMakeCredentialResult& aResult) {
687 MOZ_ASSERT(NS_IsMainThread());
688
689 // Check for a valid transaction.
690 if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
691 return;
692 }
693
694 CryptoBuffer clientDataBuf;
695 if (NS_WARN_IF(!clientDataBuf.Assign(aResult.ClientDataJSON()))) {
696 RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
697 return;
698 }
699
700 CryptoBuffer attObjBuf;
701 if (NS_WARN_IF(!attObjBuf.Assign(aResult.AttestationObject()))) {
702 RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
703 return;
704 }
705
706 CryptoBuffer keyHandleBuf;
707 if (NS_WARN_IF(!keyHandleBuf.Assign(aResult.KeyHandle()))) {
708 RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
709 return;
710 }
711
712 nsAutoString keyHandleBase64Url;
713 nsresult rv = keyHandleBuf.ToJwkBase64(keyHandleBase64Url);
714 if (NS_WARN_IF(NS_FAILED(rv))) {
715 RejectTransaction(rv);
716 return;
717 }
718
719 // Create a new PublicKeyCredential object and populate its fields with the
720 // values returned from the authenticator as well as the clientDataJSON
721 // computed earlier.
722 RefPtr<AuthenticatorAttestationResponse> attestation =
723 new AuthenticatorAttestationResponse(mParent);
724 attestation->SetClientDataJSON(clientDataBuf);
725 attestation->SetAttestationObject(attObjBuf);
726
727 RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mParent);
728 credential->SetId(keyHandleBase64Url);
729 credential->SetType(u"public-key"_ns);
730 credential->SetRawId(keyHandleBuf);
731 credential->SetResponse(attestation);
732
733 // Forward client extension results.
734 for (auto& ext : aResult.Extensions()) {
735 if (ext.type() ==
736 WebAuthnExtensionResult::TWebAuthnExtensionResultHmacSecret) {
737 bool hmacCreateSecret =
738 ext.get_WebAuthnExtensionResultHmacSecret().hmacCreateSecret();
739 credential->SetClientExtensionResultHmacSecret(hmacCreateSecret);
740 }
741 }
742
743 mTransaction.ref().mPromise->MaybeResolve(credential);
744 ClearTransaction();
745 }
746
FinishGetAssertion(const uint64_t & aTransactionId,const WebAuthnGetAssertionResult & aResult)747 void WebAuthnManager::FinishGetAssertion(
748 const uint64_t& aTransactionId, const WebAuthnGetAssertionResult& aResult) {
749 MOZ_ASSERT(NS_IsMainThread());
750
751 // Check for a valid transaction.
752 if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
753 return;
754 }
755
756 CryptoBuffer clientDataBuf;
757 if (!clientDataBuf.Assign(aResult.ClientDataJSON())) {
758 RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
759 return;
760 }
761
762 CryptoBuffer credentialBuf;
763 if (!credentialBuf.Assign(aResult.KeyHandle())) {
764 RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
765 return;
766 }
767
768 CryptoBuffer signatureBuf;
769 if (!signatureBuf.Assign(aResult.Signature())) {
770 RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
771 return;
772 }
773
774 CryptoBuffer authenticatorDataBuf;
775 if (!authenticatorDataBuf.Assign(aResult.AuthenticatorData())) {
776 RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
777 return;
778 }
779
780 nsAutoString credentialBase64Url;
781 nsresult rv = credentialBuf.ToJwkBase64(credentialBase64Url);
782 if (NS_WARN_IF(NS_FAILED(rv))) {
783 RejectTransaction(rv);
784 return;
785 }
786
787 CryptoBuffer userHandleBuf;
788 // U2FTokenManager don't return user handle.
789 // Best effort.
790 userHandleBuf.Assign(aResult.UserHandle());
791
792 // If any authenticator returns success:
793
794 // Create a new PublicKeyCredential object named value and populate its fields
795 // with the values returned from the authenticator as well as the
796 // clientDataJSON computed earlier.
797 RefPtr<AuthenticatorAssertionResponse> assertion =
798 new AuthenticatorAssertionResponse(mParent);
799 assertion->SetClientDataJSON(clientDataBuf);
800 assertion->SetAuthenticatorData(authenticatorDataBuf);
801 assertion->SetSignature(signatureBuf);
802 if (!userHandleBuf.IsEmpty()) {
803 assertion->SetUserHandle(userHandleBuf);
804 }
805
806 RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mParent);
807 credential->SetId(credentialBase64Url);
808 credential->SetType(u"public-key"_ns);
809 credential->SetRawId(credentialBuf);
810 credential->SetResponse(assertion);
811
812 // Forward client extension results.
813 for (auto& ext : aResult.Extensions()) {
814 if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultAppId) {
815 bool appid = ext.get_WebAuthnExtensionResultAppId().AppId();
816 credential->SetClientExtensionResultAppId(appid);
817 }
818 }
819
820 mTransaction.ref().mPromise->MaybeResolve(credential);
821 ClearTransaction();
822 }
823
RequestAborted(const uint64_t & aTransactionId,const nsresult & aError)824 void WebAuthnManager::RequestAborted(const uint64_t& aTransactionId,
825 const nsresult& aError) {
826 MOZ_ASSERT(NS_IsMainThread());
827
828 if (mTransaction.isSome() && mTransaction.ref().mId == aTransactionId) {
829 RejectTransaction(aError);
830 }
831 }
832
RunAbortAlgorithm()833 void WebAuthnManager::RunAbortAlgorithm() {
834 CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
835 }
836
837 } // namespace dom
838 } // namespace mozilla
839