Bug 1323339 - Add WebAuthnManager and support IPC Child classes; r=baku,jcj

Takes functionality once in the WebAuthentication DOM class that needs
to be handled by the content process, and moves it to a
singleton (per-content-process) manager class. This allows the
WebAuthn API to centralize management of transactions and IPC
channels. Patch also creates the child (content-process) classes for
WebAuthn IPC channels.

MozReview-Commit-ID: 6ju2LK8lvNR

--HG--
extra : rebase_source : 3d800902631ee88e3702c978e9d8ed9c6f88601f
This commit is contained in:
Kyle Machulis 2017-04-19 17:46:08 -07:00
parent c8e3b2a85f
commit 29df95101e
9 changed files with 1214 additions and 0 deletions

View File

@ -0,0 +1,794 @@
/* -*- 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 "nsNetCID.h"
#include "nsICryptoHash.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/WebAuthnManager.h"
#include "mozilla/dom/WebAuthnUtil.h"
#include "mozilla/dom/PWebAuthnTransaction.h"
#include "mozilla/dom/WebAuthnTransactionChild.h"
#include "mozilla/dom/WebCryptoCommon.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ipc/BackgroundChild.h"
using namespace mozilla::ipc;
namespace mozilla {
namespace dom {
/***********************************************************************
* Statics
**********************************************************************/
namespace {
StaticRefPtr<WebAuthnManager> gWebAuthnManager;
static mozilla::LazyLogModule gWebAuthnManagerLog("webauthnmanager");
}
NS_IMPL_ISUPPORTS(WebAuthnManager, nsIIPCBackgroundChildCreateCallback);
/***********************************************************************
* Utility Functions
**********************************************************************/
template<class OOS>
static nsresult
GetAlgorithmName(JSContext* aCx, 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
}
if (!NormalizeToken(aName, aName)) {
return NS_ERROR_DOM_SYNTAX_ERR;
}
return NS_OK;
}
static nsresult
HashCString(nsICryptoHash* aHashService, const nsACString& aIn,
/* out */ CryptoBuffer& aOut)
{
MOZ_ASSERT(aHashService);
nsresult rv = aHashService->Init(nsICryptoHash::SHA256);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = aHashService->Update(
reinterpret_cast<const uint8_t*>(aIn.BeginReading()),aIn.Length());
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
nsAutoCString fullHash;
// Passing false below means we will get a binary result rather than a
// base64-encoded string.
rv = aHashService->Finish(false, fullHash);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
aOut.Assign(fullHash);
return rv;
}
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;
}
WebAuthnClientData clientDataObject;
clientDataObject.mOrigin.Assign(aOrigin);
clientDataObject.mHashAlg.SetAsString().Assign(NS_LITERAL_STRING("S256"));
clientDataObject.mChallenge.Assign(challengeBase64);
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)
{
nsCOMPtr<nsIDocument> doc = aParent->GetDoc();
MOZ_ASSERT(doc);
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;
}
return NS_OK;
}
nsresult
RelaxSameOrigin(nsPIDOMWindowInner* aParent,
const nsAString& aInputRpId,
/* out */ nsACString& aRelaxedRpId)
{
MOZ_ASSERT(aParent);
nsCOMPtr<nsIDocument> document = aParent->GetDoc();
if (!document || !document->IsHTMLDocument()) {
return NS_ERROR_FAILURE;
}
// TODO: Bug 1329764: Invoke the Relax Algorithm, once properly defined
aRelaxedRpId.Assign(NS_ConvertUTF16toUTF8(aInputRpId));
return NS_OK;
}
/***********************************************************************
* WebAuthnManager Implementation
**********************************************************************/
WebAuthnManager::WebAuthnManager()
{
MOZ_ASSERT(NS_IsMainThread());
}
void
WebAuthnManager::MaybeClearTransaction()
{
mClientData.reset();
mInfo.reset();
mTransactionPromise = nullptr;
if (mChild) {
RefPtr<WebAuthnTransactionChild> c;
mChild.swap(c);
c->Send__delete__(c);
}
}
WebAuthnManager::~WebAuthnManager()
{
MaybeClearTransaction();
}
already_AddRefed<MozPromise<nsresult, nsresult, false>>
WebAuthnManager::GetOrCreateBackgroundActor()
{
bool ok = BackgroundChild::GetOrCreateForCurrentThread(this);
if (NS_WARN_IF(!ok)) {
ActorFailed();
}
return mPBackgroundCreationPromise.Ensure(__func__);
}
//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, JSContext* aCx,
const Account& aAccountInformation,
const Sequence<ScopedCredentialParameters>& aCryptoParameters,
const ArrayBufferViewOrArrayBuffer& aChallenge,
const ScopedCredentialOptions& 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;
rv = GetOrigin(aParent, origin);
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.
double adjustedTimeout = 30.0;
if (aOptions.mTimeoutSeconds.WasPassed()) {
adjustedTimeout = aOptions.mTimeoutSeconds.Value();
adjustedTimeout = std::max(15.0, adjustedTimeout);
adjustedTimeout = std::min(120.0, adjustedTimeout);
}
nsCString rpId;
if (!aOptions.mRpId.WasPassed()) {
// If rpId is not specified, then set rpId to callerOrigin, and rpIdHash to
// the SHA-256 hash of rpId.
rpId.Assign(NS_ConvertUTF16toUTF8(origin));
} else {
// 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();
}
// Process each element of cryptoParameters using the following steps, to
// produce a new sequence normalizedParameters.
nsTArray<ScopedCredentialParameters> normalizedParams;
for (size_t a = 0; a < aCryptoParameters.Length(); ++a) {
// Let current be the currently selected element of
// cryptoParameters.
// If current.type does not contain a ScopedCredentialType
// supported by this implementation, then stop processing current and move
// on to the next element in cryptoParameters.
if (aCryptoParameters[a].mType != ScopedCredentialType::ScopedCred) {
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(aCx, aCryptoParameters[a].mAlgorithm,
algName))) {
continue;
}
// Add a new object of type ScopedCredentialParameters to
// normalizedParameters, with type set to current.type and algorithm set to
// normalizedAlgorithm.
ScopedCredentialParameters normalizedObj;
normalizedObj.mType = aCryptoParameters[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() && !aCryptoParameters.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
// ScopedCredentialParameters first.
// Check if at least one of the specified combinations of ScopedCredentialType
// 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 == ScopedCredentialType::ScopedCred &&
normalizedParams[a].mAlgorithm.IsString() &&
normalizedParams[a].mAlgorithm.GetAsString().EqualsLiteral(
WEBCRYPTO_NAMED_CURVE_P256)) {
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(aChallenge)) {
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;
c.type() = static_cast<uint32_t>(s.mType);
CryptoBuffer cb;
cb.Assign(s.mId);
c.id() = cb;
if (s.mTransports.WasPassed()) {
for (const auto& t: s.mTransports.Value()) {
c.transports().AppendElement(static_cast<uint32_t>(t));
}
}
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(AbstractThread::MainThread(), __func__,
[this]() {
this->StartRegister();
},
[this]() {
this->MaybeClearTransaction();
});
mTransactionPromise = promise;
mClientData = Some(clientDataJSON);
mCurrentParent = aParent;
mInfo = Some(info);
return promise.forget();
}
void
WebAuthnManager::StartRegister() {
if (mChild) {
mChild->SendRequestRegister(mInfo.ref());
}
}
void
WebAuthnManager::StartSign() {
if (mChild) {
mChild->SendRequestSign(mInfo.ref());
}
}
already_AddRefed<Promise>
WebAuthnManager::GetAssertion(nsPIDOMWindowInner* aParent,
const ArrayBufferViewOrArrayBuffer& aChallenge,
const AssertionOptions& 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;
rv = GetOrigin(aParent, origin);
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.
double adjustedTimeout = 30.0;
if (aOptions.mTimeoutSeconds.WasPassed()) {
adjustedTimeout = aOptions.mTimeoutSeconds.Value();
adjustedTimeout = std::max(15.0, adjustedTimeout);
adjustedTimeout = std::min(120.0, adjustedTimeout);
}
nsCString rpId;
if (!aOptions.mRpId.WasPassed()) {
// If rpId is not specified, then set rpId to callerOrigin, and rpIdHash to
// the SHA-256 hash of rpId.
rpId.Assign(NS_ConvertUTF16toUTF8(origin));
} else {
// 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(aChallenge)) {
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.WasPassed()) {
promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
return promise.forget();
}
nsTArray<WebAuthnScopedCredentialDescriptor> allowList;
for (const auto& s: aOptions.mAllowList.Value()) {
WebAuthnScopedCredentialDescriptor c;
c.type() = static_cast<uint32_t>(s.mType);
CryptoBuffer cb;
cb.Assign(s.mId);
c.id() = cb;
if (s.mTransports.WasPassed()) {
for (const auto& t: s.mTransports.Value()) {
c.transports().AppendElement(static_cast<uint32_t>(t));
}
}
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,
10000,
allowList,
extensions);
RefPtr<MozPromise<nsresult, nsresult, false>> p = GetOrCreateBackgroundActor();
p->Then(AbstractThread::MainThread(), __func__,
[this]() {
this->StartSign();
},
[this]() {
this->MaybeClearTransaction();
});
// Only store off the promise if we've succeeded in sending the IPC event.
mTransactionPromise = promise;
mClientData = Some(clientDataJSON);
mCurrentParent = aParent;
mInfo = Some(info);
return promise.forget();
}
void
WebAuthnManager::FinishMakeCredential(nsTArray<uint8_t>& aRegBuffer,
nsTArray<uint8_t>& aSigBuffer)
{
MOZ_ASSERT(mTransactionPromise);
MOZ_ASSERT(mInfo.isSome());
CryptoBuffer regData;
if (NS_WARN_IF(!regData.Assign(aRegBuffer.Elements(), aRegBuffer.Length()))) {
mTransactionPromise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
return;
}
// Decompose the U2F registration packet
CryptoBuffer pubKeyBuf;
CryptoBuffer keyHandleBuf;
CryptoBuffer attestationCertBuf;
CryptoBuffer signatureBuf;
nsresult rv = U2FDecomposeRegistrationResponse(regData, pubKeyBuf, keyHandleBuf,
attestationCertBuf, signatureBuf);
if (NS_WARN_IF(NS_FAILED(rv))) {
Cancel(rv);
return;
}
CryptoBuffer signatureData;
if (NS_WARN_IF(!signatureData.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 authenticatorDataBuf;
rv = U2FAssembleAuthenticatorData(authenticatorDataBuf, rpIdHashBuf,
signatureData);
if (NS_WARN_IF(NS_FAILED(rv))) {
Cancel(NS_ERROR_OUT_OF_MEMORY);
return;
}
// Create a new ScopedCredentialInfo object named value and populate its
// fields with the values returned from the authenticator as well as the
// clientDataJSON computed earlier.
RefPtr<ScopedCredential> credential = new ScopedCredential(mCurrentParent);
credential->SetType(ScopedCredentialType::ScopedCred);
credential->SetId(keyHandleBuf);
RefPtr<WebAuthnAttestation> attestation = new WebAuthnAttestation(mCurrentParent);
attestation->SetFormat(NS_LITERAL_STRING("u2f"));
attestation->SetClientData(clientDataBuf);
attestation->SetAuthenticatorData(authenticatorDataBuf);
attestation->SetAttestation(regData);
RefPtr<ScopedCredentialInfo> info = new ScopedCredentialInfo(mCurrentParent);
info->SetCredential(credential);
info->SetAttestation(attestation);
mTransactionPromise->MaybeResolve(info);
MaybeClearTransaction();
}
void
WebAuthnManager::FinishGetAssertion(nsTArray<uint8_t>& aCredentialId,
nsTArray<uint8_t>& aSigBuffer)
{
MOZ_ASSERT(mTransactionPromise);
MOZ_ASSERT(mInfo.isSome());
CryptoBuffer signatureData;
if (NS_WARN_IF(!signatureData.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 authenticatorDataBuf;
nsresult rv = U2FAssembleAuthenticatorData(authenticatorDataBuf, rpIdHashBuf,
signatureData);
if (NS_WARN_IF(NS_FAILED(rv))) {
Cancel(rv);
return;
}
CryptoBuffer credentialBuf;
if (!credentialBuf.Assign(aCredentialId)) {
Cancel(rv);
return;
}
// If any authenticator returns success:
// Create a new WebAuthnAssertion object named value and populate its fields
// with the values returned from the authenticator as well as the
// clientDataJSON computed earlier.
RefPtr<ScopedCredential> credential = new ScopedCredential(mCurrentParent);
credential->SetType(ScopedCredentialType::ScopedCred);
credential->SetId(credentialBuf);
RefPtr<WebAuthnAssertion> assertion = new WebAuthnAssertion(mCurrentParent);
assertion->SetCredential(credential);
assertion->SetClientData(clientDataBuf);
assertion->SetAuthenticatorData(authenticatorDataBuf);
assertion->SetSignature(signatureData);
mTransactionPromise->MaybeResolve(assertion);
MaybeClearTransaction();
}
void
WebAuthnManager::Cancel(const nsresult& aError)
{
if (mChild) {
mChild->SendRequestCancel();
}
if (mTransactionPromise) {
mTransactionPromise->MaybeReject(aError);
}
MaybeClearTransaction();
}
void
WebAuthnManager::ActorCreated(PBackgroundChild* aActor)
{
MOZ_ASSERT(aActor);
RefPtr<WebAuthnTransactionChild> mgr(new WebAuthnTransactionChild());
PWebAuthnTransactionChild* constructedMgr =
aActor->SendPWebAuthnTransactionConstructor(mgr);
if (NS_WARN_IF(!constructedMgr)) {
ActorFailed();
return;
}
MOZ_ASSERT(constructedMgr == mgr);
mChild = mgr.forget();
// Add a ref to mChild here, that will be deref'd by
// BackgroundChildImpl::DeallocPMIDIManagerChild on IPC cleanup.
mChild->AddRef();
mPBackgroundCreationPromise.Resolve(NS_OK, __func__);
}
void
WebAuthnManager::ActorDestroyed()
{
mChild = nullptr;
}
void
WebAuthnManager::ActorFailed()
{
MOZ_CRASH("We shouldn't be here!");
}
}
}

View File

@ -0,0 +1,127 @@
/* -*- 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/. */
#ifndef mozilla_dom_WebAuthnManager_h
#define mozilla_dom_WebAuthnManager_h
#include "nsIIPCBackgroundChildCreateCallback.h"
#include "mozilla/MozPromise.h"
#include "mozilla/dom/PWebAuthnTransaction.h"
/*
* Content process manager for the WebAuthn protocol. Created on calls to the
* WebAuthentication DOM object, this manager handles establishing IPC channels
* for WebAuthn transactions, as well as keeping track of JS Promise objects
* representing transactions in flight.
*
* The WebAuthn spec (https://www.w3.org/TR/webauthn/) allows for two different
* types of transactions: registration and signing. When either of these is
* requested via the DOM API, the following steps are executed in the
* WebAuthnManager:
*
* - Validation of the request. Return a failed promise to js if request does
* not have correct parameters.
*
* - If request is valid, open a new IPC channel for running the transaction. If
* another transaction is already running in this content process, cancel it.
* Return a pending promise to js.
*
* - Send transaction information to parent process (by running the Start*
* functions of WebAuthnManager). Assuming another transaction is currently in
* flight in another content process, parent will handle canceling it.
*
* - On return of successful transaction information from parent process, turn
* information into DOM object format required by spec, and resolve promise
* (by running the Finish* functions of WebAuthnManager). On cancellation
* request from parent, reject promise with corresponding error code. Either
* outcome will also close the IPC channel.
*
*/
namespace mozilla {
namespace dom {
struct Account;
class ArrayBufferViewOrArrayBuffer;
struct AssertionOptions;
class OwningArrayBufferViewOrArrayBuffer;
struct ScopedCredentialOptions;
struct ScopedCredentialParameters;
class Promise;
class WebAuthnTransactionChild;
class WebAuthnTransactionInfo;
class WebAuthnManager final : public nsIIPCBackgroundChildCreateCallback
{
public:
NS_DECL_ISUPPORTS
static WebAuthnManager* GetOrCreate();
static WebAuthnManager* Get();
void
FinishMakeCredential(nsTArray<uint8_t>& aRegBuffer,
nsTArray<uint8_t>& aSigBuffer);
void
FinishGetAssertion(nsTArray<uint8_t>& aCredentialId,
nsTArray<uint8_t>& aSigBuffer);
void
Cancel(const nsresult& aError);
already_AddRefed<Promise>
MakeCredential(nsPIDOMWindowInner* aParent, JSContext* aCx,
const Account& aAccountInformation,
const Sequence<ScopedCredentialParameters>& aCryptoParameters,
const ArrayBufferViewOrArrayBuffer& aAttestationChallenge,
const ScopedCredentialOptions& aOptions);
already_AddRefed<Promise>
GetAssertion(nsPIDOMWindowInner* aParent,
const ArrayBufferViewOrArrayBuffer& aAssertionChallenge,
const AssertionOptions& aOptions);
void StartRegister();
void StartSign();
// nsIIPCbackgroundChildCreateCallback methods
void ActorCreated(PBackgroundChild* aActor) override;
void ActorFailed() override;
void ActorDestroyed();
private:
WebAuthnManager();
virtual ~WebAuthnManager();
void MaybeClearTransaction();
already_AddRefed<MozPromise<nsresult, nsresult, false>>
GetOrCreateBackgroundActor();
// JS Promise representing transaction status.
RefPtr<Promise> mTransactionPromise;
// IPC Channel for the current transaction.
RefPtr<WebAuthnTransactionChild> mChild;
// Parent of the context we're currently running the transaction in.
nsCOMPtr<nsPIDOMWindowInner> mCurrentParent;
// Client data, stored on successful transaction creation, so that it can be
// used to assemble reply objects.
Maybe<nsCString> mClientData;
// Holds the parameters of the current transaction, as we need them both
// before the transaction request is sent, and on successful return.
Maybe<WebAuthnTransactionInfo> mInfo;
// Promise for dealing with PBackground Actor creation.
MozPromiseHolder<MozPromise<nsresult, nsresult, false>> mPBackgroundCreationPromise;
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_WebAuthnManager_h

View File

@ -0,0 +1,53 @@
/* -*- 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 "mozilla/dom/WebAuthnTransactionChild.h"
#include "mozilla/dom/WebAuthnManager.h"
namespace mozilla {
namespace dom {
mozilla::ipc::IPCResult
WebAuthnTransactionChild::RecvConfirmRegister(nsTArray<uint8_t>&& aRegBuffer,
nsTArray<uint8_t>&& aSigBuffer)
{
RefPtr<WebAuthnManager> mgr = WebAuthnManager::Get();
MOZ_ASSERT(mgr);
mgr->FinishMakeCredential(aRegBuffer, aSigBuffer);
return IPC_OK();
}
mozilla::ipc::IPCResult
WebAuthnTransactionChild::RecvConfirmSign(nsTArray<uint8_t>&& aCredentialId,
nsTArray<uint8_t>&& aBuffer)
{
RefPtr<WebAuthnManager> mgr = WebAuthnManager::Get();
MOZ_ASSERT(mgr);
mgr->FinishGetAssertion(aCredentialId, aBuffer);
return IPC_OK();
}
mozilla::ipc::IPCResult
WebAuthnTransactionChild::RecvCancel(const nsresult& aError)
{
RefPtr<WebAuthnManager> mgr = WebAuthnManager::Get();
MOZ_ASSERT(mgr);
mgr->Cancel(aError);
return IPC_OK();
}
void
WebAuthnTransactionChild::ActorDestroy(ActorDestroyReason why)
{
RefPtr<WebAuthnManager> mgr = WebAuthnManager::Get();
// This could happen after the WebAuthnManager has been shut down.
if (mgr) {
mgr->ActorDestroyed();
}
}
}
}

View File

@ -0,0 +1,40 @@
/* -*- 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/. */
#ifndef mozilla_dom_WebAuthnTransactionChild_h
#define mozilla_dom_WebAuthnTransactionChild_h
#include "mozilla/dom/PWebAuthnTransactionChild.h"
/*
* Child process IPC implementation for WebAuthn API. Receives results of
* WebAuthn transactions from the parent process, and sends them to the
* WebAuthnManager either cancel the transaction, or be formatted and relayed to
* content.
*/
namespace mozilla {
namespace dom {
class WebAuthnTransactionChild final : public PWebAuthnTransactionChild
{
public:
NS_INLINE_DECL_REFCOUNTING(WebAuthnTransactionChild);
WebAuthnTransactionChild() = default;
mozilla::ipc::IPCResult RecvConfirmRegister(nsTArray<uint8_t>&& aRegBuffer,
nsTArray<uint8_t>&& aSigBuffer) override;
mozilla::ipc::IPCResult RecvConfirmSign(nsTArray<uint8_t>&& aCredentialId,
nsTArray<uint8_t>&& aBuffer) override;
mozilla::ipc::IPCResult RecvCancel(const nsresult& aError) override;
void ActorDestroy(ActorDestroyReason why) override;
private:
~WebAuthnTransactionChild() = default;
};
}
}
#endif //mozilla_dom_WebAuthnTransactionChild_h

View File

@ -0,0 +1,131 @@
/* -*- 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 "mozilla/dom/WebAuthnUtil.h"
#include "pkixutil.h"
namespace mozilla {
namespace dom {
nsresult
ReadToCryptoBuffer(pkix::Reader& aSrc, /* out */ CryptoBuffer& aDest,
uint32_t aLen)
{
if (aSrc.EnsureLength(aLen) != pkix::Success) {
return NS_ERROR_DOM_UNKNOWN_ERR;
}
aDest.ClearAndRetainStorage();
for (uint32_t offset = 0; offset < aLen; ++offset) {
uint8_t b;
if (aSrc.Read(b) != pkix::Success) {
return NS_ERROR_DOM_UNKNOWN_ERR;
}
if (!aDest.AppendElement(b, mozilla::fallible)) {
return NS_ERROR_OUT_OF_MEMORY;
}
}
return NS_OK;
}
nsresult
U2FAssembleAuthenticatorData(/* out */ CryptoBuffer& aAuthenticatorData,
const CryptoBuffer& aRpIdHash,
const CryptoBuffer& aSignatureData)
{
// The AuthenticatorData for U2F devices is the concatenation of the
// RP ID with the output of the U2F Sign operation.
if (aRpIdHash.Length() != 32) {
return NS_ERROR_INVALID_ARG;
}
if (!aAuthenticatorData.AppendElements(aRpIdHash, mozilla::fallible)) {
return NS_ERROR_OUT_OF_MEMORY;
}
if (!aAuthenticatorData.AppendElements(aSignatureData, mozilla::fallible)) {
return NS_ERROR_OUT_OF_MEMORY;
}
return NS_OK;
}
nsresult
U2FDecomposeRegistrationResponse(const CryptoBuffer& aResponse,
/* out */ CryptoBuffer& aPubKeyBuf,
/* out */ CryptoBuffer& aKeyHandleBuf,
/* out */ CryptoBuffer& aAttestationCertBuf,
/* out */ CryptoBuffer& aSignatureBuf)
{
// U2F v1.1 Format via
// http://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html
//
// Bytes Value
// 1 0x05
// 65 public key
// 1 key handle length
// * key handle
// ASN.1 attestation certificate
// * attestation signature
pkix::Input u2fResponse;
u2fResponse.Init(aResponse.Elements(), aResponse.Length());
pkix::Reader input(u2fResponse);
uint8_t b;
if (input.Read(b) != pkix::Success) {
return NS_ERROR_DOM_UNKNOWN_ERR;
}
if (b != 0x05) {
return NS_ERROR_DOM_UNKNOWN_ERR;
}
nsresult rv = ReadToCryptoBuffer(input, aPubKeyBuf, 65);
if (NS_FAILED(rv)) {
return rv;
}
uint8_t handleLen;
if (input.Read(handleLen) != pkix::Success) {
return NS_ERROR_DOM_UNKNOWN_ERR;
}
rv = ReadToCryptoBuffer(input, aKeyHandleBuf, handleLen);
if (NS_FAILED(rv)) {
return rv;
}
// We have to parse the ASN.1 SEQUENCE on the outside to determine the cert's
// length.
pkix::Input cert;
if (pkix::der::ExpectTagAndGetValue(input, pkix::der::SEQUENCE, cert)
!= pkix::Success) {
return NS_ERROR_DOM_UNKNOWN_ERR;
}
pkix::Reader certInput(cert);
rv = ReadToCryptoBuffer(certInput, aAttestationCertBuf, cert.GetLength());
if (NS_FAILED(rv)) {
return rv;
}
// The remainder of u2fResponse is the signature
pkix::Input u2fSig;
input.SkipToEnd(u2fSig);
pkix::Reader sigInput(u2fSig);
rv = ReadToCryptoBuffer(sigInput, aSignatureBuf, u2fSig.GetLength());
if (NS_FAILED(rv)) {
return rv;
}
return NS_OK;
}
}
}

View File

@ -0,0 +1,38 @@
/* -*- 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/. */
#ifndef mozilla_dom_WebAuthnUtil_h
#define mozilla_dom_WebAuthnUtil_h
/*
* Utility functions used by both WebAuthnManager and U2FTokenManager.
*/
#include "mozilla/dom/CryptoBuffer.h"
#include "pkix/Input.h"
namespace mozilla {
namespace dom {
nsresult
U2FAssembleAuthenticatorData(/* out */ CryptoBuffer& aAuthenticatorData,
const CryptoBuffer& aRpIdHash,
const CryptoBuffer& aSignatureData);
nsresult
U2FDecomposeRegistrationResponse(const CryptoBuffer& aResponse,
/* out */ CryptoBuffer& aPubKeyBuf,
/* out */ CryptoBuffer& aKeyHandleBuf,
/* out */ CryptoBuffer& aAttestationCertBuf,
/* out */ CryptoBuffer& aSignatureBuf);
nsresult
ReadToCryptoBuffer(pkix::Reader& aSrc, /* out */ CryptoBuffer& aDest,
uint32_t aLen);
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_WebAuthnUtil_h

View File

@ -18,7 +18,10 @@ EXPORTS.mozilla.dom += [
'WebAuthentication.h',
'WebAuthnAssertion.h',
'WebAuthnAttestation.h',
'WebAuthnManager.h',
'WebAuthnRequest.h',
'WebAuthnTransactionChild.h',
'WebAuthnUtil.h'
]
UNIFIED_SOURCES += [
@ -28,6 +31,9 @@ UNIFIED_SOURCES += [
'WebAuthentication.cpp',
'WebAuthnAssertion.cpp',
'WebAuthnAttestation.cpp',
'WebAuthnManager.cpp',
'WebAuthnTransactionChild.cpp',
'WebAuthnUtil.cpp'
]
include('/ipc/chromium/chromium-config.mozbuild')

View File

@ -37,6 +37,7 @@
#include "mozilla/layout/VsyncChild.h"
#include "mozilla/net/PUDPSocketChild.h"
#include "mozilla/dom/network/UDPSocketChild.h"
#include "mozilla/dom/WebAuthnTransactionChild.h"
#include "nsID.h"
#include "nsTraceRefcnt.h"
@ -78,6 +79,8 @@ using mozilla::dom::cache::PCacheChild;
using mozilla::dom::cache::PCacheStorageChild;
using mozilla::dom::cache::PCacheStreamControlChild;
using mozilla::dom::WebAuthnTransactionChild;
// -----------------------------------------------------------------------------
// BackgroundChildImpl::ThreadLocal
// -----------------------------------------------------------------------------
@ -560,6 +563,22 @@ BackgroundChildImpl::OnChannelReceivedMessage(const Message& aMsg)
}
#endif
dom::PWebAuthnTransactionChild*
BackgroundChildImpl::AllocPWebAuthnTransactionChild()
{
MOZ_CRASH("PWebAuthnTransaction actor should be manually constructed!");
return nullptr;
}
bool
BackgroundChildImpl::DeallocPWebAuthnTransactionChild(PWebAuthnTransactionChild* aActor)
{
MOZ_ASSERT(aActor);
RefPtr<dom::WebAuthnTransactionChild> child =
dont_AddRef(static_cast<dom::WebAuthnTransactionChild*>(aActor));
return true;
}
} // namespace ipc
} // namespace mozilla

View File

@ -202,6 +202,12 @@ protected:
virtual void
OnChannelReceivedMessage(const Message& aMsg) override;
#endif
virtual PWebAuthnTransactionChild*
AllocPWebAuthnTransactionChild() override;
virtual bool
DeallocPWebAuthnTransactionChild(PWebAuthnTransactionChild* aActor) override;
};
class BackgroundChildImpl::ThreadLocal final