gecko-dev/dom/webauthn/WebAuthnManager.cpp
J.C. Jones 2afc1ad18b Bug 1245527 - Rewrite U2F.cpp to use U2FTokenManager. r=keeler, r=ttaubert
- This patch reworks the U2F module to asynchronously call U2FManager,
  which in turn handles constructing and managing the U2FTokenManager
  via IPC.
- Add U2FTransaction{Parent,Child} implementations to mirror similar ones for
  WebAuthn
- Rewrite all tests to compensate for U2F executing asynchronously now.
  - Used async tasks, used the manifest parameters for scheme, and generally
    made these cleaner.
- The mochitest "pref =" functionality from Bug 1328830 doesn't support Android
  yet, causing breakage on Android. Rework the tests to go back to the old way
  of using iframes to test U2F.


NOTE TO REVIEWERS:
 Since this is huge, I recommend the following:

 keeler - please review U2F.cpp/h, the tests, and the security-prefs.js. Most
          of the U2F logic is still in U2F.cpp like before, but there's been
          some reworking of how it is called.

 ttaubert - please review U2FManager, the Transaction classes, build changes,
            and the changes to nsGlobalWindow. All of these should be very
            similar to the WebAuthn code it's patterned off.


MozReview-Commit-ID: C1ZN2ch66Rm

--HG--
extra : rebase_source : 5a2c52b0340c13f471af5040b998eb7e661b1981
2017-09-11 12:56:59 -07:00

958 lines
28 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "hasht.h"
#include "nsICryptoHash.h"
#include "nsNetCID.h"
#include "nsThreadUtils.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/dom/AuthenticatorAttestationResponse.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/PWebAuthnTransaction.h"
#include "mozilla/dom/U2FUtil.h"
#include "mozilla/dom/WebAuthnCBORUtil.h"
#include "mozilla/dom/WebAuthnManager.h"
#include "mozilla/dom/WebAuthnTransactionChild.h"
#include "mozilla/dom/WebAuthnUtil.h"
#include "mozilla/dom/WebCryptoCommon.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/ipc/PBackgroundChild.h"
using namespace mozilla::ipc;
namespace mozilla {
namespace dom {
/***********************************************************************
* Protocol Constants
**********************************************************************/
const uint8_t FLAG_TUP = 0x01; // Test of User Presence required
const uint8_t FLAG_AT = 0x40; // Authenticator Data is provided
/***********************************************************************
* Statics
**********************************************************************/
namespace {
StaticRefPtr<WebAuthnManager> gWebAuthnManager;
static mozilla::LazyLogModule gWebAuthnManagerLog("webauthnmanager");
}
NS_NAMED_LITERAL_STRING(kVisibilityChange, "visibilitychange");
NS_IMPL_ISUPPORTS(WebAuthnManager, nsIIPCBackgroundChildCreateCallback,
nsIDOMEventListener);
/***********************************************************************
* Utility Functions
**********************************************************************/
template<class OOS>
static nsresult
GetAlgorithmName(const OOS& aAlgorithm,
/* out */ nsString& aName)
{
MOZ_ASSERT(aAlgorithm.IsString()); // TODO: remove assertion when we coerce.
if (aAlgorithm.IsString()) {
// If string, then treat as algorithm name
aName.Assign(aAlgorithm.GetAsString());
} else {
// TODO: Coerce to string and extract name. See WebCryptoTask.cpp
}
// Only ES256 is currently supported
if (NORMALIZED_EQUALS(aName, JWK_ALG_ECDSA_P_256)) {
aName.AssignLiteral(JWK_ALG_ECDSA_P_256);
} else {
return NS_ERROR_DOM_SYNTAX_ERR;
}
return NS_OK;
}
static nsresult
AssembleClientData(const nsAString& aOrigin, const CryptoBuffer& aChallenge,
/* out */ nsACString& aJsonOut)
{
MOZ_ASSERT(NS_IsMainThread());
nsString challengeBase64;
nsresult rv = aChallenge.ToJwkBase64(challengeBase64);
if (NS_WARN_IF(NS_FAILED(rv))) {
return NS_ERROR_FAILURE;
}
CollectedClientData clientDataObject;
clientDataObject.mChallenge.Assign(challengeBase64);
clientDataObject.mOrigin.Assign(aOrigin);
clientDataObject.mHashAlg.Assign(NS_LITERAL_STRING("SHA-256"));
nsAutoString temp;
if (NS_WARN_IF(!clientDataObject.ToJSON(temp))) {
return NS_ERROR_FAILURE;
}
aJsonOut.Assign(NS_ConvertUTF16toUTF8(temp));
return NS_OK;
}
nsresult
GetOrigin(nsPIDOMWindowInner* aParent,
/*out*/ nsAString& aOrigin, /*out*/ nsACString& aHost)
{
MOZ_ASSERT(aParent);
nsCOMPtr<nsIDocument> doc = aParent->GetDoc();
MOZ_ASSERT(doc);
nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
nsresult rv = nsContentUtils::GetUTFOrigin(principal, aOrigin);
if (NS_WARN_IF(NS_FAILED(rv)) ||
NS_WARN_IF(aOrigin.IsEmpty())) {
return NS_ERROR_FAILURE;
}
if (aOrigin.EqualsLiteral("null")) {
// 4.1.1.3 If callerOrigin is an opaque origin, reject promise with a
// DOMException whose name is "NotAllowedError", and terminate this
// algorithm
MOZ_LOG(gWebAuthnManagerLog, LogLevel::Debug, ("Rejecting due to opaque origin"));
return NS_ERROR_DOM_NOT_ALLOWED_ERR;
}
nsCOMPtr<nsIURI> originUri;
if (NS_FAILED(principal->GetURI(getter_AddRefs(originUri)))) {
return NS_ERROR_FAILURE;
}
if (NS_FAILED(originUri->GetAsciiHost(aHost))) {
return NS_ERROR_FAILURE;
}
return NS_OK;
}
nsresult
RelaxSameOrigin(nsPIDOMWindowInner* aParent,
const nsAString& aInputRpId,
/* out */ nsACString& aRelaxedRpId)
{
MOZ_ASSERT(aParent);
nsCOMPtr<nsIDocument> doc = aParent->GetDoc();
MOZ_ASSERT(doc);
nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
nsCOMPtr<nsIURI> uri;
if (NS_FAILED(principal->GetURI(getter_AddRefs(uri)))) {
return NS_ERROR_FAILURE;
}
nsAutoCString originHost;
if (NS_FAILED(uri->GetAsciiHost(originHost))) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsIDocument> document = aParent->GetDoc();
if (!document || !document->IsHTMLDocument()) {
return NS_ERROR_FAILURE;
}
nsHTMLDocument* html = document->AsHTMLDocument();
if (NS_WARN_IF(!html)) {
return NS_ERROR_FAILURE;
}
if (!html->IsRegistrableDomainSuffixOfOrEqualTo(aInputRpId, originHost)) {
return NS_ERROR_DOM_SECURITY_ERR;
}
aRelaxedRpId.Assign(NS_ConvertUTF16toUTF8(aInputRpId));
return NS_OK;
}
static void
ListenForVisibilityEvents(nsPIDOMWindowInner* aParent,
WebAuthnManager* aListener)
{
MOZ_ASSERT(aParent);
MOZ_ASSERT(aListener);
nsCOMPtr<nsIDocument> doc = aParent->GetExtantDoc();
if (NS_WARN_IF(!doc)) {
return;
}
nsresult rv = doc->AddSystemEventListener(kVisibilityChange, aListener,
/* use capture */ true,
/* wants untrusted */ false);
Unused << NS_WARN_IF(NS_FAILED(rv));
}
static void
StopListeningForVisibilityEvents(nsPIDOMWindowInner* aParent,
WebAuthnManager* aListener)
{
MOZ_ASSERT(aParent);
MOZ_ASSERT(aListener);
nsCOMPtr<nsIDocument> doc = aParent->GetExtantDoc();
if (NS_WARN_IF(!doc)) {
return;
}
nsresult rv = doc->RemoveSystemEventListener(kVisibilityChange, aListener,
/* use capture */ true);
Unused << NS_WARN_IF(NS_FAILED(rv));
}
/***********************************************************************
* WebAuthnManager Implementation
**********************************************************************/
WebAuthnManager::WebAuthnManager()
{
MOZ_ASSERT(NS_IsMainThread());
}
void
WebAuthnManager::MaybeClearTransaction()
{
mClientData.reset();
mInfo.reset();
mTransactionPromise = nullptr;
if (mCurrentParent) {
StopListeningForVisibilityEvents(mCurrentParent, this);
mCurrentParent = nullptr;
}
if (mChild) {
RefPtr<WebAuthnTransactionChild> c;
mChild.swap(c);
c->Send__delete__(c);
}
}
WebAuthnManager::~WebAuthnManager()
{
MaybeClearTransaction();
}
RefPtr<WebAuthnManager::BackgroundActorPromise>
WebAuthnManager::GetOrCreateBackgroundActor()
{
MOZ_ASSERT(NS_IsMainThread());
PBackgroundChild *actor = BackgroundChild::GetForCurrentThread();
RefPtr<WebAuthnManager::BackgroundActorPromise> promise =
mPBackgroundCreationPromise.Ensure(__func__);
if (actor) {
ActorCreated(actor);
} else {
bool ok = BackgroundChild::GetOrCreateForCurrentThread(this);
if (NS_WARN_IF(!ok)) {
ActorFailed();
}
}
return promise;
}
//static
WebAuthnManager*
WebAuthnManager::GetOrCreate()
{
MOZ_ASSERT(NS_IsMainThread());
if (gWebAuthnManager) {
return gWebAuthnManager;
}
gWebAuthnManager = new WebAuthnManager();
ClearOnShutdown(&gWebAuthnManager);
return gWebAuthnManager;
}
//static
WebAuthnManager*
WebAuthnManager::Get()
{
MOZ_ASSERT(NS_IsMainThread());
return gWebAuthnManager;
}
already_AddRefed<Promise>
WebAuthnManager::MakeCredential(nsPIDOMWindowInner* aParent,
const MakeCredentialOptions& aOptions)
{
MOZ_ASSERT(aParent);
MaybeClearTransaction();
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aParent);
ErrorResult rv;
RefPtr<Promise> promise = Promise::Create(global, rv);
if(rv.Failed()) {
return nullptr;
}
nsString origin;
nsCString rpId;
rv = GetOrigin(aParent, origin, rpId);
if (NS_WARN_IF(rv.Failed())) {
promise->MaybeReject(rv);
return promise.forget();
}
// If timeoutSeconds was specified, check if its value lies within a
// reasonable range as defined by the platform and if not, correct it to the
// closest value lying within that range.
uint32_t adjustedTimeout = 30000;
if (aOptions.mTimeout.WasPassed()) {
adjustedTimeout = aOptions.mTimeout.Value();
adjustedTimeout = std::max(15000u, adjustedTimeout);
adjustedTimeout = std::min(120000u, adjustedTimeout);
}
if (aOptions.mRp.mId.WasPassed()) {
// If rpId is specified, then invoke the procedure used for relaxing the
// same-origin restriction by setting the document.domain attribute, using
// rpId as the given value but without changing the current documents
// domain. If no errors are thrown, set rpId to the value of host as
// computed by this procedure, and rpIdHash to the SHA-256 hash of rpId.
// Otherwise, reject promise with a DOMException whose name is
// "SecurityError", and terminate this algorithm.
if (NS_FAILED(RelaxSameOrigin(aParent, aOptions.mRp.mId.Value(), rpId))) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
}
CryptoBuffer rpIdHash;
if (!rpIdHash.SetLength(SHA256_LENGTH, fallible)) {
promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
return promise.forget();
}
nsresult srv;
nsCOMPtr<nsICryptoHash> hashService =
do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &srv);
if (NS_WARN_IF(NS_FAILED(srv))) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
srv = HashCString(hashService, rpId, rpIdHash);
if (NS_WARN_IF(NS_FAILED(srv))) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
// Process each element of cryptoParameters using the following steps, to
// produce a new sequence normalizedParameters.
nsTArray<PublicKeyCredentialParameters> normalizedParams;
for (size_t a = 0; a < aOptions.mParameters.Length(); ++a) {
// Let current be the currently selected element of
// cryptoParameters.
// If current.type does not contain a PublicKeyCredentialType
// supported by this implementation, then stop processing current and move
// on to the next element in cryptoParameters.
if (aOptions.mParameters[a].mType != PublicKeyCredentialType::Public_key) {
continue;
}
// Let normalizedAlgorithm be the result of normalizing an algorithm using
// the procedure defined in [WebCryptoAPI], with alg set to
// current.algorithm and op set to 'generateKey'. If an error occurs during
// this procedure, then stop processing current and move on to the next
// element in cryptoParameters.
nsString algName;
if (NS_FAILED(GetAlgorithmName(aOptions.mParameters[a].mAlgorithm,
algName))) {
continue;
}
// Add a new object of type PublicKeyCredentialParameters to
// normalizedParameters, with type set to current.type and algorithm set to
// normalizedAlgorithm.
PublicKeyCredentialParameters normalizedObj;
normalizedObj.mType = aOptions.mParameters[a].mType;
normalizedObj.mAlgorithm.SetAsString().Assign(algName);
if (!normalizedParams.AppendElement(normalizedObj, mozilla::fallible)){
promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
return promise.forget();
}
}
// If normalizedAlgorithm is empty and cryptoParameters was not empty, cancel
// the timer started in step 2, reject promise with a DOMException whose name
// is "NotSupportedError", and terminate this algorithm.
if (normalizedParams.IsEmpty() && !aOptions.mParameters.IsEmpty()) {
promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
return promise.forget();
}
// TODO: The following check should not be here. This is checking for
// parameters specific to the soft key, and should be put in the soft key
// manager in the parent process. Still need to serialize
// PublicKeyCredentialParameters first.
// Check if at least one of the specified combinations of
// PublicKeyCredentialParameters and cryptographic parameters is supported. If
// not, return an error code equivalent to NotSupportedError and terminate the
// operation.
bool isValidCombination = false;
for (size_t a = 0; a < normalizedParams.Length(); ++a) {
if (normalizedParams[a].mType == PublicKeyCredentialType::Public_key &&
normalizedParams[a].mAlgorithm.IsString() &&
normalizedParams[a].mAlgorithm.GetAsString().EqualsLiteral(
JWK_ALG_ECDSA_P_256)) {
isValidCombination = true;
break;
}
}
if (!isValidCombination) {
promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
return promise.forget();
}
// If excludeList is undefined, set it to the empty list.
//
// If extensions was specified, process any extensions supported by this
// client platform, to produce the extension data that needs to be sent to the
// authenticator. If an error is encountered while processing an extension,
// skip that extension and do not produce any extension data for it. Call the
// result of this processing clientExtensions.
//
// Currently no extensions are supported
//
// Use attestationChallenge, callerOrigin and rpId, along with the token
// binding key associated with callerOrigin (if any), to create a ClientData
// structure representing this request. Choose a hash algorithm for hashAlg
// and compute the clientDataJSON and clientDataHash.
CryptoBuffer challenge;
if (!challenge.Assign(aOptions.mChallenge)) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
nsAutoCString clientDataJSON;
srv = AssembleClientData(origin, challenge, clientDataJSON);
if (NS_WARN_IF(NS_FAILED(srv))) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
CryptoBuffer clientDataHash;
if (!clientDataHash.SetLength(SHA256_LENGTH, fallible)) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
srv = HashCString(hashService, clientDataJSON, clientDataHash);
if (NS_WARN_IF(NS_FAILED(srv))) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
nsTArray<WebAuthnScopedCredentialDescriptor> excludeList;
if (aOptions.mExcludeList.WasPassed()) {
for (const auto& s: aOptions.mExcludeList.Value()) {
WebAuthnScopedCredentialDescriptor c;
CryptoBuffer cb;
cb.Assign(s.mId);
c.id() = cb;
excludeList.AppendElement(c);
}
}
// TODO: Add extension list building
nsTArray<WebAuthnExtension> extensions;
WebAuthnTransactionInfo info(rpIdHash,
clientDataHash,
adjustedTimeout,
excludeList,
extensions);
RefPtr<MozPromise<nsresult, nsresult, false>> p = GetOrCreateBackgroundActor();
p->Then(GetMainThreadSerialEventTarget(), __func__,
[]() {
WebAuthnManager* mgr = WebAuthnManager::Get();
if (!mgr) {
return;
}
mgr->StartRegister();
},
[]() {
// This case can't actually happen, we'll have crashed if the child
// failed to create.
});
mTransactionPromise = promise;
mClientData = Some(clientDataJSON);
mCurrentParent = aParent;
mInfo = Some(info);
ListenForVisibilityEvents(aParent, this);
return promise.forget();
}
void
WebAuthnManager::StartRegister() {
if (mChild) {
mChild->SendRequestRegister(mInfo.ref());
}
}
void
WebAuthnManager::StartSign() {
if (mChild) {
mChild->SendRequestSign(mInfo.ref());
}
}
void
WebAuthnManager::StartCancel() {
if (mChild) {
mChild->SendRequestCancel();
}
}
already_AddRefed<Promise>
WebAuthnManager::GetAssertion(nsPIDOMWindowInner* aParent,
const PublicKeyCredentialRequestOptions& aOptions)
{
MOZ_ASSERT(aParent);
MaybeClearTransaction();
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aParent);
ErrorResult rv;
RefPtr<Promise> promise = Promise::Create(global, rv);
if(rv.Failed()) {
return nullptr;
}
nsString origin;
nsCString rpId;
rv = GetOrigin(aParent, origin, rpId);
if (NS_WARN_IF(rv.Failed())) {
promise->MaybeReject(rv);
return promise.forget();
}
// If timeoutSeconds was specified, check if its value lies within a
// reasonable range as defined by the platform and if not, correct it to the
// closest value lying within that range.
uint32_t adjustedTimeout = 30000;
if (aOptions.mTimeout.WasPassed()) {
adjustedTimeout = aOptions.mTimeout.Value();
adjustedTimeout = std::max(15000u, adjustedTimeout);
adjustedTimeout = std::min(120000u, adjustedTimeout);
}
if (aOptions.mRpId.WasPassed()) {
// If rpId is specified, then invoke the procedure used for relaxing the
// same-origin restriction by setting the document.domain attribute, using
// rpId as the given value but without changing the current documents
// domain. If no errors are thrown, set rpId to the value of host as
// computed by this procedure, and rpIdHash to the SHA-256 hash of rpId.
// Otherwise, reject promise with a DOMException whose name is
// "SecurityError", and terminate this algorithm.
if (NS_FAILED(RelaxSameOrigin(aParent, aOptions.mRpId.Value(), rpId))) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
}
CryptoBuffer rpIdHash;
if (!rpIdHash.SetLength(SHA256_LENGTH, fallible)) {
promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
return promise.forget();
}
nsresult srv;
nsCOMPtr<nsICryptoHash> hashService =
do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &srv);
if (NS_WARN_IF(NS_FAILED(srv))) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
srv = HashCString(hashService, rpId, rpIdHash);
if (NS_WARN_IF(NS_FAILED(srv))) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
// Use assertionChallenge, callerOrigin and rpId, along with the token binding
// key associated with callerOrigin (if any), to create a ClientData structure
// representing this request. Choose a hash algorithm for hashAlg and compute
// the clientDataJSON and clientDataHash.
CryptoBuffer challenge;
if (!challenge.Assign(aOptions.mChallenge)) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
nsAutoCString clientDataJSON;
srv = AssembleClientData(origin, challenge, clientDataJSON);
if (NS_WARN_IF(NS_FAILED(srv))) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
CryptoBuffer clientDataHash;
if (!clientDataHash.SetLength(SHA256_LENGTH, fallible)) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
srv = HashCString(hashService, clientDataJSON, clientDataHash);
if (NS_WARN_IF(NS_FAILED(srv))) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
// Note: we only support U2F-style authentication for now, so we effectively
// require an AllowList.
if (aOptions.mAllowList.Length() < 1) {
promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
return promise.forget();
}
nsTArray<WebAuthnScopedCredentialDescriptor> allowList;
for (const auto& s: aOptions.mAllowList) {
WebAuthnScopedCredentialDescriptor c;
CryptoBuffer cb;
cb.Assign(s.mId);
c.id() = cb;
allowList.AppendElement(c);
}
// TODO: Add extension list building
// If extensions was specified, process any extensions supported by this
// client platform, to produce the extension data that needs to be sent to the
// authenticator. If an error is encountered while processing an extension,
// skip that extension and do not produce any extension data for it. Call the
// result of this processing clientExtensions.
nsTArray<WebAuthnExtension> extensions;
WebAuthnTransactionInfo info(rpIdHash,
clientDataHash,
adjustedTimeout,
allowList,
extensions);
RefPtr<MozPromise<nsresult, nsresult, false>> p = GetOrCreateBackgroundActor();
p->Then(GetMainThreadSerialEventTarget(), __func__,
[]() {
WebAuthnManager* mgr = WebAuthnManager::Get();
if (!mgr) {
return;
}
mgr->StartSign();
},
[]() {
// This case can't actually happen, we'll have crashed if the child
// failed to create.
});
// Only store off the promise if we've succeeded in sending the IPC event.
mTransactionPromise = promise;
mClientData = Some(clientDataJSON);
mCurrentParent = aParent;
mInfo = Some(info);
ListenForVisibilityEvents(aParent, this);
return promise.forget();
}
void
WebAuthnManager::FinishMakeCredential(nsTArray<uint8_t>& aRegBuffer)
{
MOZ_ASSERT(mTransactionPromise);
MOZ_ASSERT(mInfo.isSome());
CryptoBuffer regData;
if (NS_WARN_IF(!regData.Assign(aRegBuffer.Elements(), aRegBuffer.Length()))) {
Cancel(NS_ERROR_OUT_OF_MEMORY);
return;
}
mozilla::dom::CryptoBuffer aaguidBuf;
if (NS_WARN_IF(!aaguidBuf.SetCapacity(16, mozilla::fallible))) {
Cancel(NS_ERROR_OUT_OF_MEMORY);
return;
}
// TODO: Adjust the AAGUID from all zeroes in Bug 1381575 (if needed)
// See https://github.com/w3c/webauthn/issues/506
for (int i=0; i<16; i++) {
aaguidBuf.AppendElement(0x00, mozilla::fallible);
}
// Decompose the U2F registration packet
CryptoBuffer pubKeyBuf;
CryptoBuffer keyHandleBuf;
CryptoBuffer attestationCertBuf;
CryptoBuffer signatureBuf;
// Only handles attestation cert chains of length=1.
nsresult rv = U2FDecomposeRegistrationResponse(regData, pubKeyBuf, keyHandleBuf,
attestationCertBuf, signatureBuf);
if (NS_WARN_IF(NS_FAILED(rv))) {
Cancel(rv);
return;
}
MOZ_ASSERT(keyHandleBuf.Length() <= 0xFFFF);
nsAutoString keyHandleBase64Url;
rv = keyHandleBuf.ToJwkBase64(keyHandleBase64Url);
if (NS_WARN_IF(NS_FAILED(rv))) {
Cancel(rv);
return;
}
CryptoBuffer clientDataBuf;
if (!clientDataBuf.Assign(mClientData.ref())) {
Cancel(NS_ERROR_OUT_OF_MEMORY);
return;
}
CryptoBuffer rpIdHashBuf;
if (!rpIdHashBuf.Assign(mInfo.ref().RpIdHash())) {
Cancel(NS_ERROR_OUT_OF_MEMORY);
return;
}
// Construct the public key object
CryptoBuffer pubKeyObj;
rv = CBOREncodePublicKeyObj(pubKeyBuf, pubKeyObj);
if (NS_FAILED(rv)) {
Cancel(rv);
return;
}
// During create credential, counter is always 0 for U2F
// See https://github.com/w3c/webauthn/issues/507
mozilla::dom::CryptoBuffer counterBuf;
if (NS_WARN_IF(!counterBuf.SetCapacity(4, mozilla::fallible))) {
Cancel(NS_ERROR_OUT_OF_MEMORY);
return;
}
counterBuf.AppendElement(0x00, mozilla::fallible);
counterBuf.AppendElement(0x00, mozilla::fallible);
counterBuf.AppendElement(0x00, mozilla::fallible);
counterBuf.AppendElement(0x00, mozilla::fallible);
// Construct the Attestation Data, which slots into the end of the
// Authentication Data buffer.
CryptoBuffer attDataBuf;
rv = AssembleAttestationData(aaguidBuf, keyHandleBuf, pubKeyObj, attDataBuf);
if (NS_FAILED(rv)) {
Cancel(rv);
return;
}
mozilla::dom::CryptoBuffer authDataBuf;
rv = AssembleAuthenticatorData(rpIdHashBuf, FLAG_TUP, counterBuf, attDataBuf,
authDataBuf);
if (NS_FAILED(rv)) {
Cancel(rv);
return;
}
// The Authentication Data buffer gets CBOR-encoded with the Cert and
// Signature to build the Attestation Object.
CryptoBuffer attObj;
rv = CBOREncodeAttestationObj(authDataBuf, attestationCertBuf, signatureBuf,
attObj);
if (NS_FAILED(rv)) {
Cancel(rv);
return;
}
// Create a new PublicKeyCredential object and populate its fields with the
// values returned from the authenticator as well as the clientDataJSON
// computed earlier.
RefPtr<AuthenticatorAttestationResponse> attestation =
new AuthenticatorAttestationResponse(mCurrentParent);
attestation->SetClientDataJSON(clientDataBuf);
attestation->SetAttestationObject(attObj);
RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mCurrentParent);
credential->SetId(keyHandleBase64Url);
credential->SetType(NS_LITERAL_STRING("public-key"));
credential->SetRawId(keyHandleBuf);
credential->SetResponse(attestation);
mTransactionPromise->MaybeResolve(credential);
MaybeClearTransaction();
}
void
WebAuthnManager::FinishGetAssertion(nsTArray<uint8_t>& aCredentialId,
nsTArray<uint8_t>& aSigBuffer)
{
MOZ_ASSERT(mTransactionPromise);
MOZ_ASSERT(mInfo.isSome());
CryptoBuffer tokenSignatureData;
if (NS_WARN_IF(!tokenSignatureData.Assign(aSigBuffer.Elements(),
aSigBuffer.Length()))) {
Cancel(NS_ERROR_OUT_OF_MEMORY);
return;
}
CryptoBuffer clientDataBuf;
if (!clientDataBuf.Assign(mClientData.ref())) {
Cancel(NS_ERROR_OUT_OF_MEMORY);
return;
}
CryptoBuffer rpIdHashBuf;
if (!rpIdHashBuf.Assign(mInfo.ref().RpIdHash())) {
Cancel(NS_ERROR_OUT_OF_MEMORY);
return;
}
CryptoBuffer signatureBuf;
CryptoBuffer counterBuf;
uint8_t flags = 0;
nsresult rv = U2FDecomposeSignResponse(tokenSignatureData, flags, counterBuf,
signatureBuf);
if (NS_WARN_IF(NS_FAILED(rv))) {
Cancel(rv);
return;
}
CryptoBuffer attestationDataBuf;
CryptoBuffer authenticatorDataBuf;
rv = AssembleAuthenticatorData(rpIdHashBuf, FLAG_TUP, counterBuf,
/* deliberately empty */ attestationDataBuf,
authenticatorDataBuf);
if (NS_WARN_IF(NS_FAILED(rv))) {
Cancel(rv);
return;
}
CryptoBuffer credentialBuf;
if (!credentialBuf.Assign(aCredentialId)) {
Cancel(NS_ERROR_OUT_OF_MEMORY);
return;
}
nsAutoString credentialBase64Url;
rv = credentialBuf.ToJwkBase64(credentialBase64Url);
if (NS_WARN_IF(NS_FAILED(rv))) {
Cancel(rv);
return;
}
// If any authenticator returns success:
// Create a new PublicKeyCredential object named value and populate its fields
// with the values returned from the authenticator as well as the
// clientDataJSON computed earlier.
RefPtr<AuthenticatorAssertionResponse> assertion =
new AuthenticatorAssertionResponse(mCurrentParent);
assertion->SetClientDataJSON(clientDataBuf);
assertion->SetAuthenticatorData(authenticatorDataBuf);
assertion->SetSignature(signatureBuf);
RefPtr<PublicKeyCredential> credential =
new PublicKeyCredential(mCurrentParent);
credential->SetId(credentialBase64Url);
credential->SetType(NS_LITERAL_STRING("public-key"));
credential->SetRawId(credentialBuf);
credential->SetResponse(assertion);
mTransactionPromise->MaybeResolve(credential);
MaybeClearTransaction();
}
void
WebAuthnManager::Cancel(const nsresult& aError)
{
MOZ_ASSERT(NS_IsMainThread());
if (mTransactionPromise) {
mTransactionPromise->MaybeReject(aError);
}
MaybeClearTransaction();
}
NS_IMETHODIMP
WebAuthnManager::HandleEvent(nsIDOMEvent* aEvent)
{
MOZ_ASSERT(aEvent);
nsAutoString type;
aEvent->GetType(type);
if (!type.Equals(kVisibilityChange)) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsIDocument> doc =
do_QueryInterface(aEvent->InternalDOMEvent()->GetTarget());
MOZ_ASSERT(doc);
if (doc && doc->Hidden()) {
MOZ_LOG(gWebAuthnManagerLog, LogLevel::Debug,
("Visibility change: WebAuthn window is hidden, cancelling job."));
StartCancel();
Cancel(NS_ERROR_ABORT);
}
return NS_OK;
}
void
WebAuthnManager::ActorCreated(PBackgroundChild* aActor)
{
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aActor);
if (mChild) {
return;
}
RefPtr<WebAuthnTransactionChild> mgr(new WebAuthnTransactionChild());
PWebAuthnTransactionChild* constructedMgr =
aActor->SendPWebAuthnTransactionConstructor(mgr);
if (NS_WARN_IF(!constructedMgr)) {
ActorFailed();
return;
}
MOZ_ASSERT(constructedMgr == mgr);
mChild = mgr.forget();
mPBackgroundCreationPromise.Resolve(NS_OK, __func__);
}
void
WebAuthnManager::ActorDestroyed()
{
mChild = nullptr;
}
void
WebAuthnManager::ActorFailed()
{
MOZ_CRASH("We shouldn't be here!");
}
}
}