WebAuthnHandler.cpp (36115B)
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 "mozilla/dom/WebAuthnHandler.h" 8 9 #include "WebAuthnCoseIdentifiers.h" 10 #include "WebAuthnEnumStrings.h" 11 #include "WebAuthnTransportIdentifiers.h" 12 #include "hasht.h" 13 #include "mozilla/Base64.h" 14 #include "mozilla/BasePrincipal.h" 15 #include "mozilla/BounceTrackingProtection.h" 16 #include "mozilla/dom/AuthenticatorAssertionResponse.h" 17 #include "mozilla/dom/AuthenticatorAttestationResponse.h" 18 #include "mozilla/dom/PWebAuthnTransaction.h" 19 #include "mozilla/dom/PublicKeyCredential.h" 20 #include "mozilla/dom/WebAuthnTransactionChild.h" 21 #include "mozilla/dom/WebAuthnUtil.h" 22 #include "mozilla/dom/WindowGlobalChild.h" 23 #include "mozilla/glean/DomWebauthnMetrics.h" 24 #include "nsHTMLDocument.h" 25 #include "nsIURIMutator.h" 26 #include "nsThreadUtils.h" 27 28 #ifdef XP_WIN 29 # include "WinWebAuthnService.h" 30 #endif 31 32 using namespace mozilla::ipc; 33 34 namespace mozilla::dom { 35 36 /*********************************************************************** 37 * Statics 38 **********************************************************************/ 39 40 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebAuthnHandler) 41 NS_INTERFACE_MAP_ENTRY(nsISupports) 42 NS_INTERFACE_MAP_END 43 44 NS_IMPL_CYCLE_COLLECTION(WebAuthnHandler, mWindow, mTransaction) 45 46 NS_IMPL_CYCLE_COLLECTING_ADDREF(WebAuthnHandler) 47 NS_IMPL_CYCLE_COLLECTING_RELEASE(WebAuthnHandler) 48 49 /*********************************************************************** 50 * Utility Functions 51 **********************************************************************/ 52 53 static uint8_t SerializeTransports( 54 const mozilla::dom::Sequence<nsString>& aTransports) { 55 uint8_t transports = 0; 56 57 // We ignore unknown transports for forward-compatibility, but this 58 // needs to be reviewed if values are added to the 59 // AuthenticatorTransport enum. 60 static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3); 61 for (const nsAString& str : aTransports) { 62 if (str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_USB)) { 63 transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_USB; 64 } else if (str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_NFC)) { 65 transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_NFC; 66 } else if (str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_BLE)) { 67 transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_BLE; 68 } else if (str.EqualsLiteral( 69 MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_INTERNAL)) { 70 transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_INTERNAL; 71 } else if (str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_HYBRID)) { 72 transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_HYBRID; 73 } 74 } 75 return transports; 76 } 77 78 /*********************************************************************** 79 * WebAuthnHandler Implementation 80 **********************************************************************/ 81 82 WebAuthnHandler::~WebAuthnHandler() { 83 MOZ_ASSERT(NS_IsMainThread()); 84 if (mActor) { 85 if (mTransaction.isSome()) { 86 CancelTransaction(NS_ERROR_DOM_ABORT_ERR); 87 } 88 mActor->SetHandler(nullptr); 89 } 90 } 91 92 bool WebAuthnHandler::MaybeCreateActor() { 93 MOZ_ASSERT(NS_IsMainThread()); 94 95 if (mActor) { 96 return true; 97 } 98 99 RefPtr<WebAuthnTransactionChild> actor = new WebAuthnTransactionChild(); 100 101 WindowGlobalChild* windowGlobalChild = mWindow->GetWindowGlobalChild(); 102 if (!windowGlobalChild || 103 !windowGlobalChild->SendPWebAuthnTransactionConstructor(actor)) { 104 return false; 105 } 106 107 mActor = actor; 108 mActor->SetHandler(this); 109 110 return true; 111 } 112 113 void WebAuthnHandler::ActorDestroyed() { 114 MOZ_ASSERT(NS_IsMainThread()); 115 mActor = nullptr; 116 } 117 118 already_AddRefed<Promise> WebAuthnHandler::MakeCredential( 119 const PublicKeyCredentialCreationOptions& aOptions, 120 const Optional<OwningNonNull<AbortSignal>>& aSignal, ErrorResult& aError) { 121 MOZ_ASSERT(NS_IsMainThread()); 122 123 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWindow); 124 125 RefPtr<Promise> promise = Promise::Create(global, aError); 126 if (aError.Failed()) { 127 return nullptr; 128 } 129 130 if (mTransaction.isSome()) { 131 // abort the old transaction and take over control from here. 132 CancelTransaction(NS_ERROR_DOM_ABORT_ERR); 133 } 134 135 if (!MaybeCreateActor()) { 136 promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR); 137 return promise.forget(); 138 } 139 140 nsCOMPtr<Document> doc = mWindow->GetDoc(); 141 if (!IsWebAuthnAllowedInDocument(doc)) { 142 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); 143 return promise.forget(); 144 } 145 146 nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); 147 148 nsCString rpId; 149 if (aOptions.mRp.mId.WasPassed()) { 150 rpId = NS_ConvertUTF16toUTF8(aOptions.mRp.mId.Value()); 151 } else { 152 nsresult rv = DefaultRpId(principal, rpId); 153 if (NS_FAILED(rv)) { 154 promise->MaybeReject(NS_ERROR_FAILURE); 155 return promise.forget(); 156 } 157 } 158 if (!IsValidRpId(principal, rpId)) { 159 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); 160 return promise.forget(); 161 } 162 163 // Enforce 5.4.3 User Account Parameters for Credential Generation 164 // When we add UX, we'll want to do more with this value, but for now 165 // we just have to verify its correctness. 166 CryptoBuffer userId; 167 userId.Assign(aOptions.mUser.mId); 168 if (userId.Length() > 64) { 169 promise->MaybeRejectWithTypeError("user.id is too long"); 170 return promise.forget(); 171 } 172 173 // If timeoutSeconds was specified, check if its value lies within a 174 // reasonable range as defined by the platform and if not, correct it to the 175 // closest value lying within that range. 176 177 uint32_t adjustedTimeout = 30000; 178 if (aOptions.mTimeout.WasPassed()) { 179 adjustedTimeout = aOptions.mTimeout.Value(); 180 adjustedTimeout = std::max(15000u, adjustedTimeout); 181 adjustedTimeout = std::min(120000u, adjustedTimeout); 182 } 183 184 // <https://w3c.github.io/webauthn/#sctn-appid-extension> 185 if (aOptions.mExtensions.mAppid.WasPassed()) { 186 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); 187 return promise.forget(); 188 } 189 190 // Process each element of mPubKeyCredParams using the following steps, to 191 // produce a new sequence of coseAlgos. 192 nsTArray<CoseAlg> coseAlgos; 193 // If pubKeyCredParams is empty, append ES256 and RS256 194 if (aOptions.mPubKeyCredParams.IsEmpty()) { 195 coseAlgos.AppendElement(static_cast<long>(CoseAlgorithmIdentifier::ES256)); 196 coseAlgos.AppendElement(static_cast<long>(CoseAlgorithmIdentifier::RS256)); 197 } else { 198 for (size_t a = 0; a < aOptions.mPubKeyCredParams.Length(); ++a) { 199 // If current.type does not contain a PublicKeyCredentialType 200 // supported by this implementation, then stop processing current and move 201 // on to the next element in mPubKeyCredParams. 202 if (!aOptions.mPubKeyCredParams[a].mType.EqualsLiteral( 203 MOZ_WEBAUTHN_PUBLIC_KEY_CREDENTIAL_TYPE_PUBLIC_KEY)) { 204 continue; 205 } 206 207 coseAlgos.AppendElement(aOptions.mPubKeyCredParams[a].mAlg); 208 } 209 } 210 211 // If there are algorithms specified, but none are Public_key algorithms, 212 // reject the promise. 213 if (coseAlgos.IsEmpty() && !aOptions.mPubKeyCredParams.IsEmpty()) { 214 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); 215 return promise.forget(); 216 } 217 218 // If excludeList is undefined, set it to the empty list. 219 // 220 // If extensions was specified, process any extensions supported by this 221 // client platform, to produce the extension data that needs to be sent to the 222 // authenticator. If an error is encountered while processing an extension, 223 // skip that extension and do not produce any extension data for it. Call the 224 // result of this processing clientExtensions. 225 // 226 // Currently no extensions are supported 227 228 CryptoBuffer challenge; 229 if (!challenge.Assign(aOptions.mChallenge)) { 230 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); 231 return promise.forget(); 232 } 233 234 nsTArray<WebAuthnScopedCredential> excludeList; 235 for (const auto& s : aOptions.mExcludeCredentials) { 236 WebAuthnScopedCredential c; 237 CryptoBuffer cb; 238 cb.Assign(s.mId); 239 c.id() = cb; 240 if (s.mTransports.WasPassed()) { 241 c.transports() = SerializeTransports(s.mTransports.Value()); 242 } 243 excludeList.AppendElement(c); 244 } 245 246 // TODO: Add extension list building 247 nsTArray<WebAuthnExtension> extensions; 248 249 // <https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#sctn-hmac-secret-extension> 250 if (aOptions.mExtensions.mHmacCreateSecret.WasPassed()) { 251 bool hmacCreateSecret = aOptions.mExtensions.mHmacCreateSecret.Value(); 252 if (hmacCreateSecret) { 253 extensions.AppendElement(WebAuthnExtensionHmacSecret(hmacCreateSecret)); 254 } 255 } 256 257 if (aOptions.mExtensions.mCredentialProtectionPolicy.WasPassed()) { 258 bool enforceCredProtect = false; 259 if (aOptions.mExtensions.mEnforceCredentialProtectionPolicy.WasPassed()) { 260 enforceCredProtect = 261 aOptions.mExtensions.mEnforceCredentialProtectionPolicy.Value(); 262 } 263 extensions.AppendElement(WebAuthnExtensionCredProtect( 264 aOptions.mExtensions.mCredentialProtectionPolicy.Value(), 265 enforceCredProtect)); 266 } 267 268 if (aOptions.mExtensions.mCredProps.WasPassed()) { 269 bool credProps = aOptions.mExtensions.mCredProps.Value(); 270 if (credProps) { 271 extensions.AppendElement(WebAuthnExtensionCredProps(credProps)); 272 } 273 } 274 275 if (aOptions.mExtensions.mMinPinLength.WasPassed()) { 276 bool minPinLength = aOptions.mExtensions.mMinPinLength.Value(); 277 if (minPinLength) { 278 extensions.AppendElement(WebAuthnExtensionMinPinLength(minPinLength)); 279 } 280 } 281 282 // <https://w3c.github.io/webauthn/#sctn-large-blob-extension> 283 if (aOptions.mExtensions.mLargeBlob.WasPassed()) { 284 if (aOptions.mExtensions.mLargeBlob.Value().mRead.WasPassed() || 285 aOptions.mExtensions.mLargeBlob.Value().mWrite.WasPassed()) { 286 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); 287 return promise.forget(); 288 } 289 Maybe<bool> supportRequired; 290 const Optional<nsString>& largeBlobSupport = 291 aOptions.mExtensions.mLargeBlob.Value().mSupport; 292 if (largeBlobSupport.WasPassed()) { 293 supportRequired.emplace(largeBlobSupport.Value().Equals(u"required"_ns)); 294 } 295 nsTArray<uint8_t> write; // unused 296 extensions.AppendElement( 297 WebAuthnExtensionLargeBlob(supportRequired, write)); 298 } 299 300 // <https://w3c.github.io/webauthn/#prf-extension> 301 if (aOptions.mExtensions.mPrf.WasPassed()) { 302 const AuthenticationExtensionsPRFInputs& prf = 303 aOptions.mExtensions.mPrf.Value(); 304 305 Maybe<WebAuthnExtensionPrfValues> eval = Nothing(); 306 if (prf.mEval.WasPassed()) { 307 CryptoBuffer first; 308 first.Assign(prf.mEval.Value().mFirst); 309 const bool secondMaybe = prf.mEval.Value().mSecond.WasPassed(); 310 CryptoBuffer second; 311 if (secondMaybe) { 312 second.Assign(prf.mEval.Value().mSecond.Value()); 313 } 314 eval = Some(WebAuthnExtensionPrfValues(first, secondMaybe, second)); 315 } 316 317 const bool evalByCredentialMaybe = prf.mEvalByCredential.WasPassed(); 318 nsTArray<WebAuthnExtensionPrfEvalByCredentialEntry> evalByCredential; 319 if (evalByCredentialMaybe) { 320 // evalByCredential is only allowed in GetAssertion. 321 // https://w3c.github.io/webauthn/#prf-extension 322 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); 323 return promise.forget(); 324 } 325 326 extensions.AppendElement( 327 WebAuthnExtensionPrf(eval, evalByCredentialMaybe, evalByCredential)); 328 } 329 330 const auto& selection = aOptions.mAuthenticatorSelection; 331 const auto& attachment = selection.mAuthenticatorAttachment; 332 const nsString& attestation = aOptions.mAttestation; 333 334 // Attachment 335 Maybe<nsString> authenticatorAttachment; 336 if (attachment.WasPassed()) { 337 authenticatorAttachment.emplace(attachment.Value()); 338 } 339 340 // The residentKey field was added in WebAuthn level 2. It takes precedent 341 // over the requireResidentKey field if and only if it is present and it is a 342 // member of the ResidentKeyRequirement enum. 343 static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 3); 344 bool useResidentKeyValue = 345 selection.mResidentKey.WasPassed() && 346 (selection.mResidentKey.Value().EqualsLiteral( 347 MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_REQUIRED) || 348 selection.mResidentKey.Value().EqualsLiteral( 349 MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_PREFERRED) || 350 selection.mResidentKey.Value().EqualsLiteral( 351 MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_DISCOURAGED)); 352 353 nsString residentKey; 354 if (useResidentKeyValue) { 355 residentKey = selection.mResidentKey.Value(); 356 } else { 357 // "If no value is given then the effective value is required if 358 // requireResidentKey is true or discouraged if it is false or absent." 359 if (selection.mRequireResidentKey) { 360 residentKey.AssignLiteral(MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_REQUIRED); 361 } else { 362 residentKey.AssignLiteral( 363 MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_DISCOURAGED); 364 } 365 } 366 367 // Create and forward authenticator selection criteria. 368 WebAuthnAuthenticatorSelection authSelection( 369 residentKey, selection.mUserVerification, authenticatorAttachment); 370 371 WebAuthnMakeCredentialRpInfo rpInfo(aOptions.mRp.mName); 372 373 WebAuthnMakeCredentialUserInfo userInfo(userId, aOptions.mUser.mName, 374 aOptions.mUser.mDisplayName); 375 376 // Abort the request if aborted flag is already set. 377 if (aSignal.WasPassed() && aSignal.Value().Aborted()) { 378 AutoJSAPI jsapi; 379 if (!jsapi.Init(global)) { 380 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); 381 return promise.forget(); 382 } 383 JSContext* cx = jsapi.cx(); 384 JS::Rooted<JS::Value> reason(cx); 385 aSignal.Value().GetReason(cx, &reason); 386 promise->MaybeReject(reason); 387 return promise.forget(); 388 } 389 390 WebAuthnMakeCredentialInfo info(rpId, challenge, adjustedTimeout, excludeList, 391 rpInfo, userInfo, coseAlgos, extensions, 392 authSelection, attestation, aOptions.mHints); 393 394 // Set up the transaction state. Fallible operations should not be performed 395 // below this line, as we must not leave the transaction state partially 396 // initialized. Once the transaction state is initialized the only valid ways 397 // to end the transaction are CancelTransaction, RejectTransaction, and 398 // FinishMakeCredential. 399 AbortSignal* signal = nullptr; 400 if (aSignal.WasPassed()) { 401 signal = &aSignal.Value(); 402 Follow(signal); 403 } 404 405 MOZ_ASSERT(mTransaction.isNothing()); 406 mTransaction = 407 Some(WebAuthnTransaction(promise, WebAuthnTransactionType::Create)); 408 mActor->SendRequestRegister(info) 409 ->Then( 410 GetCurrentSerialEventTarget(), __func__, 411 [self = RefPtr{this}]( 412 const PWebAuthnTransactionChild::RequestRegisterPromise:: 413 ResolveOrRejectValue& aValue) { 414 self->mTransaction.ref().mRegisterHolder.Complete(); 415 if (aValue.IsResolve() && aValue.ResolveValue().type() == 416 WebAuthnMakeCredentialResponse::Type:: 417 TWebAuthnMakeCredentialResult) { 418 self->FinishMakeCredential(aValue.ResolveValue()); 419 } else if (aValue.IsResolve()) { 420 self->RejectTransaction(aValue.ResolveValue()); 421 } else { 422 self->RejectTransaction(NS_ERROR_DOM_NOT_ALLOWED_ERR); 423 } 424 }) 425 ->Track(mTransaction.ref().mRegisterHolder); 426 427 return promise.forget(); 428 } 429 430 const size_t MAX_ALLOWED_CREDENTIALS = 20; 431 432 already_AddRefed<Promise> WebAuthnHandler::GetAssertion( 433 const PublicKeyCredentialRequestOptions& aOptions, 434 const bool aConditionallyMediated, 435 const Optional<OwningNonNull<AbortSignal>>& aSignal, ErrorResult& aError) { 436 MOZ_ASSERT(NS_IsMainThread()); 437 438 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWindow); 439 440 RefPtr<Promise> promise = Promise::Create(global, aError); 441 if (aError.Failed()) { 442 return nullptr; 443 } 444 445 if (mTransaction.isSome()) { 446 // abort the old transaction and take over control from here. 447 CancelTransaction(NS_ERROR_DOM_ABORT_ERR); 448 } 449 450 if (!MaybeCreateActor()) { 451 promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR); 452 return promise.forget(); 453 } 454 455 nsCOMPtr<Document> doc = mWindow->GetDoc(); 456 if (!IsWebAuthnAllowedInDocument(doc)) { 457 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); 458 return promise.forget(); 459 } 460 461 nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); 462 463 nsCString rpId; 464 if (aOptions.mRpId.WasPassed()) { 465 rpId = NS_ConvertUTF16toUTF8(aOptions.mRpId.Value()); 466 } else { 467 nsresult rv = DefaultRpId(principal, rpId); 468 if (NS_FAILED(rv)) { 469 promise->MaybeReject(NS_ERROR_FAILURE); 470 return promise.forget(); 471 } 472 } 473 if (!IsValidRpId(principal, rpId)) { 474 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); 475 return promise.forget(); 476 } 477 478 // If timeoutSeconds was specified, check if its value lies within a 479 // reasonable range as defined by the platform and if not, correct it to the 480 // closest value lying within that range. 481 482 uint32_t adjustedTimeout = 30000; 483 if (aOptions.mTimeout.WasPassed()) { 484 adjustedTimeout = aOptions.mTimeout.Value(); 485 adjustedTimeout = std::max(15000u, adjustedTimeout); 486 adjustedTimeout = std::min(120000u, adjustedTimeout); 487 } 488 489 // Abort the request if the allowCredentials set is too large 490 if (aOptions.mAllowCredentials.Length() > MAX_ALLOWED_CREDENTIALS) { 491 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); 492 return promise.forget(); 493 } 494 495 CryptoBuffer challenge; 496 if (!challenge.Assign(aOptions.mChallenge)) { 497 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); 498 return promise.forget(); 499 } 500 501 nsTArray<WebAuthnScopedCredential> allowList; 502 for (const auto& s : aOptions.mAllowCredentials) { 503 if (s.mType.EqualsLiteral( 504 MOZ_WEBAUTHN_PUBLIC_KEY_CREDENTIAL_TYPE_PUBLIC_KEY)) { 505 WebAuthnScopedCredential c; 506 CryptoBuffer cb; 507 cb.Assign(s.mId); 508 c.id() = cb; 509 if (s.mTransports.WasPassed()) { 510 c.transports() = SerializeTransports(s.mTransports.Value()); 511 } 512 allowList.AppendElement(c); 513 } 514 } 515 if (allowList.Length() == 0 && aOptions.mAllowCredentials.Length() != 0) { 516 promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR); 517 return promise.forget(); 518 } 519 520 // If extensions were specified, process any extensions supported by this 521 // client platform, to produce the extension data that needs to be sent to the 522 // authenticator. If an error is encountered while processing an extension, 523 // skip that extension and do not produce any extension data for it. Call the 524 // result of this processing clientExtensions. 525 nsTArray<WebAuthnExtension> extensions; 526 527 // credProps is only supported in MakeCredentials 528 if (aOptions.mExtensions.mCredProps.WasPassed()) { 529 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); 530 return promise.forget(); 531 } 532 533 // minPinLength is only supported in MakeCredentials 534 if (aOptions.mExtensions.mMinPinLength.WasPassed()) { 535 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); 536 return promise.forget(); 537 } 538 539 // <https://w3c.github.io/webauthn/#sctn-appid-extension> 540 Maybe<nsCString> maybeAppId; 541 if (aOptions.mExtensions.mAppid.WasPassed()) { 542 nsCString appId(NS_ConvertUTF16toUTF8(aOptions.mExtensions.mAppid.Value())); 543 544 // Step 2 of Algorithm 3.1.2 of 545 // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-appid-and-facets-v2.0-id-20180227.html#determining-if-a-caller-s-facetid-is-authorized-for-an-appid 546 if (appId.IsEmpty() || appId.EqualsLiteral("null")) { 547 auto* basePrin = BasePrincipal::Cast(principal); 548 nsresult rv = basePrin->GetWebExposedOriginSerialization(appId); 549 if (NS_FAILED(rv)) { 550 promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); 551 return promise.forget(); 552 } 553 } 554 555 maybeAppId.emplace(std::move(appId)); 556 } 557 558 // <https://w3c.github.io/webauthn/#sctn-large-blob-extension> 559 if (aOptions.mExtensions.mLargeBlob.WasPassed()) { 560 const AuthenticationExtensionsLargeBlobInputs& extLargeBlob = 561 aOptions.mExtensions.mLargeBlob.Value(); 562 if (extLargeBlob.mSupport.WasPassed() || 563 (extLargeBlob.mRead.WasPassed() && extLargeBlob.mWrite.WasPassed()) || 564 (extLargeBlob.mWrite.WasPassed() && 565 aOptions.mAllowCredentials.Length() != 1)) { 566 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); 567 return promise.forget(); 568 } 569 Maybe<bool> read = Nothing(); 570 if (extLargeBlob.mRead.WasPassed() && extLargeBlob.mRead.Value()) { 571 read.emplace(true); 572 } 573 574 CryptoBuffer write; 575 if (extLargeBlob.mWrite.WasPassed()) { 576 read.emplace(false); 577 write.Assign(extLargeBlob.mWrite.Value()); 578 } 579 extensions.AppendElement(WebAuthnExtensionLargeBlob(read, write)); 580 } 581 582 // <https://w3c.github.io/webauthn/#prf-extension> 583 if (aOptions.mExtensions.mPrf.WasPassed()) { 584 const AuthenticationExtensionsPRFInputs& prf = 585 aOptions.mExtensions.mPrf.Value(); 586 587 Maybe<WebAuthnExtensionPrfValues> eval = Nothing(); 588 if (prf.mEval.WasPassed()) { 589 CryptoBuffer first; 590 first.Assign(prf.mEval.Value().mFirst); 591 const bool secondMaybe = prf.mEval.Value().mSecond.WasPassed(); 592 CryptoBuffer second; 593 if (secondMaybe) { 594 second.Assign(prf.mEval.Value().mSecond.Value()); 595 } 596 eval = Some(WebAuthnExtensionPrfValues(first, secondMaybe, second)); 597 } 598 599 const bool evalByCredentialMaybe = prf.mEvalByCredential.WasPassed(); 600 nsTArray<WebAuthnExtensionPrfEvalByCredentialEntry> evalByCredential; 601 if (evalByCredentialMaybe) { 602 if (allowList.Length() == 0) { 603 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); 604 return promise.forget(); 605 } 606 607 for (const auto& entry : prf.mEvalByCredential.Value().Entries()) { 608 FallibleTArray<uint8_t> evalByCredentialEntryId; 609 nsresult rv = Base64URLDecode(NS_ConvertUTF16toUTF8(entry.mKey), 610 Base64URLDecodePaddingPolicy::Ignore, 611 evalByCredentialEntryId); 612 if (NS_FAILED(rv)) { 613 promise->MaybeReject(NS_ERROR_DOM_SYNTAX_ERR); 614 return promise.forget(); 615 } 616 617 bool foundMatchingAllowListEntry = false; 618 for (const auto& cred : allowList) { 619 if (evalByCredentialEntryId == cred.id()) { 620 foundMatchingAllowListEntry = true; 621 } 622 } 623 if (!foundMatchingAllowListEntry) { 624 promise->MaybeReject(NS_ERROR_DOM_SYNTAX_ERR); 625 return promise.forget(); 626 } 627 628 CryptoBuffer first; 629 first.Assign(entry.mValue.mFirst); 630 const bool secondMaybe = entry.mValue.mSecond.WasPassed(); 631 CryptoBuffer second; 632 if (secondMaybe) { 633 second.Assign(entry.mValue.mSecond.Value()); 634 } 635 evalByCredential.AppendElement( 636 WebAuthnExtensionPrfEvalByCredentialEntry( 637 evalByCredentialEntryId, 638 WebAuthnExtensionPrfValues(first, secondMaybe, second))); 639 } 640 } 641 642 extensions.AppendElement( 643 WebAuthnExtensionPrf(eval, evalByCredentialMaybe, evalByCredential)); 644 } 645 646 // Abort the request if aborted flag is already set. 647 if (aSignal.WasPassed() && aSignal.Value().Aborted()) { 648 AutoJSAPI jsapi; 649 if (!jsapi.Init(global)) { 650 promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); 651 return promise.forget(); 652 } 653 JSContext* cx = jsapi.cx(); 654 JS::Rooted<JS::Value> reason(cx); 655 aSignal.Value().GetReason(cx, &reason); 656 promise->MaybeReject(reason); 657 return promise.forget(); 658 } 659 660 WebAuthnGetAssertionInfo info( 661 rpId, maybeAppId, challenge, adjustedTimeout, allowList, extensions, 662 aOptions.mUserVerification, aConditionallyMediated, aOptions.mHints); 663 664 // Set up the transaction state. Fallible operations should not be performed 665 // below this line, as we must not leave the transaction state partially 666 // initialized. Once the transaction state is initialized the only valid ways 667 // to end the transaction are CancelTransaction, RejectTransaction, and 668 // FinishGetAssertion. 669 AbortSignal* signal = nullptr; 670 if (aSignal.WasPassed()) { 671 signal = &aSignal.Value(); 672 Follow(signal); 673 } 674 675 MOZ_ASSERT(mTransaction.isNothing()); 676 mTransaction = 677 Some(WebAuthnTransaction(promise, WebAuthnTransactionType::Get)); 678 mActor->SendRequestSign(info) 679 ->Then( 680 GetCurrentSerialEventTarget(), __func__, 681 [self = RefPtr{this}]( 682 const PWebAuthnTransactionChild::RequestSignPromise:: 683 ResolveOrRejectValue& aValue) { 684 self->mTransaction.ref().mSignHolder.Complete(); 685 if (aValue.IsResolve() && aValue.ResolveValue().type() == 686 WebAuthnGetAssertionResponse::Type:: 687 TWebAuthnGetAssertionResult) { 688 self->FinishGetAssertion(aValue.ResolveValue()); 689 } else if (aValue.IsResolve()) { 690 self->RejectTransaction(aValue.ResolveValue()); 691 } else { 692 self->RejectTransaction(NS_ERROR_DOM_NOT_ALLOWED_ERR); 693 } 694 }) 695 ->Track(mTransaction.ref().mSignHolder); 696 697 return promise.forget(); 698 } 699 700 already_AddRefed<Promise> WebAuthnHandler::Store(const Credential& aCredential, 701 ErrorResult& aError) { 702 MOZ_ASSERT(NS_IsMainThread()); 703 704 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWindow); 705 706 RefPtr<Promise> promise = Promise::Create(global, aError); 707 if (aError.Failed()) { 708 return nullptr; 709 } 710 711 if (mTransaction.isSome()) { 712 // abort the old transaction and take over control from here. 713 CancelTransaction(NS_ERROR_DOM_ABORT_ERR); 714 } 715 716 promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR); 717 return promise.forget(); 718 } 719 720 already_AddRefed<Promise> WebAuthnHandler::IsUVPAA(GlobalObject& aGlobal, 721 ErrorResult& aError) { 722 RefPtr<Promise> promise = 723 Promise::Create(xpc::CurrentNativeGlobal(aGlobal.Context()), aError); 724 if (aError.Failed()) { 725 return nullptr; 726 } 727 728 if (!MaybeCreateActor()) { 729 promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR); 730 return promise.forget(); 731 } 732 733 mActor->SendRequestIsUVPAA()->Then( 734 GetCurrentSerialEventTarget(), __func__, 735 [promise](const PWebAuthnTransactionChild::RequestIsUVPAAPromise:: 736 ResolveOrRejectValue& aValue) { 737 if (aValue.IsResolve()) { 738 promise->MaybeResolve(aValue.ResolveValue()); 739 } else { 740 promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR); 741 } 742 }); 743 return promise.forget(); 744 } 745 746 void WebAuthnHandler::FinishMakeCredential( 747 const WebAuthnMakeCredentialResult& aResult) { 748 MOZ_ASSERT(NS_IsMainThread()); 749 MOZ_ASSERT(mTransaction.isSome()); 750 751 nsAutoCString keyHandleBase64Url; 752 nsresult rv = Base64URLEncode( 753 aResult.KeyHandle().Length(), aResult.KeyHandle().Elements(), 754 Base64URLEncodePaddingPolicy::Omit, keyHandleBase64Url); 755 if (NS_WARN_IF(NS_FAILED(rv))) { 756 RejectTransaction(rv); 757 return; 758 } 759 760 // Create a new PublicKeyCredential object and populate its fields with the 761 // values returned from the authenticator as well as the clientDataJSON 762 // computed earlier. 763 RefPtr<AuthenticatorAttestationResponse> attestation = 764 new AuthenticatorAttestationResponse(mWindow); 765 attestation->SetClientDataJSON(aResult.ClientDataJSON()); 766 attestation->SetAttestationObject(aResult.AttestationObject()); 767 attestation->SetTransports(aResult.Transports()); 768 769 RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mWindow); 770 credential->SetId(NS_ConvertASCIItoUTF16(keyHandleBase64Url)); 771 credential->SetType(u"public-key"_ns); 772 credential->SetRawId(aResult.KeyHandle()); 773 credential->SetAttestationResponse(attestation); 774 775 if (aResult.AuthenticatorAttachment().isSome()) { 776 credential->SetAuthenticatorAttachment(aResult.AuthenticatorAttachment()); 777 778 mozilla::glean::webauthn_create::authenticator_attachment 779 .Get(NS_ConvertUTF16toUTF8(aResult.AuthenticatorAttachment().ref())) 780 .Add(1); 781 } else { 782 mozilla::glean::webauthn_get::authenticator_attachment.Get("unknown"_ns) 783 .Add(1); 784 } 785 786 // Forward client extension results. 787 for (const auto& ext : aResult.Extensions()) { 788 if (ext.type() == 789 WebAuthnExtensionResult::TWebAuthnExtensionResultCredProps) { 790 bool credPropsRk = ext.get_WebAuthnExtensionResultCredProps().rk(); 791 credential->SetClientExtensionResultCredPropsRk(credPropsRk); 792 if (credPropsRk) { 793 mozilla::glean::webauthn_create::passkey.Add(1); 794 } 795 } 796 if (ext.type() == 797 WebAuthnExtensionResult::TWebAuthnExtensionResultHmacSecret) { 798 bool hmacCreateSecret = 799 ext.get_WebAuthnExtensionResultHmacSecret().hmacCreateSecret(); 800 credential->SetClientExtensionResultHmacSecret(hmacCreateSecret); 801 } 802 if (ext.type() == 803 WebAuthnExtensionResult::TWebAuthnExtensionResultLargeBlob) { 804 credential->InitClientExtensionResultLargeBlob(); 805 credential->SetClientExtensionResultLargeBlobSupported( 806 ext.get_WebAuthnExtensionResultLargeBlob().flag()); 807 } 808 if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultPrf) { 809 credential->InitClientExtensionResultPrf(); 810 const Maybe<bool> prfEnabled = 811 ext.get_WebAuthnExtensionResultPrf().enabled(); 812 if (prfEnabled.isSome()) { 813 credential->SetClientExtensionResultPrfEnabled(prfEnabled.value()); 814 } 815 const Maybe<WebAuthnExtensionPrfValues> prfValues = 816 ext.get_WebAuthnExtensionResultPrf().results(); 817 if (prfValues.isSome()) { 818 credential->SetClientExtensionResultPrfResultsFirst( 819 prfValues.value().first()); 820 if (prfValues.value().secondMaybe()) { 821 credential->SetClientExtensionResultPrfResultsSecond( 822 prfValues.value().second()); 823 } 824 } 825 } 826 } 827 828 ResolveTransaction(credential); 829 } 830 831 void WebAuthnHandler::FinishGetAssertion( 832 const WebAuthnGetAssertionResult& aResult) { 833 MOZ_ASSERT(NS_IsMainThread()); 834 MOZ_ASSERT(mTransaction.isSome()); 835 836 nsAutoCString keyHandleBase64Url; 837 nsresult rv = Base64URLEncode( 838 aResult.KeyHandle().Length(), aResult.KeyHandle().Elements(), 839 Base64URLEncodePaddingPolicy::Omit, keyHandleBase64Url); 840 if (NS_WARN_IF(NS_FAILED(rv))) { 841 RejectTransaction(rv); 842 return; 843 } 844 845 // Create a new PublicKeyCredential object named value and populate its fields 846 // with the values returned from the authenticator as well as the 847 // clientDataJSON computed earlier. 848 RefPtr<AuthenticatorAssertionResponse> assertion = 849 new AuthenticatorAssertionResponse(mWindow); 850 assertion->SetClientDataJSON(aResult.ClientDataJSON()); 851 assertion->SetAuthenticatorData(aResult.AuthenticatorData()); 852 assertion->SetSignature(aResult.Signature()); 853 assertion->SetUserHandle(aResult.UserHandle()); // may be empty 854 855 RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mWindow); 856 credential->SetId(NS_ConvertASCIItoUTF16(keyHandleBase64Url)); 857 credential->SetType(u"public-key"_ns); 858 credential->SetRawId(aResult.KeyHandle()); 859 credential->SetAssertionResponse(assertion); 860 861 if (aResult.AuthenticatorAttachment().isSome()) { 862 credential->SetAuthenticatorAttachment(aResult.AuthenticatorAttachment()); 863 864 mozilla::glean::webauthn_get::authenticator_attachment 865 .Get(NS_ConvertUTF16toUTF8(aResult.AuthenticatorAttachment().ref())) 866 .Add(1); 867 } else { 868 mozilla::glean::webauthn_get::authenticator_attachment.Get("unknown"_ns) 869 .Add(1); 870 } 871 872 // Forward client extension results. 873 for (const auto& ext : aResult.Extensions()) { 874 if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultAppId) { 875 bool appid = ext.get_WebAuthnExtensionResultAppId().AppId(); 876 credential->SetClientExtensionResultAppId(appid); 877 } 878 if (ext.type() == 879 WebAuthnExtensionResult::TWebAuthnExtensionResultLargeBlob) { 880 if (ext.get_WebAuthnExtensionResultLargeBlob().flag() && 881 ext.get_WebAuthnExtensionResultLargeBlob().written()) { 882 // Signal a read failure by including an empty largeBlob extension. 883 credential->InitClientExtensionResultLargeBlob(); 884 } else if (ext.get_WebAuthnExtensionResultLargeBlob().flag()) { 885 const nsTArray<uint8_t>& largeBlobValue = 886 ext.get_WebAuthnExtensionResultLargeBlob().blob(); 887 credential->InitClientExtensionResultLargeBlob(); 888 credential->SetClientExtensionResultLargeBlobValue(largeBlobValue); 889 } else { 890 bool largeBlobWritten = 891 ext.get_WebAuthnExtensionResultLargeBlob().written(); 892 credential->InitClientExtensionResultLargeBlob(); 893 credential->SetClientExtensionResultLargeBlobWritten(largeBlobWritten); 894 } 895 } 896 if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultPrf) { 897 credential->InitClientExtensionResultPrf(); 898 Maybe<WebAuthnExtensionPrfValues> prfResults = 899 ext.get_WebAuthnExtensionResultPrf().results(); 900 if (prfResults.isSome()) { 901 credential->SetClientExtensionResultPrfResultsFirst( 902 prfResults.value().first()); 903 if (prfResults.value().secondMaybe()) { 904 credential->SetClientExtensionResultPrfResultsSecond( 905 prfResults.value().second()); 906 } 907 } 908 } 909 } 910 911 // Treat successful assertion as user activation for BounceTrackingProtection. 912 nsIGlobalObject* global = mTransaction.ref().mPromise->GetGlobalObject(); 913 if (global) { 914 nsPIDOMWindowInner* window = global->GetAsInnerWindow(); 915 if (window) { 916 (void)BounceTrackingProtection::RecordUserActivation( 917 window->GetWindowContext()); 918 } 919 } 920 921 ResolveTransaction(credential); 922 } 923 924 void WebAuthnHandler::RunAbortAlgorithm() { 925 if (NS_WARN_IF(mTransaction.isNothing())) { 926 return; 927 } 928 929 nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWindow); 930 931 AutoJSAPI jsapi; 932 if (!jsapi.Init(global)) { 933 CancelTransaction(NS_ERROR_DOM_ABORT_ERR); 934 return; 935 } 936 JSContext* cx = jsapi.cx(); 937 JS::Rooted<JS::Value> reason(cx); 938 Signal()->GetReason(cx, &reason); 939 CancelTransaction(reason); 940 } 941 942 void WebAuthnHandler::ResolveTransaction( 943 const RefPtr<PublicKeyCredential>& aCredential) { 944 MOZ_ASSERT(mTransaction.isSome()); 945 946 switch (mTransaction.ref().mType) { 947 case WebAuthnTransactionType::Create: 948 mozilla::glean::webauthn_create::success.Add(1); 949 break; 950 case WebAuthnTransactionType::Get: 951 mozilla::glean::webauthn_get::success.Add(1); 952 break; 953 } 954 955 // Bug 1969341 - we need to reset the transaction before resolving the 956 // promise. This lets us handle the case where resolving the promise initiates 957 // a new WebAuthn request. 958 RefPtr<Promise> promise = mTransaction.ref().mPromise; 959 mTransaction.reset(); 960 Unfollow(); 961 962 promise->MaybeResolve(aCredential); 963 } 964 965 template <typename T> 966 void WebAuthnHandler::RejectTransaction(const T& aReason) { 967 MOZ_ASSERT(mTransaction.isSome()); 968 969 switch (mTransaction.ref().mType) { 970 case WebAuthnTransactionType::Create: 971 mozilla::glean::webauthn_create::failure.Add(1); 972 break; 973 case WebAuthnTransactionType::Get: 974 mozilla::glean::webauthn_get::failure.Add(1); 975 break; 976 } 977 978 // Bug 1969341 - we need to reset the transaction before rejecting the 979 // promise. This lets us handle the case where rejecting the promise initiates 980 // a new WebAuthn request. 981 RefPtr<Promise> promise = mTransaction.ref().mPromise; 982 mTransaction.reset(); 983 Unfollow(); 984 985 promise->MaybeReject(aReason); 986 } 987 988 } // namespace mozilla::dom