fune/dom/webauthn/WebAuthnManager.cpp
Tim Taubert 6ded2cf370 Bug 1430150 - Implement WebAuthentication permission prompts r=jcj,johannh
Reviewers: jcj, johannh

Reviewed By: jcj, johannh

Bug #: 1430150

Differential Revision: https://phabricator.services.mozilla.com/D638

--HG--
rename : dom/webauthn/tests/cbor/cbor.js => dom/webauthn/tests/cbor.js
extra : amend_source : f137c2aebea5797cd07d4500cb71c2b9be4e4844
2018-03-11 18:47:14 +01:00

917 lines
29 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: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "WebAuthnCoseIdentifiers.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/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
const uint8_t FLAG_UV = 0x04; // User was Verified (biometrics, etc.); this
// flag is not possible with U2F devices
/***********************************************************************
* Statics
**********************************************************************/
namespace {
static mozilla::LazyLogModule gWebAuthnManagerLog("webauthnmanager");
}
NS_IMPL_ISUPPORTS(WebAuthnManager, nsIDOMEventListener);
/***********************************************************************
* Utility Functions
**********************************************************************/
static nsresult
AssembleClientData(const nsAString& aOrigin,
const CryptoBuffer& aChallenge,
const nsAString& aType,
const AuthenticationExtensionsClientInputs& aExtensions,
/* 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.mType.Assign(aType);
clientDataObject.mChallenge.Assign(challengeBase64);
clientDataObject.mOrigin.Assign(aOrigin);
clientDataObject.mHashAlgorithm.AssignLiteral(u"SHA-256");
clientDataObject.mClientExtensions = aExtensions;
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;
}
/***********************************************************************
* WebAuthnManager Implementation
**********************************************************************/
void
WebAuthnManager::ClearTransaction()
{
if (!NS_WARN_IF(mTransaction.isNothing())) {
StopListeningForVisibilityEvents();
}
mTransaction.reset();
Unfollow();
}
void
WebAuthnManager::RejectTransaction(const nsresult& aError)
{
if (!NS_WARN_IF(mTransaction.isNothing())) {
mTransaction.ref().mPromise->MaybeReject(aError);
}
ClearTransaction();
}
void
WebAuthnManager::CancelTransaction(const nsresult& aError)
{
if (!NS_WARN_IF(!mChild || mTransaction.isNothing())) {
mChild->SendRequestCancel(mTransaction.ref().mId);
}
RejectTransaction(aError);
}
WebAuthnManager::~WebAuthnManager()
{
MOZ_ASSERT(NS_IsMainThread());
if (mTransaction.isSome()) {
RejectTransaction(NS_ERROR_ABORT);
}
if (mChild) {
RefPtr<WebAuthnTransactionChild> c;
mChild.swap(c);
c->Disconnect();
}
}
already_AddRefed<Promise>
WebAuthnManager::MakeCredential(const PublicKeyCredentialCreationOptions& aOptions,
const Optional<OwningNonNull<AbortSignal>>& aSignal)
{
MOZ_ASSERT(NS_IsMainThread());
if (mTransaction.isSome()) {
CancelTransaction(NS_ERROR_ABORT);
}
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
ErrorResult rv;
RefPtr<Promise> promise = Promise::Create(global, rv);
if (rv.Failed()) {
return nullptr;
}
// Abort the request if aborted flag is already set.
if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
return promise.forget();
}
nsString origin;
nsCString rpId;
rv = GetOrigin(mParent, origin, rpId);
if (NS_WARN_IF(rv.Failed())) {
promise->MaybeReject(rv);
return promise.forget();
}
// Enforce 5.4.3 User Account Parameters for Credential Generation
// When we add UX, we'll want to do more with this value, but for now
// we just have to verify its correctness.
{
CryptoBuffer userId;
userId.Assign(aOptions.mUser.mId);
if (userId.Length() > 64) {
promise->MaybeReject(NS_ERROR_DOM_TYPE_ERR);
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(mParent, aOptions.mRp.mId.Value(), rpId))) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
}
// <https://w3c.github.io/webauthn/#sctn-appid-extension>
if (aOptions.mExtensions.mAppid.WasPassed()) {
promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_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();
}
// TODO: Move this logic into U2FTokenManager in Bug 1409220.
// Process each element of mPubKeyCredParams using the following steps, to
// produce a new sequence acceptableParams.
nsTArray<PublicKeyCredentialParameters> acceptableParams;
for (size_t a = 0; a < aOptions.mPubKeyCredParams.Length(); ++a) {
// Let current be the currently selected element of
// mPubKeyCredParams.
// If current.type does not contain a PublicKeyCredentialType
// supported by this implementation, then stop processing current and move
// on to the next element in mPubKeyCredParams.
if (aOptions.mPubKeyCredParams[a].mType != PublicKeyCredentialType::Public_key) {
continue;
}
nsString algName;
if (NS_FAILED(CoseAlgorithmToWebCryptoId(aOptions.mPubKeyCredParams[a].mAlg,
algName))) {
continue;
}
if (!acceptableParams.AppendElement(aOptions.mPubKeyCredParams[a],
mozilla::fallible)){
promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
return promise.forget();
}
}
// If acceptableParams is empty and mPubKeyCredParams was not empty, cancel
// the timer started in step 2, reject promise with a DOMException whose name
// is "NotSupportedError", and terminate this algorithm.
if (acceptableParams.IsEmpty() && !aOptions.mPubKeyCredParams.IsEmpty()) {
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,
NS_LITERAL_STRING("webauthn.create"),
aOptions.mExtensions, 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<WebAuthnScopedCredential> excludeList;
for (const auto& s: aOptions.mExcludeCredentials) {
WebAuthnScopedCredential c;
CryptoBuffer cb;
cb.Assign(s.mId);
c.id() = cb;
excludeList.AppendElement(c);
}
if (!MaybeCreateBackgroundActor()) {
promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
return promise.forget();
}
// TODO: Add extension list building
nsTArray<WebAuthnExtension> extensions;
const auto& selection = aOptions.mAuthenticatorSelection;
const auto& attachment = selection.mAuthenticatorAttachment;
const AttestationConveyancePreference& attestation = aOptions.mAttestation;
// Does the RP require attachment == "platform"?
bool requirePlatformAttachment =
attachment.WasPassed() && attachment.Value() == AuthenticatorAttachment::Platform;
// Does the RP require user verification?
bool requireUserVerification =
selection.mUserVerification == UserVerificationRequirement::Required;
// Does the RP desire direct attestation? Indirect attestation is not
// implemented, and thus is equivilent to None.
bool requestDirectAttestation =
attestation == AttestationConveyancePreference::Direct;
// Create and forward authenticator selection criteria.
WebAuthnAuthenticatorSelection authSelection(selection.mRequireResidentKey,
requireUserVerification,
requirePlatformAttachment);
WebAuthnMakeCredentialInfo info(origin,
rpIdHash,
clientDataHash,
adjustedTimeout,
excludeList,
extensions,
authSelection,
requestDirectAttestation);
ListenForVisibilityEvents();
AbortSignal* signal = nullptr;
if (aSignal.WasPassed()) {
signal = &aSignal.Value();
Follow(signal);
}
MOZ_ASSERT(mTransaction.isNothing());
mTransaction = Some(WebAuthnTransaction(promise,
rpIdHash,
clientDataJSON,
requestDirectAttestation,
signal));
mChild->SendRequestRegister(mTransaction.ref().mId, info);
return promise.forget();
}
already_AddRefed<Promise>
WebAuthnManager::GetAssertion(const PublicKeyCredentialRequestOptions& aOptions,
const Optional<OwningNonNull<AbortSignal>>& aSignal)
{
MOZ_ASSERT(NS_IsMainThread());
if (mTransaction.isSome()) {
CancelTransaction(NS_ERROR_ABORT);
}
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
ErrorResult rv;
RefPtr<Promise> promise = Promise::Create(global, rv);
if (rv.Failed()) {
return nullptr;
}
// Abort the request if aborted flag is already set.
if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
return promise.forget();
}
nsString origin;
nsCString rpId;
rv = GetOrigin(mParent, 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(mParent, 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, NS_LITERAL_STRING("webauthn.get"),
aOptions.mExtensions, 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<WebAuthnScopedCredential> allowList;
for (const auto& s: aOptions.mAllowCredentials) {
if (s.mType == PublicKeyCredentialType::Public_key) {
WebAuthnScopedCredential c;
CryptoBuffer cb;
cb.Assign(s.mId);
c.id() = cb;
// Serialize transports.
if (s.mTransports.WasPassed()) {
uint8_t transports = 0;
for (const auto& t: s.mTransports.Value()) {
if (t == AuthenticatorTransport::Usb) {
transports |= U2F_AUTHENTICATOR_TRANSPORT_USB;
}
if (t == AuthenticatorTransport::Nfc) {
transports |= U2F_AUTHENTICATOR_TRANSPORT_NFC;
}
if (t == AuthenticatorTransport::Ble) {
transports |= U2F_AUTHENTICATOR_TRANSPORT_BLE;
}
}
c.transports() = transports;
}
allowList.AppendElement(c);
}
}
if (!MaybeCreateBackgroundActor()) {
promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
return promise.forget();
}
// Does the RP require user verification?
bool requireUserVerification =
aOptions.mUserVerification == UserVerificationRequirement::Required;
// If extensions were 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;
// <https://w3c.github.io/webauthn/#sctn-appid-extension>
if (aOptions.mExtensions.mAppid.WasPassed()) {
nsString appId(aOptions.mExtensions.mAppid.Value());
// Check that the appId value is allowed.
if (!EvaluateAppID(mParent, origin, U2FOperation::Sign, appId)) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
CryptoBuffer appIdHash;
if (!appIdHash.SetLength(SHA256_LENGTH, fallible)) {
promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
return promise.forget();
}
// We need the SHA-256 hash of the appId.
nsresult srv = HashCString(hashService, NS_ConvertUTF16toUTF8(appId), appIdHash);
if (NS_WARN_IF(NS_FAILED(srv))) {
promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
return promise.forget();
}
// Append the hash and send it to the backend.
extensions.AppendElement(WebAuthnExtensionAppId(appIdHash));
}
WebAuthnGetAssertionInfo info(origin,
rpIdHash,
clientDataHash,
adjustedTimeout,
allowList,
requireUserVerification,
extensions);
ListenForVisibilityEvents();
AbortSignal* signal = nullptr;
if (aSignal.WasPassed()) {
signal = &aSignal.Value();
Follow(signal);
}
MOZ_ASSERT(mTransaction.isNothing());
mTransaction = Some(WebAuthnTransaction(promise,
rpIdHash,
clientDataJSON,
false /* aDirectAttestation */,
signal));
mChild->SendRequestSign(mTransaction.ref().mId, info);
return promise.forget();
}
already_AddRefed<Promise>
WebAuthnManager::Store(const Credential& aCredential)
{
MOZ_ASSERT(NS_IsMainThread());
if (mTransaction.isSome()) {
CancelTransaction(NS_ERROR_ABORT);
}
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
ErrorResult rv;
RefPtr<Promise> promise = Promise::Create(global, rv);
if (rv.Failed()) {
return nullptr;
}
promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
return promise.forget();
}
void
WebAuthnManager::FinishMakeCredential(const uint64_t& aTransactionId,
const WebAuthnMakeCredentialResult& aResult)
{
MOZ_ASSERT(NS_IsMainThread());
// Check for a valid transaction.
if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
return;
}
CryptoBuffer regData;
if (NS_WARN_IF(!regData.Assign(aResult.RegBuffer().Elements(),
aResult.RegBuffer().Length()))) {
RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
return;
}
mozilla::dom::CryptoBuffer aaguidBuf;
if (NS_WARN_IF(!aaguidBuf.SetCapacity(16, mozilla::fallible))) {
RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
return;
}
// FIDO U2F devices have no AAGUIDs, so they'll be all zeros until we add
// support for CTAP2 devices.
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))) {
RejectTransaction(rv);
return;
}
MOZ_ASSERT(keyHandleBuf.Length() <= 0xFFFF);
nsAutoString keyHandleBase64Url;
rv = keyHandleBuf.ToJwkBase64(keyHandleBase64Url);
if (NS_WARN_IF(NS_FAILED(rv))) {
RejectTransaction(rv);
return;
}
CryptoBuffer clientDataBuf;
if (!clientDataBuf.Assign(mTransaction.ref().mClientData)) {
RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
return;
}
CryptoBuffer rpIdHashBuf;
if (!rpIdHashBuf.Assign(mTransaction.ref().mRpIdHash)) {
RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
return;
}
// Construct the public key object
CryptoBuffer pubKeyObj;
rv = CBOREncodePublicKeyObj(pubKeyBuf, pubKeyObj);
if (NS_FAILED(rv)) {
RejectTransaction(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))) {
RejectTransaction(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)) {
RejectTransaction(rv);
return;
}
mozilla::dom::CryptoBuffer authDataBuf;
rv = AssembleAuthenticatorData(rpIdHashBuf, FLAG_TUP, counterBuf, attDataBuf,
authDataBuf);
if (NS_FAILED(rv)) {
RejectTransaction(rv);
return;
}
// Direct attestation might have been requested by the RP. This will
// be true only if the user consented via the permission UI.
CryptoBuffer attObj;
if (aResult.DirectAttestationPermitted()) {
rv = CBOREncodeFidoU2FAttestationObj(authDataBuf, attestationCertBuf,
signatureBuf, attObj);
} else {
rv = CBOREncodeNoneAttestationObj(authDataBuf, attObj);
}
if (NS_FAILED(rv)) {
RejectTransaction(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(mParent);
attestation->SetClientDataJSON(clientDataBuf);
attestation->SetAttestationObject(attObj);
RefPtr<PublicKeyCredential> credential =
new PublicKeyCredential(mParent);
credential->SetId(keyHandleBase64Url);
credential->SetType(NS_LITERAL_STRING("public-key"));
credential->SetRawId(keyHandleBuf);
credential->SetResponse(attestation);
mTransaction.ref().mPromise->MaybeResolve(credential);
ClearTransaction();
}
void
WebAuthnManager::FinishGetAssertion(const uint64_t& aTransactionId,
const WebAuthnGetAssertionResult& aResult)
{
MOZ_ASSERT(NS_IsMainThread());
// Check for a valid transaction.
if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
return;
}
CryptoBuffer tokenSignatureData;
if (NS_WARN_IF(!tokenSignatureData.Assign(aResult.SigBuffer().Elements(),
aResult.SigBuffer().Length()))) {
RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
return;
}
CryptoBuffer clientDataBuf;
if (!clientDataBuf.Assign(mTransaction.ref().mClientData)) {
RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
return;
}
CryptoBuffer rpIdHashBuf;
if (!rpIdHashBuf.Assign(aResult.RpIdHash())) {
RejectTransaction(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))) {
RejectTransaction(rv);
return;
}
CryptoBuffer attestationDataBuf;
CryptoBuffer authenticatorDataBuf;
rv = AssembleAuthenticatorData(rpIdHashBuf, FLAG_TUP, counterBuf,
/* deliberately empty */ attestationDataBuf,
authenticatorDataBuf);
if (NS_WARN_IF(NS_FAILED(rv))) {
RejectTransaction(rv);
return;
}
CryptoBuffer credentialBuf;
if (!credentialBuf.Assign(aResult.CredentialID())) {
RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
return;
}
nsAutoString credentialBase64Url;
rv = credentialBuf.ToJwkBase64(credentialBase64Url);
if (NS_WARN_IF(NS_FAILED(rv))) {
RejectTransaction(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(mParent);
assertion->SetClientDataJSON(clientDataBuf);
assertion->SetAuthenticatorData(authenticatorDataBuf);
assertion->SetSignature(signatureBuf);
RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mParent);
credential->SetId(credentialBase64Url);
credential->SetType(NS_LITERAL_STRING("public-key"));
credential->SetRawId(credentialBuf);
credential->SetResponse(assertion);
// Forward client extension results.
for (auto& ext: aResult.Extensions()) {
if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultAppId) {
bool appid = ext.get_WebAuthnExtensionResultAppId().AppId();
credential->SetClientExtensionResultAppId(appid);
}
}
mTransaction.ref().mPromise->MaybeResolve(credential);
ClearTransaction();
}
void
WebAuthnManager::RequestAborted(const uint64_t& aTransactionId,
const nsresult& aError)
{
MOZ_ASSERT(NS_IsMainThread());
if (mTransaction.isSome() && mTransaction.ref().mId == aTransactionId) {
RejectTransaction(aError);
}
}
void
WebAuthnManager::Abort()
{
CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
}
}
}