summaryrefslogtreecommitdiffstats
path: root/dom/webauthn/U2FHIDTokenManager.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webauthn/U2FHIDTokenManager.cpp')
-rw-r--r--dom/webauthn/U2FHIDTokenManager.cpp419
1 files changed, 419 insertions, 0 deletions
diff --git a/dom/webauthn/U2FHIDTokenManager.cpp b/dom/webauthn/U2FHIDTokenManager.cpp
new file mode 100644
index 0000000000..3b37347c53
--- /dev/null
+++ b/dom/webauthn/U2FHIDTokenManager.cpp
@@ -0,0 +1,419 @@
+/* -*- 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, Nothing(), 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.InfallibleClone());
+
+ Maybe<nsTArray<uint8_t>> appIdHashExt = Nothing();
+
+ 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) {
+ appIdHashExt = Some(ext.get_WebAuthnExtensionAppId().AppId().Clone());
+ appIds.AppendElement(appIdHashExt->Clone());
+ }
+ }
+ }
+
+ 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, std::move(rpIdHash), std::move(appIdHashExt),
+ 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;
+ }
+
+ nsTArray<WebAuthnExtensionResult> extensions;
+ WebAuthnMakeCredentialResult result(mTransaction.ref().mClientDataJSON,
+ attObj, keyHandle, regData, extensions);
+ 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> hashChosenByAuthenticator;
+ if (!aResult->CopyAppId(hashChosenByAuthenticator)) {
+ 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 (mTransaction.ref().mAppIdHash.isSome()) {
+ bool usedAppId =
+ (hashChosenByAuthenticator == mTransaction.ref().mAppIdHash.ref());
+ extensions.AppendElement(WebAuthnExtensionResultAppId(usedAppId));
+ }
+
+ 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(hashChosenByAuthenticator)) {
+ 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;
+ }
+
+ nsTArray<uint8_t> userHandle;
+
+ WebAuthnGetAssertionResult result(mTransaction.ref().mClientDataJSON,
+ keyHandle, signatureBuf, authenticatorData,
+ extensions, rawSignatureBuf, userHandle);
+ mSignPromise.Resolve(std::move(result), __func__);
+}
+
+} // namespace dom
+} // namespace mozilla