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/. */
7 #include "mozilla/dom/U2F.h"
8 #include "mozilla/dom/WebCryptoCommon.h"
9 #include "mozilla/ipc/PBackgroundChild.h"
10 #include "mozilla/ipc/BackgroundChild.h"
11 #include "mozilla/dom/WebAuthnTransactionChild.h"
12 #include "mozilla/dom/WebAuthnUtil.h"
13 #include "nsContentUtils.h"
14 #include "nsNetUtil.h"
15 #include "nsURLParsers.h"
18 # include "WinWebAuthnManager.h"
21 using namespace mozilla::ipc
;
23 // Forward decl because of nsHTMLDocument.h's complex dependency on
25 class nsHTMLDocument
{
27 bool IsRegistrableDomainSuffixOfOrEqualTo(const nsAString
& aHostSuffixString
,
28 const nsACString
& aOrigHost
);
34 NS_NAMED_LITERAL_STRING(kFinishEnrollment
, "navigator.id.finishEnrollment");
35 NS_NAMED_LITERAL_STRING(kGetAssertion
, "navigator.id.getAssertion");
37 // Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023.
38 NS_NAMED_LITERAL_STRING(kGoogleAccountsAppId1
,
39 "https://www.gstatic.com/securitykey/origins.json");
40 NS_NAMED_LITERAL_STRING(
41 kGoogleAccountsAppId2
,
42 "https://www.gstatic.com/securitykey/a/google.com/origins.json");
44 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(U2F
)
45 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
46 NS_INTERFACE_MAP_END_INHERITING(WebAuthnManagerBase
)
48 NS_IMPL_ADDREF_INHERITED(U2F
, WebAuthnManagerBase
)
49 NS_IMPL_RELEASE_INHERITED(U2F
, WebAuthnManagerBase
)
51 NS_IMPL_CYCLE_COLLECTION_CLASS(U2F
)
52 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(U2F
, WebAuthnManagerBase
)
53 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransaction
)
54 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
55 tmp
->mTransaction
.reset();
56 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
57 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(U2F
, WebAuthnManagerBase
)
58 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransaction
)
59 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
60 NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(U2F
)
62 /***********************************************************************
64 **********************************************************************/
66 static ErrorCode
ConvertNSResultToErrorCode(const nsresult
& aError
) {
67 if (aError
== NS_ERROR_DOM_TIMEOUT_ERR
) {
68 return ErrorCode::TIMEOUT
;
70 /* Emitted by U2F{Soft,HID}TokenManager when we really mean ineligible */
71 if (aError
== NS_ERROR_DOM_INVALID_STATE_ERR
) {
72 return ErrorCode::DEVICE_INELIGIBLE
;
74 return ErrorCode::OTHER_ERROR
;
77 static uint32_t AdjustedTimeoutMillis(
78 const Optional
<Nullable
<int32_t>>& opt_aSeconds
) {
79 uint32_t adjustedTimeoutMillis
= 30000u;
80 if (opt_aSeconds
.WasPassed() && !opt_aSeconds
.Value().IsNull()) {
81 adjustedTimeoutMillis
= opt_aSeconds
.Value().Value() * 1000u;
82 adjustedTimeoutMillis
= std::max(15000u, adjustedTimeoutMillis
);
83 adjustedTimeoutMillis
= std::min(120000u, adjustedTimeoutMillis
);
85 return adjustedTimeoutMillis
;
88 static nsresult
AssembleClientData(const nsAString
& aOrigin
,
89 const nsAString
& aTyp
,
90 const nsAString
& aChallenge
,
91 /* out */ nsString
& aClientData
) {
92 MOZ_ASSERT(NS_IsMainThread());
93 U2FClientData clientDataObject
;
94 clientDataObject
.mTyp
.Construct(aTyp
); // "Typ" from the U2F specification
95 clientDataObject
.mChallenge
.Construct(aChallenge
);
96 clientDataObject
.mOrigin
.Construct(aOrigin
);
98 if (NS_WARN_IF(!clientDataObject
.ToJSON(aClientData
))) {
99 return NS_ERROR_FAILURE
;
105 static void RegisteredKeysToScopedCredentialList(
106 const nsAString
& aAppId
, const nsTArray
<RegisteredKey
>& aKeys
,
107 nsTArray
<WebAuthnScopedCredential
>& aList
) {
108 for (const RegisteredKey
& key
: aKeys
) {
109 // Check for required attributes
110 if (!key
.mVersion
.WasPassed() || !key
.mKeyHandle
.WasPassed() ||
111 key
.mVersion
.Value() != kRequiredU2FVersion
) {
115 // If this key's mAppId doesn't match the invocation, we can't handle it.
116 if (key
.mAppId
.WasPassed() && !key
.mAppId
.Value().Equals(aAppId
)) {
120 CryptoBuffer keyHandle
;
121 nsresult rv
= keyHandle
.FromJwkBase64(key
.mKeyHandle
.Value());
122 if (NS_WARN_IF(NS_FAILED(rv
))) {
126 WebAuthnScopedCredential c
;
128 aList
.AppendElement(c
);
132 /***********************************************************************
133 * U2F JavaScript API Implementation
134 **********************************************************************/
137 MOZ_ASSERT(NS_IsMainThread());
139 if (mTransaction
.isSome()) {
144 RefPtr
<WebAuthnTransactionChild
> c
;
150 void U2F::Init(ErrorResult
& aRv
) {
153 nsCOMPtr
<Document
> doc
= mParent
->GetDoc();
156 aRv
.Throw(NS_ERROR_FAILURE
);
160 nsIPrincipal
* principal
= doc
->NodePrincipal();
161 aRv
= nsContentUtils::GetUTFOrigin(principal
, mOrigin
);
162 if (NS_WARN_IF(aRv
.Failed())) {
166 if (NS_WARN_IF(mOrigin
.IsEmpty())) {
167 aRv
.Throw(NS_ERROR_FAILURE
);
173 JSObject
* U2F::WrapObject(JSContext
* aCx
, JS::Handle
<JSObject
*> aGivenProto
) {
174 return U2F_Binding::Wrap(aCx
, this, aGivenProto
);
177 template <typename T
, typename C
>
178 void U2F::ExecuteCallback(T
& aResp
, nsMainThreadPtrHandle
<C
>& aCb
) {
179 MOZ_ASSERT(NS_IsMainThread());
183 aCb
->Call(aResp
, error
);
184 NS_WARNING_ASSERTION(!error
.Failed(), "dom::U2F::Promise callback failed");
185 error
.SuppressException(); // Useful exceptions already emitted
188 void U2F::Register(const nsAString
& aAppId
,
189 const Sequence
<RegisterRequest
>& aRegisterRequests
,
190 const Sequence
<RegisteredKey
>& aRegisteredKeys
,
191 U2FRegisterCallback
& aCallback
,
192 const Optional
<Nullable
<int32_t>>& opt_aTimeoutSeconds
,
194 MOZ_ASSERT(NS_IsMainThread());
196 nsMainThreadPtrHandle
<U2FRegisterCallback
> callback(
197 new nsMainThreadPtrHolder
<U2FRegisterCallback
>("U2F::Register::callback",
200 // Ensure we have a callback.
201 if (NS_WARN_IF(!callback
)) {
205 if (mTransaction
.isSome()) {
206 // If there hasn't been a visibility change during the current
207 // transaction, then let's let that one complete rather than
208 // cancelling it on a subsequent call.
209 if (!mTransaction
.ref().mVisibilityChanged
) {
210 RegisterResponse response
;
211 response
.mErrorCode
.Construct(
212 static_cast<uint32_t>(ErrorCode::OTHER_ERROR
));
213 ExecuteCallback(response
, callback
);
217 // Otherwise, the user may well have clicked away, so let's
218 // abort the old transaction and take over control from here.
219 CancelTransaction(NS_ERROR_ABORT
);
222 // Evaluate the AppID
223 nsString
adjustedAppId(aAppId
);
224 if (!EvaluateAppID(mParent
, mOrigin
, adjustedAppId
)) {
225 RegisterResponse response
;
226 response
.mErrorCode
.Construct(
227 static_cast<uint32_t>(ErrorCode::BAD_REQUEST
));
228 ExecuteCallback(response
, callback
);
232 nsAutoString clientDataJSON
;
234 // Pick the first valid RegisterRequest; we can only work with one.
235 CryptoBuffer challenge
;
236 for (const RegisterRequest
& req
: aRegisterRequests
) {
237 if (!req
.mChallenge
.WasPassed() || !req
.mVersion
.WasPassed() ||
238 req
.mVersion
.Value() != kRequiredU2FVersion
) {
241 if (!challenge
.Assign(NS_ConvertUTF16toUTF8(req
.mChallenge
.Value()))) {
245 nsresult rv
= AssembleClientData(mOrigin
, kFinishEnrollment
,
246 req
.mChallenge
.Value(), clientDataJSON
);
247 if (NS_WARN_IF(NS_FAILED(rv
))) {
252 // Did we not get a valid RegisterRequest? Abort.
253 if (clientDataJSON
.IsEmpty()) {
254 RegisterResponse response
;
255 response
.mErrorCode
.Construct(
256 static_cast<uint32_t>(ErrorCode::BAD_REQUEST
));
257 ExecuteCallback(response
, callback
);
261 // Build the exclusion list, if any
262 nsTArray
<WebAuthnScopedCredential
> excludeList
;
263 RegisteredKeysToScopedCredentialList(adjustedAppId
, aRegisteredKeys
,
266 if (!MaybeCreateBackgroundActor()) {
267 RegisterResponse response
;
268 response
.mErrorCode
.Construct(
269 static_cast<uint32_t>(ErrorCode::OTHER_ERROR
));
270 ExecuteCallback(response
, callback
);
275 if (!WinWebAuthnManager::AreWebAuthNApisAvailable()) {
276 ListenForVisibilityEvents();
279 ListenForVisibilityEvents();
282 NS_ConvertUTF16toUTF8
clientData(clientDataJSON
);
283 uint32_t adjustedTimeoutMillis
= AdjustedTimeoutMillis(opt_aTimeoutSeconds
);
285 WebAuthnMakeCredentialInfo
info(mOrigin
, adjustedAppId
, challenge
, clientData
,
286 adjustedTimeoutMillis
, excludeList
,
287 Nothing() /* no extra info for U2F */);
289 MOZ_ASSERT(mTransaction
.isNothing());
290 mTransaction
= Some(U2FTransaction(AsVariant(callback
)));
291 mChild
->SendRequestRegister(mTransaction
.ref().mId
, info
);
294 using binding_detail::GenericMethod
;
295 using binding_detail::NormalThisPolicy
;
296 using binding_detail::ThrowExceptions
;
298 // register_impl_methodinfo is generated by bindings.
299 namespace U2F_Binding
{
300 extern const JSJitInfo register_impl_methodinfo
;
301 } // namespace U2F_Binding
303 // We have 4 non-optional args.
304 static const JSFunctionSpec register_spec
= JS_FNSPEC(
305 "register", (GenericMethod
<NormalThisPolicy
, ThrowExceptions
>),
306 &U2F_Binding::register_impl_methodinfo
, 4, JSPROP_ENUMERATE
, nullptr);
308 void U2F::GetRegister(JSContext
* aCx
,
309 JS::MutableHandle
<JSObject
*> aRegisterFunc
,
311 JSFunction
* fun
= JS::NewFunctionFromSpec(aCx
, ®ister_spec
);
313 aRv
.NoteJSContextException(aCx
);
317 aRegisterFunc
.set(JS_GetFunctionObject(fun
));
320 void U2F::FinishMakeCredential(const uint64_t& aTransactionId
,
321 const WebAuthnMakeCredentialResult
& aResult
) {
322 MOZ_ASSERT(NS_IsMainThread());
324 // Check for a valid transaction.
325 if (mTransaction
.isNothing() || mTransaction
.ref().mId
!= aTransactionId
) {
329 if (NS_WARN_IF(!mTransaction
.ref().HasRegisterCallback())) {
330 RejectTransaction(NS_ERROR_ABORT
);
335 if (aResult
.RegistrationData().Length() == 0) {
336 RejectTransaction(NS_ERROR_ABORT
);
340 CryptoBuffer clientDataBuf
;
341 if (NS_WARN_IF(!clientDataBuf
.Assign(aResult
.ClientDataJSON()))) {
342 RejectTransaction(NS_ERROR_ABORT
);
347 if (NS_WARN_IF(!regBuf
.Assign(aResult
.RegistrationData()))) {
348 RejectTransaction(NS_ERROR_ABORT
);
352 nsString clientDataBase64
;
353 nsString registrationDataBase64
;
354 nsresult rvClientData
= clientDataBuf
.ToJwkBase64(clientDataBase64
);
355 nsresult rvRegistrationData
= regBuf
.ToJwkBase64(registrationDataBase64
);
357 if (NS_WARN_IF(NS_FAILED(rvClientData
)) ||
358 NS_WARN_IF(NS_FAILED(rvRegistrationData
))) {
359 RejectTransaction(NS_ERROR_ABORT
);
363 // Assemble a response object to return
364 RegisterResponse response
;
365 response
.mVersion
.Construct(kRequiredU2FVersion
);
366 response
.mClientData
.Construct(clientDataBase64
);
367 response
.mRegistrationData
.Construct(registrationDataBase64
);
368 response
.mErrorCode
.Construct(static_cast<uint32_t>(ErrorCode::OK
));
370 // Keep the callback pointer alive.
371 nsMainThreadPtrHandle
<U2FRegisterCallback
> callback(
372 mTransaction
.ref().GetRegisterCallback());
375 ExecuteCallback(response
, callback
);
378 void U2F::Sign(const nsAString
& aAppId
, const nsAString
& aChallenge
,
379 const Sequence
<RegisteredKey
>& aRegisteredKeys
,
380 U2FSignCallback
& aCallback
,
381 const Optional
<Nullable
<int32_t>>& opt_aTimeoutSeconds
,
383 MOZ_ASSERT(NS_IsMainThread());
385 nsMainThreadPtrHandle
<U2FSignCallback
> callback(
386 new nsMainThreadPtrHolder
<U2FSignCallback
>("U2F::Sign::callback",
389 // Ensure we have a callback.
390 if (NS_WARN_IF(!callback
)) {
394 if (mTransaction
.isSome()) {
395 // If there hasn't been a visibility change during the current
396 // transaction, then let's let that one complete rather than
397 // cancelling it on a subsequent call.
398 if (!mTransaction
.ref().mVisibilityChanged
) {
399 SignResponse response
;
400 response
.mErrorCode
.Construct(
401 static_cast<uint32_t>(ErrorCode::OTHER_ERROR
));
402 ExecuteCallback(response
, callback
);
406 // Otherwise, the user may well have clicked away, so let's
407 // abort the old transaction and take over control from here.
408 CancelTransaction(NS_ERROR_ABORT
);
411 // Evaluate the AppID
412 nsString
adjustedAppId(aAppId
);
413 if (!EvaluateAppID(mParent
, mOrigin
, adjustedAppId
)) {
414 SignResponse response
;
415 response
.mErrorCode
.Construct(
416 static_cast<uint32_t>(ErrorCode::BAD_REQUEST
));
417 ExecuteCallback(response
, callback
);
421 // Produce the AppParam from the current AppID
422 nsCString cAppId
= NS_ConvertUTF16toUTF8(adjustedAppId
);
424 nsAutoString clientDataJSON
;
426 AssembleClientData(mOrigin
, kGetAssertion
, aChallenge
, clientDataJSON
);
427 if (NS_WARN_IF(NS_FAILED(rv
))) {
428 SignResponse response
;
429 response
.mErrorCode
.Construct(
430 static_cast<uint32_t>(ErrorCode::BAD_REQUEST
));
431 ExecuteCallback(response
, callback
);
435 CryptoBuffer challenge
;
436 if (!challenge
.Assign(NS_ConvertUTF16toUTF8(aChallenge
))) {
437 SignResponse response
;
438 response
.mErrorCode
.Construct(
439 static_cast<uint32_t>(ErrorCode::OTHER_ERROR
));
440 ExecuteCallback(response
, callback
);
444 // Build the key list, if any
445 nsTArray
<WebAuthnScopedCredential
> permittedList
;
446 RegisteredKeysToScopedCredentialList(adjustedAppId
, aRegisteredKeys
,
449 if (!MaybeCreateBackgroundActor()) {
450 SignResponse response
;
451 response
.mErrorCode
.Construct(
452 static_cast<uint32_t>(ErrorCode::OTHER_ERROR
));
453 ExecuteCallback(response
, callback
);
458 if (!WinWebAuthnManager::AreWebAuthNApisAvailable()) {
459 ListenForVisibilityEvents();
462 ListenForVisibilityEvents();
465 // Always blank for U2F
466 nsTArray
<WebAuthnExtension
> extensions
;
468 NS_ConvertUTF16toUTF8
clientData(clientDataJSON
);
469 uint32_t adjustedTimeoutMillis
= AdjustedTimeoutMillis(opt_aTimeoutSeconds
);
471 WebAuthnGetAssertionInfo
info(mOrigin
, adjustedAppId
, challenge
, clientData
,
472 adjustedTimeoutMillis
, permittedList
,
473 Nothing() /* no extra info for U2F */);
475 MOZ_ASSERT(mTransaction
.isNothing());
476 mTransaction
= Some(U2FTransaction(AsVariant(callback
)));
477 mChild
->SendRequestSign(mTransaction
.ref().mId
, info
);
480 // sign_impl_methodinfo is generated by bindings.
481 namespace U2F_Binding
{
482 extern const JSJitInfo sign_impl_methodinfo
;
483 } // namespace U2F_Binding
485 // We have 4 non-optional args.
486 static const JSFunctionSpec sign_spec
=
487 JS_FNSPEC("sign", (GenericMethod
<NormalThisPolicy
, ThrowExceptions
>),
488 &U2F_Binding::sign_impl_methodinfo
, 4, JSPROP_ENUMERATE
, nullptr);
490 void U2F::GetSign(JSContext
* aCx
, JS::MutableHandle
<JSObject
*> aSignFunc
,
492 JSFunction
* fun
= JS::NewFunctionFromSpec(aCx
, &sign_spec
);
494 aRv
.NoteJSContextException(aCx
);
498 aSignFunc
.set(JS_GetFunctionObject(fun
));
501 void U2F::FinishGetAssertion(const uint64_t& aTransactionId
,
502 const WebAuthnGetAssertionResult
& aResult
) {
503 MOZ_ASSERT(NS_IsMainThread());
505 // Check for a valid transaction.
506 if (mTransaction
.isNothing() || mTransaction
.ref().mId
!= aTransactionId
) {
510 if (NS_WARN_IF(!mTransaction
.ref().HasSignCallback())) {
511 RejectTransaction(NS_ERROR_ABORT
);
516 if (aResult
.SignatureData().Length() == 0) {
517 RejectTransaction(NS_ERROR_ABORT
);
521 CryptoBuffer clientDataBuf
;
522 if (NS_WARN_IF(!clientDataBuf
.Assign(aResult
.ClientDataJSON()))) {
523 RejectTransaction(NS_ERROR_ABORT
);
527 CryptoBuffer credBuf
;
528 if (NS_WARN_IF(!credBuf
.Assign(aResult
.KeyHandle()))) {
529 RejectTransaction(NS_ERROR_ABORT
);
534 if (NS_WARN_IF(!sigBuf
.Assign(aResult
.SignatureData()))) {
535 RejectTransaction(NS_ERROR_ABORT
);
539 // Assemble a response object to return
540 nsString clientDataBase64
;
541 nsString signatureDataBase64
;
542 nsString keyHandleBase64
;
543 nsresult rvClientData
= clientDataBuf
.ToJwkBase64(clientDataBase64
);
544 nsresult rvSignatureData
= sigBuf
.ToJwkBase64(signatureDataBase64
);
545 nsresult rvKeyHandle
= credBuf
.ToJwkBase64(keyHandleBase64
);
546 if (NS_WARN_IF(NS_FAILED(rvClientData
)) ||
547 NS_WARN_IF(NS_FAILED(rvSignatureData
) ||
548 NS_WARN_IF(NS_FAILED(rvKeyHandle
)))) {
549 RejectTransaction(NS_ERROR_ABORT
);
553 SignResponse response
;
554 response
.mKeyHandle
.Construct(keyHandleBase64
);
555 response
.mClientData
.Construct(clientDataBase64
);
556 response
.mSignatureData
.Construct(signatureDataBase64
);
557 response
.mErrorCode
.Construct(static_cast<uint32_t>(ErrorCode::OK
));
559 // Keep the callback pointer alive.
560 nsMainThreadPtrHandle
<U2FSignCallback
> callback(
561 mTransaction
.ref().GetSignCallback());
564 ExecuteCallback(response
, callback
);
567 void U2F::ClearTransaction() {
568 if (!mTransaction
.isNothing()) {
569 StopListeningForVisibilityEvents();
572 mTransaction
.reset();
575 void U2F::RejectTransaction(const nsresult
& aError
) {
576 if (NS_WARN_IF(mTransaction
.isNothing())) {
580 StopListeningForVisibilityEvents();
582 // Clear out mTransaction before calling ExecuteCallback() below to allow
583 // reentrancy from microtask checkpoints.
584 Maybe
<U2FTransaction
> maybeTransaction(std::move(mTransaction
));
585 MOZ_ASSERT(mTransaction
.isNothing() && maybeTransaction
.isSome());
587 U2FTransaction
& transaction
= maybeTransaction
.ref();
588 ErrorCode code
= ConvertNSResultToErrorCode(aError
);
590 if (transaction
.HasRegisterCallback()) {
591 RegisterResponse response
;
592 response
.mErrorCode
.Construct(static_cast<uint32_t>(code
));
593 // MOZ_KnownLive because "transaction" lives on the stack.
594 ExecuteCallback(response
, MOZ_KnownLive(transaction
.GetRegisterCallback()));
597 if (transaction
.HasSignCallback()) {
598 SignResponse response
;
599 response
.mErrorCode
.Construct(static_cast<uint32_t>(code
));
600 // MOZ_KnownLive because "transaction" lives on the stack.
601 ExecuteCallback(response
, MOZ_KnownLive(transaction
.GetSignCallback()));
605 void U2F::CancelTransaction(const nsresult
& aError
) {
606 if (!NS_WARN_IF(!mChild
|| mTransaction
.isNothing())) {
607 mChild
->SendRequestCancel(mTransaction
.ref().mId
);
610 RejectTransaction(aError
);
613 void U2F::RequestAborted(const uint64_t& aTransactionId
,
614 const nsresult
& aError
) {
615 MOZ_ASSERT(NS_IsMainThread());
617 if (mTransaction
.isSome() && mTransaction
.ref().mId
== aTransactionId
) {
618 RejectTransaction(aError
);
622 void U2F::HandleVisibilityChange() {
623 if (mTransaction
.isSome()) {
624 mTransaction
.ref().mVisibilityChanged
= true;
629 } // namespace mozilla