dom/webauthn/U2FHIDTokenManager.cpp
author Alex Gaynor <agaynor@mozilla.com>
Tue, 19 Mar 2019 23:25:47 +0000
changeset 465256 e516a5f9e905add224dc3bba0efd39448758ddf4
parent 465255 a41f369384368b0863dded39fb9f308ad35f1df6
child 468178 8855bf5ed33f745cadfbd59870e997cae6f3d2ca
permissions -rw-r--r--
Bug 1536097 - Part 4 - convert UserVerificationRequirement to use ParamTraits for deserialization; r=jcj Depends on D24064 Differential Revision: https://phabricator.services.mozilla.com/D24065

/* -*- 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 "WebAuthnCoseIdentifiers.h"
#include "mozilla/dom/U2FHIDTokenManager.h"
#include "mozilla/dom/WebAuthnUtil.h"
#include "mozilla/ipc/BackgroundParent.h"
#include "mozilla/StaticMutex.h"

namespace mozilla {
namespace dom {

static StaticMutex gInstanceMutex;
static U2FHIDTokenManager* gInstance;
static nsIThread* gPBackgroundThread;

static void u2f_register_callback(uint64_t aTransactionId,
                                  rust_u2f_result* aResult) {
  UniquePtr<U2FResult> rv = MakeUnique<U2FResult>(aTransactionId, aResult);

  StaticMutexAutoLock lock(gInstanceMutex);
  if (!gInstance || NS_WARN_IF(!gPBackgroundThread)) {
    return;
  }

  nsCOMPtr<nsIRunnable> r(NewRunnableMethod<UniquePtr<U2FResult>&&>(
      "U2FHIDTokenManager::HandleRegisterResult", gInstance,
      &U2FHIDTokenManager::HandleRegisterResult, std::move(rv)));

  MOZ_ALWAYS_SUCCEEDS(
      gPBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL));
}

static void u2f_sign_callback(uint64_t aTransactionId,
                              rust_u2f_result* aResult) {
  UniquePtr<U2FResult> rv = MakeUnique<U2FResult>(aTransactionId, aResult);

  StaticMutexAutoLock lock(gInstanceMutex);
  if (!gInstance || NS_WARN_IF(!gPBackgroundThread)) {
    return;
  }

  nsCOMPtr<nsIRunnable> r(NewRunnableMethod<UniquePtr<U2FResult>&&>(
      "U2FHIDTokenManager::HandleSignResult", gInstance,
      &U2FHIDTokenManager::HandleSignResult, std::move(rv)));

  MOZ_ALWAYS_SUCCEEDS(
      gPBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL));
}

U2FHIDTokenManager::U2FHIDTokenManager() {
  StaticMutexAutoLock lock(gInstanceMutex);
  mozilla::ipc::AssertIsOnBackgroundThread();
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(!gInstance);

  mU2FManager = rust_u2f_mgr_new();
  gPBackgroundThread = NS_GetCurrentThread();
  MOZ_ASSERT(gPBackgroundThread, "This should never be null!");
  gInstance = this;
}

void U2FHIDTokenManager::Drop() {
  {
    StaticMutexAutoLock lock(gInstanceMutex);
    mozilla::ipc::AssertIsOnBackgroundThread();

    mRegisterPromise.RejectIfExists(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    mSignPromise.RejectIfExists(NS_ERROR_DOM_UNKNOWN_ERR, __func__);

    gInstance = nullptr;
  }

  // Release gInstanceMutex before we call U2FManager::drop(). It will wait
  // for the work queue thread to join, and that requires the
  // u2f_{register,sign}_callback to lock and return.
  rust_u2f_mgr_free(mU2FManager);
  mU2FManager = nullptr;

  // Reset transaction ID so that queued runnables exit early.
  mTransaction.reset();
}

// A U2F Register operation causes a new key pair to be generated by the token.
// The token then returns the public key of the key pair, and a handle to the
// private key, which is a fancy way of saying "key wrapped private key", as
// well as the generated attestation certificate and a signature using that
// certificate's private key.
//
// The KeyHandleFromPrivateKey and PrivateKeyFromKeyHandle methods perform
// the actual key wrap/unwrap operations.
//
// The format of the return registration data is as follows:
//
// Bytes  Value
// 1      0x05
// 65     public key
// 1      key handle length
// *      key handle
// ASN.1  attestation certificate
// *      attestation signature
//
RefPtr<U2FRegisterPromise> U2FHIDTokenManager::Register(
    const WebAuthnMakeCredentialInfo& aInfo, bool aForceNoneAttestation) {
  mozilla::ipc::AssertIsOnBackgroundThread();

  uint64_t registerFlags = 0;

  if (aInfo.Extra().isSome()) {
    const auto& extra = aInfo.Extra().ref();
    const WebAuthnAuthenticatorSelection& sel = extra.AuthenticatorSelection();

    UserVerificationRequirement userVerificaitonRequirement =
        sel.userVerificationRequirement();

    bool requireUserVerification =
        userVerificaitonRequirement == UserVerificationRequirement::Required;

    bool requirePlatformAttachment = false;
    if (sel.authenticatorAttachment().isSome()) {
      const AuthenticatorAttachment authenticatorAttachment =
          sel.authenticatorAttachment().value();
      if (authenticatorAttachment == AuthenticatorAttachment::Platform) {
        requirePlatformAttachment = true;
      }
    }

    // Set flags for credential creation.
    if (sel.requireResidentKey()) {
      registerFlags |= U2F_FLAG_REQUIRE_RESIDENT_KEY;
    }
    if (requireUserVerification) {
      registerFlags |= U2F_FLAG_REQUIRE_USER_VERIFICATION;
    }
    if (requirePlatformAttachment) {
      registerFlags |= U2F_FLAG_REQUIRE_PLATFORM_ATTACHMENT;
    }

    nsTArray<CoseAlg> coseAlgos;
    for (const auto& coseAlg : extra.coseAlgs()) {
      switch (static_cast<CoseAlgorithmIdentifier>(coseAlg.alg())) {
        case CoseAlgorithmIdentifier::ES256:
          coseAlgos.AppendElement(coseAlg);
          break;
        default:
          continue;
      }
    }

    // Only if no algorithms were specified, default to the only CTAP 1 / U2F
    // protocol-supported algorithm. Ultimately this logic must move into
    // u2f-hid-rs in a fashion that doesn't break the tests.
    if (extra.coseAlgs().IsEmpty()) {
      coseAlgos.AppendElement(
          static_cast<int32_t>(CoseAlgorithmIdentifier::ES256));
    }

    // If there are no acceptable/supported algorithms, reject the promise.
    if (coseAlgos.IsEmpty()) {
      return U2FRegisterPromise::CreateAndReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR,
                                                 __func__);
    }
  }

  CryptoBuffer rpIdHash, clientDataHash;
  NS_ConvertUTF16toUTF8 rpId(aInfo.RpId());
  nsresult rv = BuildTransactionHashes(rpId, aInfo.ClientDataJSON(), rpIdHash,
                                       clientDataHash);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return U2FRegisterPromise::CreateAndReject(NS_ERROR_DOM_UNKNOWN_ERR,
                                               __func__);
  }

  ClearPromises();
  mTransaction.reset();
  uint64_t tid = rust_u2f_mgr_register(
      mU2FManager, registerFlags, (uint64_t)aInfo.TimeoutMS(),
      u2f_register_callback, clientDataHash.Elements(), clientDataHash.Length(),
      rpIdHash.Elements(), rpIdHash.Length(),
      U2FKeyHandles(aInfo.ExcludeList()).Get());

  if (tid == 0) {
    return U2FRegisterPromise::CreateAndReject(NS_ERROR_DOM_UNKNOWN_ERR,
                                               __func__);
  }

  mTransaction = Some(Transaction(tid, rpIdHash, aInfo.ClientDataJSON(),
                                  aForceNoneAttestation));

  return mRegisterPromise.Ensure(__func__);
}

// A U2F Sign operation creates a signature over the "param" arguments (plus
// some other stuff) using the private key indicated in the key handle argument.
//
// The format of the signed data is as follows:
//
//  32    Application parameter
//  1     User presence (0x01)
//  4     Counter
//  32    Challenge parameter
//
// The format of the signature data is as follows:
//
//  1     User presence
//  4     Counter
//  *     Signature
//
RefPtr<U2FSignPromise> U2FHIDTokenManager::Sign(
    const WebAuthnGetAssertionInfo& aInfo) {
  mozilla::ipc::AssertIsOnBackgroundThread();

  CryptoBuffer rpIdHash, clientDataHash;
  NS_ConvertUTF16toUTF8 rpId(aInfo.RpId());
  nsresult rv = BuildTransactionHashes(rpId, aInfo.ClientDataJSON(), rpIdHash,
                                       clientDataHash);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return U2FSignPromise::CreateAndReject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
  }

  uint64_t signFlags = 0;
  nsTArray<nsTArray<uint8_t>> appIds;
  appIds.AppendElement(rpIdHash);

  if (aInfo.Extra().isSome()) {
    const auto& extra = aInfo.Extra().ref();

    UserVerificationRequirement userVerificaitonReq =
        extra.userVerificationRequirement();

    // Set flags for credential requests.
    if (userVerificaitonReq == UserVerificationRequirement::Required) {
      signFlags |= U2F_FLAG_REQUIRE_USER_VERIFICATION;
    }

    // Process extensions.
    for (const WebAuthnExtension& ext : extra.Extensions()) {
      if (ext.type() == WebAuthnExtension::TWebAuthnExtensionAppId) {
        appIds.AppendElement(ext.get_WebAuthnExtensionAppId().AppId());
      }
    }
  }

  ClearPromises();
  mTransaction.reset();
  uint64_t tid = rust_u2f_mgr_sign(
      mU2FManager, signFlags, (uint64_t)aInfo.TimeoutMS(), u2f_sign_callback,
      clientDataHash.Elements(), clientDataHash.Length(),
      U2FAppIds(appIds).Get(), U2FKeyHandles(aInfo.AllowList()).Get());
  if (tid == 0) {
    return U2FSignPromise::CreateAndReject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
  }

  mTransaction = Some(Transaction(tid, rpIdHash, aInfo.ClientDataJSON()));

  return mSignPromise.Ensure(__func__);
}

void U2FHIDTokenManager::Cancel() {
  mozilla::ipc::AssertIsOnBackgroundThread();

  ClearPromises();
  rust_u2f_mgr_cancel(mU2FManager);
  mTransaction.reset();
}

void U2FHIDTokenManager::HandleRegisterResult(UniquePtr<U2FResult>&& aResult) {
  mozilla::ipc::AssertIsOnBackgroundThread();

  if (mTransaction.isNothing() ||
      aResult->GetTransactionId() != mTransaction.ref().mId) {
    return;
  }

  MOZ_ASSERT(!mRegisterPromise.IsEmpty());

  if (aResult->IsError()) {
    mRegisterPromise.Reject(aResult->GetError(), __func__);
    return;
  }

  nsTArray<uint8_t> registration;
  if (!aResult->CopyRegistration(registration)) {
    mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  // Decompose the U2F registration packet
  CryptoBuffer pubKeyBuf;
  CryptoBuffer keyHandle;
  CryptoBuffer attestationCertBuf;
  CryptoBuffer signatureBuf;

  CryptoBuffer regData;
  regData.Assign(registration);

  // Only handles attestation cert chains of length=1.
  nsresult rv = U2FDecomposeRegistrationResponse(
      regData, pubKeyBuf, keyHandle, attestationCertBuf, signatureBuf);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  CryptoBuffer rpIdHashBuf;
  if (!rpIdHashBuf.Assign(mTransaction.ref().mRpIdHash)) {
    mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  CryptoBuffer attObj;
  rv = AssembleAttestationObject(
      rpIdHashBuf, pubKeyBuf, keyHandle, attestationCertBuf, signatureBuf,
      mTransaction.ref().mForceNoneAttestation, attObj);
  if (NS_FAILED(rv)) {
    mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  WebAuthnMakeCredentialResult result(mTransaction.ref().mClientDataJSON,
                                      attObj, keyHandle, regData);
  mRegisterPromise.Resolve(std::move(result), __func__);
}

void U2FHIDTokenManager::HandleSignResult(UniquePtr<U2FResult>&& aResult) {
  mozilla::ipc::AssertIsOnBackgroundThread();

  if (mTransaction.isNothing() ||
      aResult->GetTransactionId() != mTransaction.ref().mId) {
    return;
  }

  MOZ_ASSERT(!mSignPromise.IsEmpty());

  if (aResult->IsError()) {
    mSignPromise.Reject(aResult->GetError(), __func__);
    return;
  }

  nsTArray<uint8_t> appId;
  if (!aResult->CopyAppId(appId)) {
    mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  nsTArray<uint8_t> keyHandle;
  if (!aResult->CopyKeyHandle(keyHandle)) {
    mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  nsTArray<uint8_t> signature;
  if (!aResult->CopySignature(signature)) {
    mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  CryptoBuffer rawSignatureBuf;
  if (!rawSignatureBuf.Assign(signature)) {
    mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  nsTArray<WebAuthnExtensionResult> extensions;

  if (appId != mTransaction.ref().mRpIdHash) {
    // Indicate to the RP that we used the FIDO appId.
    extensions.AppendElement(WebAuthnExtensionResultAppId(true));
  }

  CryptoBuffer signatureBuf;
  CryptoBuffer counterBuf;
  uint8_t flags = 0;
  nsresult rv = U2FDecomposeSignResponse(rawSignatureBuf, flags, counterBuf,
                                         signatureBuf);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  CryptoBuffer chosenAppIdBuf;
  if (!chosenAppIdBuf.Assign(appId)) {
    mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  // Preserve the two LSBs of the flags byte, UP and RFU1.
  // See <https://github.com/fido-alliance/fido-2-specs/pull/519>
  flags &= 0b11;

  CryptoBuffer emptyAttestationData;
  CryptoBuffer authenticatorData;
  rv = AssembleAuthenticatorData(chosenAppIdBuf, flags, counterBuf,
                                 emptyAttestationData, authenticatorData);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  WebAuthnGetAssertionResult result(mTransaction.ref().mClientDataJSON,
                                    keyHandle, signatureBuf, authenticatorData,
                                    extensions, rawSignatureBuf);
  mSignPromise.Resolve(std::move(result), __func__);
}

}  // namespace dom
}  // namespace mozilla