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 "nsIEffectiveTLDService.h"
15 #include "nsNetUtil.h"
16 #include "nsURLParsers.h"
18 using namespace mozilla::ipc
;
20 // Forward decl because of nsHTMLDocument.h's complex dependency on /layout/style
21 class nsHTMLDocument
{
23 bool IsRegistrableDomainSuffixOfOrEqualTo(const nsAString
& aHostSuffixString
,
24 const nsACString
& aOrigHost
);
30 static mozilla::LazyLogModule
gU2FLog("u2fmanager");
32 NS_NAMED_LITERAL_STRING(kFinishEnrollment
, "navigator.id.finishEnrollment");
33 NS_NAMED_LITERAL_STRING(kGetAssertion
, "navigator.id.getAssertion");
35 // Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023.
36 NS_NAMED_LITERAL_STRING(kGoogleAccountsAppId1
,
37 "https://www.gstatic.com/securitykey/origins.json");
38 NS_NAMED_LITERAL_STRING(kGoogleAccountsAppId2
,
39 "https://www.gstatic.com/securitykey/a/google.com/origins.json");
41 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(U2F
)
42 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
43 NS_INTERFACE_MAP_ENTRY(nsISupports
)
44 NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener
)
47 NS_IMPL_CYCLE_COLLECTING_ADDREF(U2F
)
48 NS_IMPL_CYCLE_COLLECTING_RELEASE(U2F
)
50 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(U2F
, mParent
)
52 /***********************************************************************
54 **********************************************************************/
57 ConvertNSResultToErrorCode(const nsresult
& aError
)
59 if (aError
== NS_ERROR_DOM_TIMEOUT_ERR
) {
60 return ErrorCode::TIMEOUT
;
62 /* Emitted by U2F{Soft,HID}TokenManager when we really mean ineligible */
63 if (aError
== NS_ERROR_DOM_INVALID_STATE_ERR
) {
64 return ErrorCode::DEVICE_INELIGIBLE
;
66 return ErrorCode::OTHER_ERROR
;
70 AdjustedTimeoutMillis(const Optional
<Nullable
<int32_t>>& opt_aSeconds
)
72 uint32_t adjustedTimeoutMillis
= 30000u;
73 if (opt_aSeconds
.WasPassed() && !opt_aSeconds
.Value().IsNull()) {
74 adjustedTimeoutMillis
= opt_aSeconds
.Value().Value() * 1000u;
75 adjustedTimeoutMillis
= std::max(15000u, adjustedTimeoutMillis
);
76 adjustedTimeoutMillis
= std::min(120000u, adjustedTimeoutMillis
);
78 return adjustedTimeoutMillis
;
82 AssembleClientData(const nsAString
& aOrigin
, const nsAString
& aTyp
,
83 const nsAString
& aChallenge
,
84 /* out */ nsString
& aClientData
)
86 MOZ_ASSERT(NS_IsMainThread());
87 U2FClientData clientDataObject
;
88 clientDataObject
.mTyp
.Construct(aTyp
); // "Typ" from the U2F specification
89 clientDataObject
.mChallenge
.Construct(aChallenge
);
90 clientDataObject
.mOrigin
.Construct(aOrigin
);
92 if (NS_WARN_IF(!clientDataObject
.ToJSON(aClientData
))) {
93 return NS_ERROR_FAILURE
;
100 RegisteredKeysToScopedCredentialList(const nsAString
& aAppId
,
101 const nsTArray
<RegisteredKey
>& aKeys
,
102 nsTArray
<WebAuthnScopedCredential
>& aList
)
104 for (const RegisteredKey
& key
: aKeys
) {
105 // Check for required attributes
106 if (!key
.mVersion
.WasPassed() || !key
.mKeyHandle
.WasPassed() ||
107 key
.mVersion
.Value() != kRequiredU2FVersion
) {
111 // If this key's mAppId doesn't match the invocation, we can't handle it.
112 if (key
.mAppId
.WasPassed() && !key
.mAppId
.Value().Equals(aAppId
)) {
116 CryptoBuffer keyHandle
;
117 nsresult rv
= keyHandle
.FromJwkBase64(key
.mKeyHandle
.Value());
118 if (NS_WARN_IF(NS_FAILED(rv
))) {
122 WebAuthnScopedCredential c
;
124 aList
.AppendElement(c
);
128 /***********************************************************************
129 * U2F JavaScript API Implementation
130 **********************************************************************/
134 MOZ_ASSERT(NS_IsMainThread());
136 if (mTransaction
.isSome()) {
137 RejectTransaction(NS_ERROR_ABORT
);
141 RefPtr
<WebAuthnTransactionChild
> c
;
148 U2F::Init(ErrorResult
& aRv
)
152 nsCOMPtr
<nsIDocument
> doc
= mParent
->GetDoc();
155 aRv
.Throw(NS_ERROR_FAILURE
);
159 nsIPrincipal
* principal
= doc
->NodePrincipal();
160 aRv
= nsContentUtils::GetUTFOrigin(principal
, mOrigin
);
161 if (NS_WARN_IF(aRv
.Failed())) {
165 if (NS_WARN_IF(mOrigin
.IsEmpty())) {
166 aRv
.Throw(NS_ERROR_FAILURE
);
171 /* virtual */ JSObject
*
172 U2F::WrapObject(JSContext
* aCx
, JS::Handle
<JSObject
*> aGivenProto
)
174 return U2F_Binding::Wrap(aCx
, this, aGivenProto
);
177 template<typename T
, typename C
>
179 U2F::ExecuteCallback(T
& aResp
, nsMainThreadPtrHandle
<C
>& aCb
)
181 MOZ_ASSERT(NS_IsMainThread());
184 // Assert that mTransaction was cleared before before we were called to allow
185 // reentrancy from microtask checkpoints.
186 MOZ_ASSERT(mTransaction
.isNothing());
189 aCb
->Call(aResp
, error
);
190 NS_WARNING_ASSERTION(!error
.Failed(), "dom::U2F::Promise callback failed");
191 error
.SuppressException(); // Useful exceptions already emitted
195 U2F::Register(const nsAString
& aAppId
,
196 const Sequence
<RegisterRequest
>& aRegisterRequests
,
197 const Sequence
<RegisteredKey
>& aRegisteredKeys
,
198 U2FRegisterCallback
& aCallback
,
199 const Optional
<Nullable
<int32_t>>& opt_aTimeoutSeconds
,
202 MOZ_ASSERT(NS_IsMainThread());
204 if (mTransaction
.isSome()) {
205 CancelTransaction(NS_ERROR_ABORT
);
208 nsMainThreadPtrHandle
<U2FRegisterCallback
> callback(
209 new nsMainThreadPtrHolder
<U2FRegisterCallback
>("U2F::Register::callback",
212 // Ensure we have a callback.
213 if (NS_WARN_IF(!callback
)) {
217 // Evaluate the AppID
218 nsString
adjustedAppId(aAppId
);
219 if (!EvaluateAppID(mParent
, mOrigin
, U2FOperation::Register
, adjustedAppId
)) {
220 RegisterResponse response
;
221 response
.mErrorCode
.Construct(static_cast<uint32_t>(ErrorCode::BAD_REQUEST
));
222 ExecuteCallback(response
, callback
);
226 nsAutoString clientDataJSON
;
228 // Pick the first valid RegisterRequest; we can only work with one.
229 CryptoBuffer challenge
;
230 for (const RegisterRequest
& req
: aRegisterRequests
) {
231 if (!req
.mChallenge
.WasPassed() || !req
.mVersion
.WasPassed() ||
232 req
.mVersion
.Value() != kRequiredU2FVersion
) {
235 if (!challenge
.Assign(NS_ConvertUTF16toUTF8(req
.mChallenge
.Value()))) {
239 nsresult rv
= AssembleClientData(mOrigin
, kFinishEnrollment
,
240 req
.mChallenge
.Value(), clientDataJSON
);
241 if (NS_WARN_IF(NS_FAILED(rv
))) {
246 // Did we not get a valid RegisterRequest? Abort.
247 if (clientDataJSON
.IsEmpty()) {
248 RegisterResponse response
;
249 response
.mErrorCode
.Construct(static_cast<uint32_t>(ErrorCode::BAD_REQUEST
));
250 ExecuteCallback(response
, callback
);
254 // Build the exclusion list, if any
255 nsTArray
<WebAuthnScopedCredential
> excludeList
;
256 RegisteredKeysToScopedCredentialList(adjustedAppId
, aRegisteredKeys
,
259 if (!MaybeCreateBackgroundActor()) {
260 RegisterResponse response
;
261 response
.mErrorCode
.Construct(static_cast<uint32_t>(ErrorCode::OTHER_ERROR
));
262 ExecuteCallback(response
, callback
);
266 ListenForVisibilityEvents();
268 NS_ConvertUTF16toUTF8
clientData(clientDataJSON
);
269 uint32_t adjustedTimeoutMillis
= AdjustedTimeoutMillis(opt_aTimeoutSeconds
);
271 WebAuthnMakeCredentialInfo
info(mOrigin
,
275 adjustedTimeoutMillis
,
277 null_t() /* no extra info for U2F */);
279 MOZ_ASSERT(mTransaction
.isNothing());
280 mTransaction
= Some(U2FTransaction(AsVariant(callback
)));
281 mChild
->SendRequestRegister(mTransaction
.ref().mId
, info
);
285 U2F::FinishMakeCredential(const uint64_t& aTransactionId
,
286 const WebAuthnMakeCredentialResult
& aResult
)
288 MOZ_ASSERT(NS_IsMainThread());
290 // Check for a valid transaction.
291 if (mTransaction
.isNothing() || mTransaction
.ref().mId
!= aTransactionId
) {
295 if (NS_WARN_IF(!mTransaction
.ref().HasRegisterCallback())) {
296 RejectTransaction(NS_ERROR_ABORT
);
301 if (aResult
.RegistrationData().Length() == 0) {
302 RejectTransaction(NS_ERROR_ABORT
);
306 CryptoBuffer clientDataBuf
;
307 if (NS_WARN_IF(!clientDataBuf
.Assign(aResult
.ClientDataJSON()))) {
308 RejectTransaction(NS_ERROR_ABORT
);
313 if (NS_WARN_IF(!regBuf
.Assign(aResult
.RegistrationData()))) {
314 RejectTransaction(NS_ERROR_ABORT
);
318 nsString clientDataBase64
;
319 nsString registrationDataBase64
;
320 nsresult rvClientData
= clientDataBuf
.ToJwkBase64(clientDataBase64
);
321 nsresult rvRegistrationData
= regBuf
.ToJwkBase64(registrationDataBase64
);
323 if (NS_WARN_IF(NS_FAILED(rvClientData
)) ||
324 NS_WARN_IF(NS_FAILED(rvRegistrationData
))) {
325 RejectTransaction(NS_ERROR_ABORT
);
329 // Assemble a response object to return
330 RegisterResponse response
;
331 response
.mVersion
.Construct(kRequiredU2FVersion
);
332 response
.mClientData
.Construct(clientDataBase64
);
333 response
.mRegistrationData
.Construct(registrationDataBase64
);
334 response
.mErrorCode
.Construct(static_cast<uint32_t>(ErrorCode::OK
));
336 // Keep the callback pointer alive.
337 nsMainThreadPtrHandle
<U2FRegisterCallback
> callback(
338 mTransaction
.ref().GetRegisterCallback());
341 ExecuteCallback(response
, callback
);
345 U2F::Sign(const nsAString
& aAppId
,
346 const nsAString
& aChallenge
,
347 const Sequence
<RegisteredKey
>& aRegisteredKeys
,
348 U2FSignCallback
& aCallback
,
349 const Optional
<Nullable
<int32_t>>& opt_aTimeoutSeconds
,
352 MOZ_ASSERT(NS_IsMainThread());
354 if (mTransaction
.isSome()) {
355 CancelTransaction(NS_ERROR_ABORT
);
358 nsMainThreadPtrHandle
<U2FSignCallback
> callback(
359 new nsMainThreadPtrHolder
<U2FSignCallback
>("U2F::Sign::callback",
362 // Ensure we have a callback.
363 if (NS_WARN_IF(!callback
)) {
367 // Evaluate the AppID
368 nsString
adjustedAppId(aAppId
);
369 if (!EvaluateAppID(mParent
, mOrigin
, U2FOperation::Sign
, adjustedAppId
)) {
370 SignResponse response
;
371 response
.mErrorCode
.Construct(static_cast<uint32_t>(ErrorCode::BAD_REQUEST
));
372 ExecuteCallback(response
, callback
);
376 // Produce the AppParam from the current AppID
377 nsCString cAppId
= NS_ConvertUTF16toUTF8(adjustedAppId
);
379 nsAutoString clientDataJSON
;
380 nsresult rv
= AssembleClientData(mOrigin
, kGetAssertion
, aChallenge
,
382 if (NS_WARN_IF(NS_FAILED(rv
))) {
383 SignResponse response
;
384 response
.mErrorCode
.Construct(static_cast<uint32_t>(ErrorCode::BAD_REQUEST
));
385 ExecuteCallback(response
, callback
);
389 CryptoBuffer challenge
;
390 if (!challenge
.Assign(NS_ConvertUTF16toUTF8(aChallenge
))) {
391 SignResponse response
;
392 response
.mErrorCode
.Construct(static_cast<uint32_t>(ErrorCode::OTHER_ERROR
));
393 ExecuteCallback(response
, callback
);
397 // Build the key list, if any
398 nsTArray
<WebAuthnScopedCredential
> permittedList
;
399 RegisteredKeysToScopedCredentialList(adjustedAppId
, aRegisteredKeys
,
402 if (!MaybeCreateBackgroundActor()) {
403 SignResponse response
;
404 response
.mErrorCode
.Construct(static_cast<uint32_t>(ErrorCode::OTHER_ERROR
));
405 ExecuteCallback(response
, callback
);
409 ListenForVisibilityEvents();
411 // Always blank for U2F
412 nsTArray
<WebAuthnExtension
> extensions
;
414 NS_ConvertUTF16toUTF8
clientData(clientDataJSON
);
415 uint32_t adjustedTimeoutMillis
= AdjustedTimeoutMillis(opt_aTimeoutSeconds
);
417 WebAuthnGetAssertionInfo
info(mOrigin
,
421 adjustedTimeoutMillis
,
423 null_t() /* no extra info for U2F */);
425 MOZ_ASSERT(mTransaction
.isNothing());
426 mTransaction
= Some(U2FTransaction(AsVariant(callback
)));
427 mChild
->SendRequestSign(mTransaction
.ref().mId
, info
);
431 U2F::FinishGetAssertion(const uint64_t& aTransactionId
,
432 const WebAuthnGetAssertionResult
& aResult
)
434 MOZ_ASSERT(NS_IsMainThread());
436 // Check for a valid transaction.
437 if (mTransaction
.isNothing() || mTransaction
.ref().mId
!= aTransactionId
) {
441 if (NS_WARN_IF(!mTransaction
.ref().HasSignCallback())) {
442 RejectTransaction(NS_ERROR_ABORT
);
447 if (aResult
.SignatureData().Length() == 0) {
448 RejectTransaction(NS_ERROR_ABORT
);
452 CryptoBuffer clientDataBuf
;
453 if (NS_WARN_IF(!clientDataBuf
.Assign(aResult
.ClientDataJSON()))) {
454 RejectTransaction(NS_ERROR_ABORT
);
458 CryptoBuffer credBuf
;
459 if (NS_WARN_IF(!credBuf
.Assign(aResult
.KeyHandle()))) {
460 RejectTransaction(NS_ERROR_ABORT
);
465 if (NS_WARN_IF(!sigBuf
.Assign(aResult
.SignatureData()))) {
466 RejectTransaction(NS_ERROR_ABORT
);
470 // Assemble a response object to return
471 nsString clientDataBase64
;
472 nsString signatureDataBase64
;
473 nsString keyHandleBase64
;
474 nsresult rvClientData
= clientDataBuf
.ToJwkBase64(clientDataBase64
);
475 nsresult rvSignatureData
= sigBuf
.ToJwkBase64(signatureDataBase64
);
476 nsresult rvKeyHandle
= credBuf
.ToJwkBase64(keyHandleBase64
);
477 if (NS_WARN_IF(NS_FAILED(rvClientData
)) ||
478 NS_WARN_IF(NS_FAILED(rvSignatureData
) ||
479 NS_WARN_IF(NS_FAILED(rvKeyHandle
)))) {
480 RejectTransaction(NS_ERROR_ABORT
);
484 SignResponse response
;
485 response
.mKeyHandle
.Construct(keyHandleBase64
);
486 response
.mClientData
.Construct(clientDataBase64
);
487 response
.mSignatureData
.Construct(signatureDataBase64
);
488 response
.mErrorCode
.Construct(static_cast<uint32_t>(ErrorCode::OK
));
490 // Keep the callback pointer alive.
491 nsMainThreadPtrHandle
<U2FSignCallback
> callback(
492 mTransaction
.ref().GetSignCallback());
495 ExecuteCallback(response
, callback
);
499 U2F::ClearTransaction()
501 if (!NS_WARN_IF(mTransaction
.isNothing())) {
502 StopListeningForVisibilityEvents();
505 mTransaction
.reset();
509 U2F::RejectTransaction(const nsresult
& aError
)
511 if (NS_WARN_IF(mTransaction
.isNothing())) {
515 StopListeningForVisibilityEvents();
517 // Clear out mTransaction before calling ExecuteCallback() below to allow
518 // reentrancy from microtask checkpoints.
519 Maybe
<U2FTransaction
> maybeTransaction(std::move(mTransaction
));
520 MOZ_ASSERT(mTransaction
.isNothing() && maybeTransaction
.isSome());
522 U2FTransaction
& transaction
= maybeTransaction
.ref();
523 ErrorCode code
= ConvertNSResultToErrorCode(aError
);
525 if (transaction
.HasRegisterCallback()) {
526 RegisterResponse response
;
527 response
.mErrorCode
.Construct(static_cast<uint32_t>(code
));
528 ExecuteCallback(response
, transaction
.GetRegisterCallback());
531 if (transaction
.HasSignCallback()) {
532 SignResponse response
;
533 response
.mErrorCode
.Construct(static_cast<uint32_t>(code
));
534 ExecuteCallback(response
, transaction
.GetSignCallback());
539 U2F::CancelTransaction(const nsresult
& aError
)
541 if (!NS_WARN_IF(!mChild
|| mTransaction
.isNothing())) {
542 mChild
->SendRequestCancel(mTransaction
.ref().mId
);
545 RejectTransaction(aError
);
549 U2F::RequestAborted(const uint64_t& aTransactionId
, const nsresult
& aError
)
551 MOZ_ASSERT(NS_IsMainThread());
553 if (mTransaction
.isSome() && mTransaction
.ref().mId
== aTransactionId
) {
554 RejectTransaction(aError
);
559 } // namespace mozilla