summaryrefslogtreecommitdiffstats
path: root/dom/webauthn/U2FSoftTokenTransport.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /dom/webauthn/U2FSoftTokenTransport.cpp
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/webauthn/U2FSoftTokenTransport.cpp')
-rw-r--r--dom/webauthn/U2FSoftTokenTransport.cpp1031
1 files changed, 1031 insertions, 0 deletions
diff --git a/dom/webauthn/U2FSoftTokenTransport.cpp b/dom/webauthn/U2FSoftTokenTransport.cpp
new file mode 100644
index 0000000000..3b2f2a6231
--- /dev/null
+++ b/dom/webauthn/U2FSoftTokenTransport.cpp
@@ -0,0 +1,1031 @@
+/* -*- 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 "CtapResults.h"
+#include "WebAuthnCoseIdentifiers.h"
+#include "WebAuthnEnumStrings.h"
+#include "U2FSoftTokenTransport.h"
+#include "mozilla/dom/WebAuthnUtil.h"
+#include "CryptoBuffer.h"
+#include "mozilla/Base64.h"
+#include "mozilla/Casting.h"
+#include "mozilla/Preferences.h"
+#include "nsNSSComponent.h"
+#include "nsThreadUtils.h"
+#include "pk11pub.h"
+#include "prerror.h"
+#include "secerr.h"
+#include "WebCryptoCommon.h"
+
+#define PREF_U2F_NSSTOKEN_COUNTER "security.webauth.softtoken_counter"
+
+namespace mozilla::dom {
+
+using namespace mozilla;
+using mozilla::dom::CreateECParamsForCurve;
+
+const nsCString U2FSoftTokenTransport::mSecretNickname = "U2F_NSSTOKEN"_ns;
+
+namespace {
+constexpr auto kAttestCertSubjectName = "CN=Firefox U2F Soft Token"_ns;
+
+// This U2F-compatible soft token uses FIDO U2F-compatible ECDSA keypairs
+// on the SEC_OID_SECG_EC_SECP256R1 curve. When asked to Register, it will
+// generate and return a new keypair KP, where the private component is wrapped
+// using AES-KW with the 128-bit mWrappingKey to make an opaque "key handle".
+// In other words, Register yields { KP_pub, AES-KW(KP_priv, key=mWrappingKey) }
+//
+// The value mWrappingKey is long-lived; it is persisted as part of the NSS DB
+// for the current profile. The attestation certificates that are produced are
+// ephemeral to counteract profiling. They have little use for a soft-token
+// at any rate, but are required by the specification.
+
+const uint32_t kParamLen = 32;
+const uint32_t kPublicKeyLen = 65;
+const uint32_t kWrappedKeyBufLen = 256;
+const uint32_t kWrappingKeyByteLen = 128 / 8;
+const uint32_t kSaltByteLen = 64 / 8;
+const uint32_t kVersion1KeyHandleLen = 162;
+constexpr auto kEcAlgorithm =
+ NS_LITERAL_STRING_FROM_CSTRING(WEBCRYPTO_NAMED_CURVE_P256);
+
+const PRTime kOneDay = PRTime(PR_USEC_PER_SEC) * PRTime(60) // sec
+ * PRTime(60) // min
+ * PRTime(24); // hours
+const PRTime kExpirationSlack = kOneDay; // Pre-date for clock skew
+const PRTime kExpirationLife = kOneDay;
+
+static mozilla::LazyLogModule gNSSTokenLog("webauthn_softtoken");
+
+enum SoftTokenHandle {
+ Version1 = 0,
+};
+
+} // namespace
+
+NS_IMPL_ISUPPORTS(U2FSoftTokenTransport, nsIWebAuthnTransport)
+
+U2FSoftTokenTransport::U2FSoftTokenTransport(uint32_t aCounter)
+ : mInitialized(false), mCounter(aCounter), mController(nullptr) {}
+
+NS_IMETHODIMP
+U2FSoftTokenTransport::GetController(nsIWebAuthnController** aController) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+U2FSoftTokenTransport::SetController(nsIWebAuthnController* aController) {
+ mController = aController;
+ return NS_OK;
+}
+
+/**
+ * Gets the first key with the given nickname from the given slot. Any other
+ * keys found are not returned.
+ * PK11_GetNextSymKey() should not be called on the returned key.
+ *
+ * @param aSlot Slot to search.
+ * @param aNickname Nickname the key should have.
+ * @return The first key found. nullptr if no key could be found.
+ */
+static UniquePK11SymKey GetSymKeyByNickname(const UniquePK11SlotInfo& aSlot,
+ const nsCString& aNickname) {
+ MOZ_ASSERT(aSlot);
+ if (NS_WARN_IF(!aSlot)) {
+ return nullptr;
+ }
+
+ MOZ_LOG(gNSSTokenLog, LogLevel::Debug,
+ ("Searching for a symmetric key named %s", aNickname.get()));
+
+ UniquePK11SymKey keyListHead(
+ PK11_ListFixedKeysInSlot(aSlot.get(), const_cast<char*>(aNickname.get()),
+ /* wincx */ nullptr));
+ if (NS_WARN_IF(!keyListHead)) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Debug, ("Symmetric key not found."));
+ return nullptr;
+ }
+
+ // Sanity check PK11_ListFixedKeysInSlot() only returns keys with the correct
+ // nickname.
+ MOZ_ASSERT(aNickname ==
+ UniquePORTString(PK11_GetSymKeyNickname(keyListHead.get())).get());
+ MOZ_LOG(gNSSTokenLog, LogLevel::Debug, ("Symmetric key found!"));
+
+ // Free any remaining keys in the key list.
+ UniquePK11SymKey freeKey(PK11_GetNextSymKey(keyListHead.get()));
+ while (freeKey) {
+ freeKey = UniquePK11SymKey(PK11_GetNextSymKey(freeKey.get()));
+ }
+
+ return keyListHead;
+}
+
+static nsresult GenEcKeypair(const UniquePK11SlotInfo& aSlot,
+ /*out*/ UniqueSECKEYPrivateKey& aPrivKey,
+ /*out*/ UniqueSECKEYPublicKey& aPubKey) {
+ MOZ_ASSERT(aSlot);
+ if (NS_WARN_IF(!aSlot)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ UniquePLArenaPool arena(PORT_NewArena(DER_DEFAULT_CHUNKSIZE));
+ if (NS_WARN_IF(!arena)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // Set the curve parameters; keyParams belongs to the arena memory space
+ SECItem* keyParams = CreateECParamsForCurve(kEcAlgorithm, arena.get());
+ if (NS_WARN_IF(!keyParams)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // Generate a key pair
+ CK_MECHANISM_TYPE mechanism = CKM_EC_KEY_PAIR_GEN;
+
+ SECKEYPublicKey* pubKeyRaw;
+ aPrivKey = UniqueSECKEYPrivateKey(
+ PK11_GenerateKeyPair(aSlot.get(), mechanism, keyParams, &pubKeyRaw,
+ /* ephemeral */ false, false,
+ /* wincx */ nullptr));
+ aPubKey = UniqueSECKEYPublicKey(pubKeyRaw);
+ pubKeyRaw = nullptr;
+ if (NS_WARN_IF(!aPrivKey.get() || !aPubKey.get())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Check that the public key has the correct length
+ if (NS_WARN_IF(aPubKey->u.ec.publicValue.len != kPublicKeyLen)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult U2FSoftTokenTransport::GetOrCreateWrappingKey(
+ const UniquePK11SlotInfo& aSlot) {
+ MOZ_ASSERT(aSlot);
+ if (NS_WARN_IF(!aSlot)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Search for an existing wrapping key. If we find it,
+ // store it for later and mark ourselves initialized.
+ mWrappingKey = GetSymKeyByNickname(aSlot, mSecretNickname);
+ if (mWrappingKey) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Debug, ("U2F Soft Token Key found."));
+ mInitialized = true;
+ return NS_OK;
+ }
+
+ MOZ_LOG(gNSSTokenLog, LogLevel::Info,
+ ("No keys found. Generating new U2F Soft Token wrapping key."));
+
+ // We did not find an existing wrapping key, so we generate one in the
+ // persistent database (e.g, Token).
+ mWrappingKey = UniquePK11SymKey(PK11_TokenKeyGenWithFlags(
+ aSlot.get(), CKM_AES_KEY_GEN,
+ /* default params */ nullptr, kWrappingKeyByteLen,
+ /* empty keyid */ nullptr,
+ /* flags */ CKF_WRAP | CKF_UNWRAP,
+ /* attributes */ PK11_ATTR_TOKEN | PK11_ATTR_PRIVATE,
+ /* wincx */ nullptr));
+
+ if (NS_WARN_IF(!mWrappingKey)) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
+ ("Failed to store wrapping key, NSS error #%d", PORT_GetError()));
+ return NS_ERROR_FAILURE;
+ }
+
+ SECStatus srv =
+ PK11_SetSymKeyNickname(mWrappingKey.get(), mSecretNickname.get());
+ if (NS_WARN_IF(srv != SECSuccess)) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
+ ("Failed to set nickname, NSS error #%d", PORT_GetError()));
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_LOG(gNSSTokenLog, LogLevel::Debug,
+ ("Key stored, nickname set to %s.", mSecretNickname.get()));
+
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ "dom::U2FSoftTokenTransport::GetOrCreateWrappingKey", []() {
+ MOZ_ASSERT(NS_IsMainThread());
+ Preferences::SetUint(PREF_U2F_NSSTOKEN_COUNTER, 0);
+ }));
+
+ return NS_OK;
+}
+
+static nsresult GetAttestationCertificate(
+ const UniquePK11SlotInfo& aSlot,
+ /*out*/ UniqueSECKEYPrivateKey& aAttestPrivKey,
+ /*out*/ UniqueCERTCertificate& aAttestCert) {
+ MOZ_ASSERT(aSlot);
+ if (NS_WARN_IF(!aSlot)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ UniqueSECKEYPublicKey pubKey;
+
+ // Construct an ephemeral keypair for this Attestation Certificate
+ nsresult rv = GenEcKeypair(aSlot, aAttestPrivKey, pubKey);
+ if (NS_WARN_IF(NS_FAILED(rv) || !aAttestPrivKey || !pubKey)) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
+ ("Failed to gen keypair, NSS error #%d", PORT_GetError()));
+ return NS_ERROR_FAILURE;
+ }
+
+ // Construct the Attestation Certificate itself
+ UniqueCERTName subjectName(CERT_AsciiToName(kAttestCertSubjectName.get()));
+ if (NS_WARN_IF(!subjectName)) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
+ ("Failed to set subject name, NSS error #%d", PORT_GetError()));
+ return NS_ERROR_FAILURE;
+ }
+
+ UniqueCERTSubjectPublicKeyInfo spki(
+ SECKEY_CreateSubjectPublicKeyInfo(pubKey.get()));
+ if (NS_WARN_IF(!spki)) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
+ ("Failed to set SPKI, NSS error #%d", PORT_GetError()));
+ return NS_ERROR_FAILURE;
+ }
+
+ UniqueCERTCertificateRequest certreq(
+ CERT_CreateCertificateRequest(subjectName.get(), spki.get(), nullptr));
+ if (NS_WARN_IF(!certreq)) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
+ ("Failed to gen CSR, NSS error #%d", PORT_GetError()));
+ return NS_ERROR_FAILURE;
+ }
+
+ PRTime now = PR_Now();
+ PRTime notBefore = now - kExpirationSlack;
+ PRTime notAfter = now + kExpirationLife;
+
+ UniqueCERTValidity validity(CERT_CreateValidity(notBefore, notAfter));
+ if (NS_WARN_IF(!validity)) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
+ ("Failed to gen validity, NSS error #%d", PORT_GetError()));
+ return NS_ERROR_FAILURE;
+ }
+
+ unsigned long serial;
+ unsigned char* serialBytes =
+ mozilla::BitwiseCast<unsigned char*, unsigned long*>(&serial);
+ SECStatus srv =
+ PK11_GenerateRandomOnSlot(aSlot.get(), serialBytes, sizeof(serial));
+ if (NS_WARN_IF(srv != SECSuccess)) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
+ ("Failed to gen serial, NSS error #%d", PORT_GetError()));
+ return NS_ERROR_FAILURE;
+ }
+ // Ensure that the most significant bit isn't set (which would
+ // indicate a negative number, which isn't valid for serial
+ // numbers).
+ serialBytes[0] &= 0x7f;
+ // Also ensure that the least significant bit on the most
+ // significant byte is set (to prevent a leading zero byte,
+ // which also wouldn't be valid).
+ serialBytes[0] |= 0x01;
+
+ // NB: CERTCertificates created with CERT_CreateCertificate are not safe to
+ // use with other NSS functions like CERT_DupCertificate.
+ // The strategy here is to create a tbsCertificate ("to-be-signed
+ // certificate"), encode it, and sign it, resulting in a signed DER
+ // certificate that can be decoded into a CERTCertificate.
+ UniqueCERTCertificate tbsCertificate(CERT_CreateCertificate(
+ serial, subjectName.get(), validity.get(), certreq.get()));
+ if (NS_WARN_IF(!tbsCertificate)) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
+ ("Failed to gen certificate, NSS error #%d", PORT_GetError()));
+ return NS_ERROR_FAILURE;
+ }
+
+ PLArenaPool* arena = tbsCertificate->arena;
+
+ srv = SECOID_SetAlgorithmID(arena, &tbsCertificate->signature,
+ SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE,
+ /* wincx */ nullptr);
+ if (NS_WARN_IF(srv != SECSuccess)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Set version to X509v3.
+ *(tbsCertificate->version.data) = SEC_CERTIFICATE_VERSION_3;
+ tbsCertificate->version.len = 1;
+
+ SECItem innerDER = {siBuffer, nullptr, 0};
+ if (NS_WARN_IF(!SEC_ASN1EncodeItem(arena, &innerDER, tbsCertificate.get(),
+ SEC_ASN1_GET(CERT_CertificateTemplate)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ SECItem* certDer = PORT_ArenaZNew(arena, SECItem);
+ if (NS_WARN_IF(!certDer)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ srv = SEC_DerSignData(arena, certDer, innerDER.data, innerDER.len,
+ aAttestPrivKey.get(),
+ SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE);
+ if (NS_WARN_IF(srv != SECSuccess)) {
+ return NS_ERROR_FAILURE;
+ }
+ aAttestCert = UniqueCERTCertificate(CERT_NewTempCertificate(
+ CERT_GetDefaultCertDB(), certDer, nullptr, false, true));
+ if (NS_WARN_IF(!aAttestCert)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_LOG(gNSSTokenLog, LogLevel::Debug,
+ ("U2F Soft Token attestation certificate generated."));
+ return NS_OK;
+}
+
+// Set up the context for the soft U2F Token. This is called by NSS
+// initialization.
+nsresult U2FSoftTokenTransport::Init() {
+ // If we've already initialized, just return.
+ if (mInitialized) {
+ return NS_OK;
+ }
+
+ UniquePK11SlotInfo slot(PK11_GetInternalKeySlot());
+ MOZ_ASSERT(slot.get());
+
+ // Search for an existing wrapping key, or create one.
+ nsresult rv = GetOrCreateWrappingKey(slot);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mInitialized = true;
+ MOZ_LOG(gNSSTokenLog, LogLevel::Debug, ("U2F Soft Token initialized."));
+ return NS_OK;
+}
+
+// Convert a Private Key object into an opaque key handle, using AES Key Wrap
+// with the long-lived aPersistentKey mixed with aAppParam to convert aPrivKey.
+// The key handle's format is version || saltLen || salt || wrappedPrivateKey
+static UniqueSECItem KeyHandleFromPrivateKey(
+ const UniquePK11SlotInfo& aSlot, const UniquePK11SymKey& aPersistentKey,
+ uint8_t* aAppParam, uint32_t aAppParamLen,
+ const UniqueSECKEYPrivateKey& aPrivKey) {
+ MOZ_ASSERT(aSlot);
+ MOZ_ASSERT(aPersistentKey);
+ MOZ_ASSERT(aAppParam);
+ MOZ_ASSERT(aPrivKey);
+ if (NS_WARN_IF(!aSlot || !aPersistentKey || !aPrivKey || !aAppParam)) {
+ return nullptr;
+ }
+
+ // Generate a random salt
+ uint8_t saltParam[kSaltByteLen];
+ SECStatus srv =
+ PK11_GenerateRandomOnSlot(aSlot.get(), saltParam, sizeof(saltParam));
+ if (NS_WARN_IF(srv != SECSuccess)) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
+ ("Failed to generate a salt, NSS error #%d", PORT_GetError()));
+ return nullptr;
+ }
+
+ // Prepare the HKDF (https://tools.ietf.org/html/rfc5869)
+ CK_NSS_HKDFParams hkdfParams = {true, saltParam, sizeof(saltParam),
+ true, aAppParam, aAppParamLen};
+ SECItem kdfParams = {siBuffer, (unsigned char*)&hkdfParams,
+ sizeof(hkdfParams)};
+
+ // Derive a wrapping key from aPersistentKey, the salt, and the aAppParam.
+ // CKM_AES_KEY_GEN and CKA_WRAP are key type and usage attributes of the
+ // derived symmetric key and don't matter because we ignore them anyway.
+ UniquePK11SymKey wrapKey(
+ PK11_Derive(aPersistentKey.get(), CKM_NSS_HKDF_SHA256, &kdfParams,
+ CKM_AES_KEY_GEN, CKA_WRAP, kWrappingKeyByteLen));
+ if (NS_WARN_IF(!wrapKey.get())) {
+ MOZ_LOG(
+ gNSSTokenLog, LogLevel::Warning,
+ ("Failed to derive a wrapping key, NSS error #%d", PORT_GetError()));
+ return nullptr;
+ }
+
+ UniqueSECItem wrappedKey(::SECITEM_AllocItem(/* default arena */ nullptr,
+ /* no buffer */ nullptr,
+ kWrappedKeyBufLen));
+ if (NS_WARN_IF(!wrappedKey)) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory"));
+ return nullptr;
+ }
+
+ UniqueSECItem param(PK11_ParamFromIV(CKM_NSS_AES_KEY_WRAP_PAD,
+ /* default IV */ nullptr));
+
+ srv =
+ PK11_WrapPrivKey(aSlot.get(), wrapKey.get(), aPrivKey.get(),
+ CKM_NSS_AES_KEY_WRAP_PAD, param.get(), wrappedKey.get(),
+ /* wincx */ nullptr);
+ if (NS_WARN_IF(srv != SECSuccess)) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
+ ("Failed to wrap U2F key, NSS error #%d", PORT_GetError()));
+ return nullptr;
+ }
+
+ // Concatenate the salt and the wrapped Private Key together
+ mozilla::dom::CryptoBuffer keyHandleBuf;
+ if (NS_WARN_IF(!keyHandleBuf.SetCapacity(
+ wrappedKey.get()->len + sizeof(saltParam) + 2, mozilla::fallible))) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory"));
+ return nullptr;
+ }
+
+ // It's OK to ignore the return values here because we're writing into
+ // pre-allocated space
+ (void)keyHandleBuf.AppendElement(SoftTokenHandle::Version1,
+ mozilla::fallible);
+ (void)keyHandleBuf.AppendElement(sizeof(saltParam), mozilla::fallible);
+ (void)keyHandleBuf.AppendElements(saltParam, sizeof(saltParam),
+ mozilla::fallible);
+ keyHandleBuf.AppendSECItem(wrappedKey.get());
+
+ UniqueSECItem keyHandle(::SECITEM_AllocItem(nullptr, nullptr, 0));
+ if (NS_WARN_IF(!keyHandle)) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory"));
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(!keyHandleBuf.ToSECItem(/* default arena */ nullptr,
+ keyHandle.get()))) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory"));
+ return nullptr;
+ }
+ return keyHandle;
+}
+
+// Convert an opaque key handle aKeyHandle back into a Private Key object, using
+// the long-lived aPersistentKey mixed with aAppParam and the AES Key Wrap
+// algorithm.
+static UniqueSECKEYPrivateKey PrivateKeyFromKeyHandle(
+ const UniquePK11SlotInfo& aSlot, const UniquePK11SymKey& aPersistentKey,
+ uint8_t* aKeyHandle, uint32_t aKeyHandleLen, uint8_t* aAppParam,
+ uint32_t aAppParamLen) {
+ MOZ_ASSERT(aSlot);
+ MOZ_ASSERT(aPersistentKey);
+ MOZ_ASSERT(aKeyHandle);
+ MOZ_ASSERT(aAppParam);
+ MOZ_ASSERT(aAppParamLen == SHA256_LENGTH);
+ if (NS_WARN_IF(!aSlot || !aPersistentKey || !aKeyHandle || !aAppParam ||
+ aAppParamLen != SHA256_LENGTH)) {
+ return nullptr;
+ }
+
+ // As we only support one key format ourselves (right now), fail early if
+ // we aren't that length
+ if (NS_WARN_IF(aKeyHandleLen != kVersion1KeyHandleLen)) {
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(aKeyHandle[0] != SoftTokenHandle::Version1)) {
+ // Unrecognized version
+ return nullptr;
+ }
+
+ uint8_t saltLen = aKeyHandle[1];
+ uint8_t* saltPtr = aKeyHandle + 2;
+ if (NS_WARN_IF(saltLen != kSaltByteLen)) {
+ return nullptr;
+ }
+
+ // Prepare the HKDF (https://tools.ietf.org/html/rfc5869)
+ CK_NSS_HKDFParams hkdfParams = {true, saltPtr, saltLen,
+ true, aAppParam, aAppParamLen};
+ SECItem kdfParams = {siBuffer, (unsigned char*)&hkdfParams,
+ sizeof(hkdfParams)};
+
+ // Derive a wrapping key from aPersistentKey, the salt, and the aAppParam.
+ // CKM_AES_KEY_GEN and CKA_WRAP are key type and usage attributes of the
+ // derived symmetric key and don't matter because we ignore them anyway.
+ UniquePK11SymKey wrapKey(
+ PK11_Derive(aPersistentKey.get(), CKM_NSS_HKDF_SHA256, &kdfParams,
+ CKM_AES_KEY_GEN, CKA_WRAP, kWrappingKeyByteLen));
+ if (NS_WARN_IF(!wrapKey.get())) {
+ MOZ_LOG(
+ gNSSTokenLog, LogLevel::Warning,
+ ("Failed to derive a wrapping key, NSS error #%d", PORT_GetError()));
+ return nullptr;
+ }
+
+ uint8_t wrappedLen = aKeyHandleLen - saltLen - 2;
+ uint8_t* wrappedPtr = aKeyHandle + saltLen + 2;
+
+ ScopedAutoSECItem wrappedKeyItem(wrappedLen);
+ memcpy(wrappedKeyItem.data, wrappedPtr, wrappedKeyItem.len);
+
+ ScopedAutoSECItem pubKey(kPublicKeyLen);
+
+ UniqueSECItem param(PK11_ParamFromIV(CKM_NSS_AES_KEY_WRAP_PAD,
+ /* default IV */ nullptr));
+
+ CK_ATTRIBUTE_TYPE usages[] = {CKA_SIGN};
+ int usageCount = 1;
+
+ UniqueSECKEYPrivateKey unwrappedKey(
+ PK11_UnwrapPrivKey(aSlot.get(), wrapKey.get(), CKM_NSS_AES_KEY_WRAP_PAD,
+ param.get(), &wrappedKeyItem,
+ /* no nickname */ nullptr,
+ /* discard pubkey */ &pubKey,
+ /* not permanent */ false,
+ /* non-exportable */ true, CKK_EC, usages, usageCount,
+ /* wincx */ nullptr));
+ if (NS_WARN_IF(!unwrappedKey)) {
+ // Not our key.
+ MOZ_LOG(gNSSTokenLog, LogLevel::Debug,
+ ("Could not unwrap key handle, NSS Error #%d", PORT_GetError()));
+ return nullptr;
+ }
+
+ return unwrappedKey;
+}
+
+// IsRegistered determines if the provided key handle is usable by this token.
+nsresult U2FSoftTokenTransport::IsRegistered(
+ const nsTArray<uint8_t>& aKeyHandle, const nsTArray<uint8_t>& aAppParam,
+ bool& aResult) {
+ if (!mInitialized) {
+ nsresult rv = Init();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ UniquePK11SlotInfo slot(PK11_GetInternalSlot());
+ MOZ_ASSERT(slot.get());
+
+ // Decode the key handle
+ UniqueSECKEYPrivateKey privKey = PrivateKeyFromKeyHandle(
+ slot, mWrappingKey, const_cast<uint8_t*>(aKeyHandle.Elements()),
+ aKeyHandle.Length(), const_cast<uint8_t*>(aAppParam.Elements()),
+ aAppParam.Length());
+ aResult = privKey.get() != nullptr;
+ return NS_OK;
+}
+
+// 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
+//
+NS_IMETHODIMP
+U2FSoftTokenTransport::MakeCredential(uint64_t aTransactionId,
+ uint64_t _aBrowsingContextId,
+ nsICtapRegisterArgs* args) {
+ nsresult rv;
+
+ if (!mInitialized) {
+ rv = Init();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ if (NS_WARN_IF(!args)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ bool requireResidentKey = false;
+ nsString residentKey;
+ rv = args->GetResidentKey(residentKey);
+ if (NS_SUCCEEDED(rv) && residentKey.EqualsLiteral(
+ MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_REQUIRED)) {
+ requireResidentKey = true;
+ }
+
+ bool requireUserVerification = false;
+ nsString userVerification;
+ // Bug 1737205 will make this infallible
+ rv = args->GetUserVerification(userVerification);
+ if (NS_SUCCEEDED(rv)) {
+ requireUserVerification = userVerification.EqualsLiteral(
+ MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED);
+ }
+
+ bool requirePlatformAttachment = false;
+ nsString authenticatorAttachment;
+ // Bug 1737205 will make this infallible
+ rv = args->GetAuthenticatorAttachment(authenticatorAttachment);
+ if (NS_SUCCEEDED(rv) && authenticatorAttachment.EqualsLiteral(
+ MOZ_WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM)) {
+ requirePlatformAttachment = true;
+ }
+
+ // The U2F softtoken neither supports resident keys or
+ // user verification, nor is it a platform authenticator.
+ if (requireResidentKey || requireUserVerification ||
+ requirePlatformAttachment) {
+ return NS_ERROR_DOM_NOT_ALLOWED_ERR;
+ }
+
+ bool noneAttestationRequested = false;
+ nsString attestationConveyancePreference;
+ rv =
+ args->GetAttestationConveyancePreference(attestationConveyancePreference);
+ if (NS_SUCCEEDED(rv) &&
+ attestationConveyancePreference.EqualsLiteral(
+ MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE)) {
+ noneAttestationRequested = true;
+ }
+
+ nsTArray<int32_t> coseAlgs;
+ // Bug 1737205 will make this infallible
+ rv = args->GetCoseAlgs(coseAlgs);
+ // If the request does not list algs, assume ES256.
+ if (NS_FAILED(rv) || coseAlgs.IsEmpty()) {
+ coseAlgs.AppendElement(
+ static_cast<int32_t>(CoseAlgorithmIdentifier::ES256));
+ }
+ // This token only supports ES256.
+ coseAlgs.RemoveElementsBy([](auto& alg) {
+ return alg != static_cast<int32_t>(CoseAlgorithmIdentifier::ES256);
+ });
+ // If there are no acceptable/supported algorithms, exit
+ if (coseAlgs.IsEmpty()) {
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+ }
+
+ nsString rpId;
+ args->GetRpId(rpId);
+
+ nsTArray<uint8_t> clientDataHash;
+ rv = args->GetClientDataHash(clientDataHash);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ CryptoBuffer rpIdHash;
+ rv = HashCString(NS_ConvertUTF16toUTF8(rpId), rpIdHash);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Optional exclusion list.
+ nsTArray<nsTArray<uint8_t>> excludeList;
+ args->GetExcludeList(excludeList);
+ for (const auto& credId : excludeList) {
+ bool isRegistered = false;
+ nsresult rv = IsRegistered(credId, rpIdHash, isRegistered);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (isRegistered) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+ }
+
+ // We should already have a wrapping key
+ MOZ_ASSERT(mWrappingKey);
+
+ UniquePK11SlotInfo slot(PK11_GetInternalSlot());
+ MOZ_ASSERT(slot.get());
+
+ // Construct a one-time-use Attestation Certificate
+ UniqueSECKEYPrivateKey attestPrivKey;
+ UniqueCERTCertificate attestCert;
+ rv = GetAttestationCertificate(slot, attestPrivKey, attestCert);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(attestCert);
+ MOZ_ASSERT(attestPrivKey);
+
+ // Generate a new keypair; the private will be wrapped into a Key Handle
+ UniqueSECKEYPrivateKey privKey;
+ UniqueSECKEYPublicKey pubKey;
+ rv = GenEcKeypair(slot, privKey, pubKey);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // The key handle will be the result of keywrap(privKey, key=mWrappingKey)
+ UniqueSECItem keyHandleItem = KeyHandleFromPrivateKey(
+ slot, mWrappingKey, const_cast<uint8_t*>(rpIdHash.Elements()),
+ rpIdHash.Length(), privKey);
+ if (NS_WARN_IF(!keyHandleItem.get())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Sign the challenge using the Attestation privkey (from attestCert)
+ mozilla::dom::CryptoBuffer signedDataBuf;
+ if (NS_WARN_IF(!signedDataBuf.SetCapacity(
+ 1 + rpIdHash.Length() + clientDataHash.Length() + keyHandleItem->len +
+ kPublicKeyLen,
+ mozilla::fallible))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // // It's OK to ignore the return values here because we're writing into
+ // // pre-allocated space
+ (void)signedDataBuf.AppendElement(0x00, mozilla::fallible);
+ (void)signedDataBuf.AppendElements(rpIdHash, mozilla::fallible);
+ (void)signedDataBuf.AppendElements(clientDataHash, mozilla::fallible);
+ signedDataBuf.AppendSECItem(keyHandleItem.get());
+ signedDataBuf.AppendSECItem(pubKey->u.ec.publicValue);
+
+ ScopedAutoSECItem signatureItem;
+ SECStatus srv = SEC_SignData(&signatureItem, signedDataBuf.Elements(),
+ signedDataBuf.Length(), attestPrivKey.get(),
+ SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE);
+ if (NS_WARN_IF(srv != SECSuccess)) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
+ ("Signature failure: %d", PORT_GetError()));
+ return NS_ERROR_FAILURE;
+ }
+
+ CryptoBuffer keyHandleBuf;
+ if (!keyHandleBuf.AppendSECItem(keyHandleItem.get())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ CryptoBuffer attestCertBuf;
+ if (!attestCertBuf.AppendSECItem(attestCert.get()->derCert)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ CryptoBuffer signatureBuf;
+ if (!signatureBuf.AppendSECItem(signatureItem)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ CryptoBuffer pubKeyBuf;
+ if (!pubKeyBuf.AppendSECItem(pubKey->u.ec.publicValue)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ CryptoBuffer attObj;
+ rv = AssembleAttestationObject(rpIdHash, pubKeyBuf, keyHandleBuf,
+ attestCertBuf, signatureBuf,
+ noneAttestationRequested, attObj);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsTArray<uint8_t> outAttObj(std::move(attObj));
+ nsTArray<uint8_t> outKeyHandleBuf(std::move(keyHandleBuf));
+ mController->FinishRegister(
+ aTransactionId, new CtapRegisterResult(NS_OK, std::move(outAttObj),
+ std::move(outKeyHandleBuf)));
+ return NS_OK;
+}
+
+bool U2FSoftTokenTransport::FindRegisteredKeyHandle(
+ const nsTArray<nsTArray<uint8_t>>& aAppIds,
+ const nsTArray<nsTArray<uint8_t>>& aCredentialIds,
+ /*out*/ nsTArray<uint8_t>& aKeyHandle,
+ /*out*/ nsTArray<uint8_t>& aAppId) {
+ for (const nsTArray<uint8_t>& app_id : aAppIds) {
+ for (const nsTArray<uint8_t>& credId : aCredentialIds) {
+ bool isRegistered = false;
+ nsresult rv = IsRegistered(credId, app_id, isRegistered);
+ if (NS_SUCCEEDED(rv) && isRegistered) {
+ aKeyHandle.Clear();
+ aKeyHandle.AppendElements(credId);
+ aAppId.Assign(app_id);
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+// 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
+//
+NS_IMETHODIMP
+U2FSoftTokenTransport::GetAssertion(uint64_t aTransactionId,
+ uint64_t _aBrowsingContextId,
+ nsICtapSignArgs* args) {
+ nsresult rv;
+ if (!mInitialized) {
+ rv = Init();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ if (NS_WARN_IF(!args)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ bool requireUserVerification = false;
+ nsString userVerification;
+ // Bug 1737205 will make this infallible
+ rv = args->GetUserVerification(userVerification);
+ if (NS_SUCCEEDED(rv)) {
+ requireUserVerification = userVerification.EqualsLiteral(
+ MOZ_WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED);
+ }
+ if (requireUserVerification) {
+ return NS_ERROR_DOM_NOT_ALLOWED_ERR;
+ }
+
+ nsString rpId;
+ args->GetRpId(rpId);
+
+ nsTArray<uint8_t> clientDataHash;
+ rv = args->GetClientDataHash(clientDataHash);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ CryptoBuffer rpIdHash;
+ rv = HashCString(NS_ConvertUTF16toUTF8(rpId), rpIdHash);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsTArray<nsTArray<uint8_t>> appIdHashes;
+ appIdHashes.AppendElement(std::move(rpIdHash));
+
+ nsTArray<uint8_t> appIdHash;
+ rv = args->GetAppIdHash(appIdHash);
+ if (NS_SUCCEEDED(rv)) {
+ appIdHashes.AppendElement(std::move(appIdHash));
+ }
+
+ nsTArray<nsTArray<uint8_t>> allowList;
+ args->GetAllowList(allowList);
+
+ nsTArray<uint8_t> chosenAppId;
+ nsTArray<uint8_t> keyHandle;
+
+ // Fail if we can't find a valid key handle.
+ if (!FindRegisteredKeyHandle(appIdHashes, allowList, keyHandle,
+ chosenAppId)) {
+ return NS_ERROR_DOM_INVALID_STATE_ERR;
+ }
+
+ MOZ_ASSERT(mWrappingKey);
+
+ UniquePK11SlotInfo slot(PK11_GetInternalSlot());
+ MOZ_ASSERT(slot.get());
+
+ if (NS_WARN_IF((clientDataHash.Length() != kParamLen) ||
+ (chosenAppId.Length() != kParamLen))) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
+ ("Parameter lengths are wrong! challenge=%d app=%d expected=%d",
+ (uint32_t)clientDataHash.Length(), (uint32_t)chosenAppId.Length(),
+ kParamLen));
+
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ // Decode the key handle
+ UniqueSECKEYPrivateKey privKey = PrivateKeyFromKeyHandle(
+ slot, mWrappingKey, const_cast<uint8_t*>(keyHandle.Elements()),
+ keyHandle.Length(), const_cast<uint8_t*>(chosenAppId.Elements()),
+ chosenAppId.Length());
+ if (NS_WARN_IF(!privKey.get())) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Couldn't get the priv key!"));
+ return NS_ERROR_FAILURE;
+ }
+
+ // Increment the counter and turn it into a SECItem
+ mCounter += 1;
+ ScopedAutoSECItem counterItem(4);
+ counterItem.data[0] = (mCounter >> 24) & 0xFF;
+ counterItem.data[1] = (mCounter >> 16) & 0xFF;
+ counterItem.data[2] = (mCounter >> 8) & 0xFF;
+ counterItem.data[3] = (mCounter >> 0) & 0xFF;
+ uint32_t counter = mCounter;
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ "dom::U2FSoftTokenTransport::GetAssertion", [counter]() {
+ MOZ_ASSERT(NS_IsMainThread());
+ Preferences::SetUint(PREF_U2F_NSSTOKEN_COUNTER, counter);
+ }));
+
+ // Compute the signature
+ mozilla::dom::CryptoBuffer signedDataBuf;
+ if (NS_WARN_IF(!signedDataBuf.SetCapacity(1 + 4 + (2 * kParamLen),
+ mozilla::fallible))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // It's OK to ignore the return values here because we're writing into
+ // pre-allocated space
+ (void)signedDataBuf.AppendElements(chosenAppId.Elements(),
+ chosenAppId.Length(), mozilla::fallible);
+ (void)signedDataBuf.AppendElement(0x01, mozilla::fallible);
+ signedDataBuf.AppendSECItem(counterItem);
+ (void)signedDataBuf.AppendElements(
+ clientDataHash.Elements(), clientDataHash.Length(), mozilla::fallible);
+
+ if (MOZ_LOG_TEST(gNSSTokenLog, LogLevel::Debug)) {
+ nsAutoCString base64;
+ rv = Base64URLEncode(signedDataBuf.Length(), signedDataBuf.Elements(),
+ Base64URLEncodePaddingPolicy::Omit, base64);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_LOG(gNSSTokenLog, LogLevel::Debug,
+ ("U2F Token signing bytes (base64): %s", base64.get()));
+ }
+
+ ScopedAutoSECItem signatureItem;
+ SECStatus srv = SEC_SignData(&signatureItem, signedDataBuf.Elements(),
+ signedDataBuf.Length(), privKey.get(),
+ SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE);
+ if (NS_WARN_IF(srv != SECSuccess)) {
+ MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
+ ("Signature failure: %d", PORT_GetError()));
+ return NS_ERROR_FAILURE;
+ }
+
+ nsTArray<WebAuthnExtensionResult> extensions;
+
+ if (appIdHashes.Length() == 2) {
+ bool usedAppId = (chosenAppId == appIdHashes[1]);
+ extensions.AppendElement(WebAuthnExtensionResultAppId(usedAppId));
+ }
+
+ CryptoBuffer counterBuf;
+ if (!counterBuf.AppendSECItem(counterItem)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ CryptoBuffer signatureBuf;
+ if (!signatureBuf.AppendSECItem(signatureItem)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ CryptoBuffer chosenAppIdBuf;
+ if (!chosenAppIdBuf.Assign(chosenAppId)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ CryptoBuffer authenticatorData;
+ CryptoBuffer emptyAttestationData;
+ rv = AssembleAuthenticatorData(chosenAppIdBuf, 0x01, counterBuf,
+ emptyAttestationData, authenticatorData);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsTArray<uint8_t> outSignatureBuf(std::move(signatureBuf));
+ nsTArray<uint8_t> outAuthenticatorData(std::move(authenticatorData));
+ nsTArray<uint8_t> userHandle; // unused because this is a CTAP1 token
+
+ nsTArray<RefPtr<nsICtapSignResult>> results;
+ results.AppendElement(new CtapSignResult(
+ NS_OK, std::move(keyHandle), std::move(outSignatureBuf),
+ std::move(outAuthenticatorData), std::move(userHandle),
+ std::move(chosenAppId)));
+
+ mController->FinishSign(aTransactionId, results);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+U2FSoftTokenTransport::Cancel(void) {
+ // This implementation is sync, requests can't be aborted.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+U2FSoftTokenTransport::PinCallback(uint64_t aTransactionId,
+ const nsACString& aPin) {
+ // This is a U2F/CTAP1 token. It doesn't support pins.
+ return NS_ERROR_FAILURE;
+}
+
+} // namespace mozilla::dom