Bug 1260318 - Scope U2F Soft Tokens to a single AppID r=qdot,rbarnes
authorJ.C. Jones <jjones@mozilla.com>
Wed, 01 Feb 2017 15:21:04 -0700
changeset 395063 ad5adacd8e14472b0abcf9065efbcc96d5a859c4
parent 395062 ddad37ba77f1bd179b8b6e530ead6cab242ef120
child 395064 d40fe2f45605d2706b34bfe23d4ba646b79a288c
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersqdot, rbarnes
bugs1260318
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1260318 - Scope U2F Soft Tokens to a single AppID r=qdot,rbarnes This change includes the FIDO "App ID" as part of the function used to generate the wrapping key used in the NSS-based U2F soft token, cryptographically binding the "Key Handle" to the site that Key Handle is intended for. This is a breaking change with existing registered U2F keys, but since our soft token is hidden behind a pref, it does not attempt to be backward-compatible. - Updated for rbarnes' and qdot's reviews comments. Thanks! - Made more strict in size restrictions, and added a version field to help us be this strict. - Bugfix for an early unprotected buffer use (Thanks again rbarnes!) - Fix a sneaky memory leak re: CryptoBuffer.ToSECItem MozReview-Commit-ID: Jf6gNPauT4Y
dom/ipc/ContentParent.cpp
dom/ipc/ContentParent.h
dom/ipc/PContent.ipdl
dom/u2f/U2F.cpp
dom/u2f/U2F.h
dom/webauthn/NSSU2FTokenRemote.cpp
dom/webauthn/WebAuthentication.cpp
security/manager/ssl/nsIU2FToken.idl
security/manager/ssl/nsNSSU2FToken.cpp
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -3437,26 +3437,28 @@ ContentParent::RecvNSSU2FTokenIsCompatib
   if (NS_FAILED(rv)) {
     return IPC_FAIL_NO_REASON(this);
   }
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
 ContentParent::RecvNSSU2FTokenIsRegistered(nsTArray<uint8_t>&& aKeyHandle,
+                                           nsTArray<uint8_t>&& aApplication,
                                            bool* aIsValidKeyHandle)
 {
   MOZ_ASSERT(aIsValidKeyHandle);
 
   nsCOMPtr<nsINSSU2FToken> nssToken(do_GetService(NS_NSSU2FTOKEN_CONTRACTID));
   if (NS_WARN_IF(!nssToken)) {
     return IPC_FAIL_NO_REASON(this);
   }
 
   nsresult rv = nssToken->IsRegistered(aKeyHandle.Elements(), aKeyHandle.Length(),
+                                       aApplication.Elements(), aApplication.Length(),
                                        aIsValidKeyHandle);
   if (NS_FAILED(rv)) {
     return IPC_FAIL_NO_REASON(this);
   }
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
--- a/dom/ipc/ContentParent.h
+++ b/dom/ipc/ContentParent.h
@@ -790,16 +790,17 @@ private:
   virtual mozilla::ipc::IPCResult
   RecvPBlobConstructor(PBlobParent* aActor,
                        const BlobConstructorParams& params) override;
 
   virtual mozilla::ipc::IPCResult RecvNSSU2FTokenIsCompatibleVersion(const nsString& aVersion,
                                                                      bool* aIsCompatible) override;
 
   virtual mozilla::ipc::IPCResult RecvNSSU2FTokenIsRegistered(nsTArray<uint8_t>&& aKeyHandle,
+                                                              nsTArray<uint8_t>&& aApplication,
                                                               bool* aIsValidKeyHandle) override;
 
   virtual mozilla::ipc::IPCResult RecvNSSU2FTokenRegister(nsTArray<uint8_t>&& aApplication,
                                                           nsTArray<uint8_t>&& aChallenge,
                                                           nsTArray<uint8_t>* aRegistration) override;
 
   virtual mozilla::ipc::IPCResult RecvNSSU2FTokenSign(nsTArray<uint8_t>&& aApplication,
                                                       nsTArray<uint8_t>&& aChallenge,
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -642,19 +642,20 @@ parent:
      */
     sync NSSU2FTokenIsCompatibleVersion(nsString version)
         returns (bool result);
 
     /**
      * Return whether the provided KeyHandle belongs to this Token
      *
      * |keyHandle| Key Handle to evaluate.
+     * |application| The FIDO Application data that is associated with this key.
      * Returns |True| if the Key Handle is ours.
      */
-    sync NSSU2FTokenIsRegistered(uint8_t[] keyHandle)
+    sync NSSU2FTokenIsRegistered(uint8_t[] keyHandle, uint8_t[] application)
         returns (bool isValidKeyHandle);
 
     /**
      * Generates a public/private keypair for the provided application
      * and challenge, returning the pubkey, challenge response, and
      * key handle in the registration data.
      *
      * |application| The FIDO Application data to associate with the key.
--- a/dom/u2f/U2F.cpp
+++ b/dom/u2f/U2F.cpp
@@ -206,19 +206,21 @@ U2FPrepTask::Execute()
   // TODO: Use a thread pool here, but we have to solve the PContentChild issues
   // of being in a worker thread.
   mAbstractMainThread->Dispatch(r.forget());
   return p;
 }
 
 U2FIsRegisteredTask::U2FIsRegisteredTask(const Authenticator& aAuthenticator,
                                          const LocalRegisteredKey& aRegisteredKey,
+                                         const CryptoBuffer& aAppParam,
                                          AbstractThread* aMainThread)
   : U2FPrepTask(aAuthenticator, aMainThread)
   , mRegisteredKey(aRegisteredKey)
+  , mAppParam(aAppParam)
 {}
 
 U2FIsRegisteredTask::~U2FIsRegisteredTask()
 {}
 
 NS_IMETHODIMP
 U2FIsRegisteredTask::Run()
 {
@@ -243,16 +245,17 @@ U2FIsRegisteredTask::Run()
     return NS_ERROR_FAILURE;
   }
 
   // We ignore mTransports, as it is intended to be used for sorting the
   // available devices by preference, but is not an exclusion factor.
 
   bool isRegistered = false;
   rv = mAuthenticator->IsRegistered(keyHandle.Elements(), keyHandle.Length(),
+                                    mAppParam.Elements(), mAppParam.Length(),
                                     &isRegistered);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     mPromise.Reject(ErrorCode::OTHER_ERROR, __func__);
     return NS_ERROR_FAILURE;
   }
 
   if (isRegistered) {
     mPromise.Reject(ErrorCode::DEVICE_INELIGIBLE, __func__);
@@ -373,16 +376,17 @@ U2FSignTask::Run()
 
   if (!isCompatible) {
     mPromise.Reject(ErrorCode::BAD_REQUEST, __func__);
     return NS_ERROR_FAILURE;
   }
 
   bool isRegistered = false;
   rv = mAuthenticator->IsRegistered(mKeyHandle.Elements(), mKeyHandle.Length(),
+                                    mAppParam.Elements(), mAppParam.Length(),
                                     &isRegistered);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     mPromise.Reject(ErrorCode::OTHER_ERROR, __func__);
     return NS_ERROR_FAILURE;
   }
 
   if (!isRegistered) {
     mPromise.Reject(ErrorCode::DEVICE_INELIGIBLE, __func__);
@@ -613,23 +617,40 @@ U2FRegisterRunnable::Run()
   RefPtr<U2FStatus> status = new U2FStatus();
 
   // Evaluate the AppID
   ErrorCode appIdResult = EvaluateAppID();
   if (appIdResult != ErrorCode::OK) {
     status->Stop(appIdResult);
   }
 
+  // Produce the AppParam from the current AppID
+  nsCString cAppId = NS_ConvertUTF16toUTF8(mAppId);
+  CryptoBuffer appParam;
+  if (!appParam.SetLength(SHA256_LENGTH, fallible)) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+
+  // Note: This could use nsICryptoHash to avoid having to interact with NSS
+  // directly.
+  SECStatus srv;
+  srv = PK11_HashBuf(SEC_OID_SHA256, appParam.Elements(),
+                     reinterpret_cast<const uint8_t*>(cAppId.BeginReading()),
+                     cAppId.Length());
+  if (srv != SECSuccess) {
+    return NS_ERROR_FAILURE;
+  }
+
   // First, we must determine if any of the RegisteredKeys are already
   // registered, e.g., in the whitelist.
   for (LocalRegisteredKey key : mRegisteredKeys) {
     nsTArray<RefPtr<U2FPrepPromise>> prepPromiseList;
     for (const Authenticator& token : mAuthenticators) {
       RefPtr<U2FIsRegisteredTask> compTask =
-        new U2FIsRegisteredTask(token, key, mAbstractMainThread);
+        new U2FIsRegisteredTask(token, key, appParam, mAbstractMainThread);
       prepPromiseList.AppendElement(compTask->Execute());
     }
 
     // Treat each call to Promise::All as a work unit, as it completes together
     status->WaitGroupAdd();
 
     U2FPrepPromise::All(mAbstractMainThread, prepPromiseList)
     ->Then(mAbstractMainThread, __func__,
@@ -663,33 +684,16 @@ U2FRegisterRunnable::Run()
       }
     ));
 
     // Don't exit until the main thread runnable completes
     status->WaitGroupWait();
     return NS_OK;
   }
 
-  // Since we're continuing, we hash the AppID into the AppParam
-  nsCString cAppId = NS_ConvertUTF16toUTF8(mAppId);
-  CryptoBuffer appParam;
-  if (!appParam.SetLength(SHA256_LENGTH, fallible)) {
-    return NS_ERROR_OUT_OF_MEMORY;
-  }
-
-  // Note: This could use nsICryptoHash to avoid having to interact with NSS
-  // directly.
-  SECStatus srv;
-  srv = PK11_HashBuf(SEC_OID_SHA256, appParam.Elements(),
-                     reinterpret_cast<const uint8_t*>(cAppId.BeginReading()),
-                     cAppId.Length());
-  if (srv != SECSuccess) {
-    return NS_ERROR_FAILURE;
-  }
-
   // Now proceed to actually register a new key.
   for (LocalRegisterRequest req : mRegisterRequests) {
     // Hash the ClientData into the ChallengeParam
     CryptoBuffer challengeParam;
     if (!challengeParam.SetLength(SHA256_LENGTH, fallible)) {
       continue;
     }
 
--- a/dom/u2f/U2F.h
+++ b/dom/u2f/U2F.h
@@ -81,23 +81,25 @@ protected:
 
 // Determine whether the provided Authenticator already knows
 // of the provided Registered Key.
 class U2FIsRegisteredTask final : public U2FPrepTask
 {
 public:
   U2FIsRegisteredTask(const Authenticator& aAuthenticator,
                       const LocalRegisteredKey& aRegisteredKey,
+                      const CryptoBuffer& aAppParam,
                       AbstractThread* aMainThread);
 
   NS_DECL_NSIRUNNABLE
 private:
   ~U2FIsRegisteredTask();
 
   LocalRegisteredKey mRegisteredKey;
+  CryptoBuffer mAppParam;
 };
 
 class U2FTask : public Runnable
 {
 public:
   U2FTask(const nsAString& aOrigin,
           const nsAString& aAppId,
           const Authenticator& aAuthenticator,
--- a/dom/webauthn/NSSU2FTokenRemote.cpp
+++ b/dom/webauthn/NSSU2FTokenRemote.cpp
@@ -32,30 +32,38 @@ NSSU2FTokenRemote::IsCompatibleVersion(c
         nsString(aVersionString), aIsCompatible)) {
     return NS_ERROR_FAILURE;
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 NSSU2FTokenRemote::IsRegistered(uint8_t* aKeyHandle, uint32_t aKeyHandleLen,
+                                uint8_t* aAppParam, uint32_t aAppParamLen,
                                 bool* aIsRegistered)
 {
   NS_ENSURE_ARG_POINTER(aKeyHandle);
+  NS_ENSURE_ARG_POINTER(aAppParam);
   NS_ENSURE_ARG_POINTER(aIsRegistered);
 
   nsTArray<uint8_t> keyHandle;
   if (!keyHandle.ReplaceElementsAt(0, keyHandle.Length(), aKeyHandle,
                                    aKeyHandleLen)) {
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
+  nsTArray<uint8_t> appParam;
+  if (!appParam.ReplaceElementsAt(0, appParam.Length(), aAppParam,
+                                  aAppParamLen)) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+
   ContentChild* cc = ContentChild::GetSingleton();
   MOZ_ASSERT(cc);
-  if (!cc->SendNSSU2FTokenIsRegistered(keyHandle, aIsRegistered)) {
+  if (!cc->SendNSSU2FTokenIsRegistered(keyHandle, appParam, aIsRegistered)) {
     return NS_ERROR_FAILURE;
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 NSSU2FTokenRemote::Register(uint8_t* aApplication,
                             uint32_t aApplicationLen,
--- a/dom/webauthn/WebAuthentication.cpp
+++ b/dom/webauthn/WebAuthentication.cpp
@@ -375,17 +375,18 @@ WebAuthentication::U2FAuthMakeCredential
       uint32_t len;
 
       // data is owned by the Descriptor, do don't free it here.
       if (NS_FAILED(ScopedCredentialGetData(scd, &data, &len))) {
         aRequest->SetFailure(NS_ERROR_DOM_UNKNOWN_ERR);
         return;
       }
 
-      nsresult rv = aToken->IsRegistered(data, len, &isRegistered);
+      nsresult rv = aToken->IsRegistered(data, len, aRpIdHash.Elements(),
+                                         aRpIdHash.Length(), &isRegistered);
       if (NS_WARN_IF(NS_FAILED(rv))) {
         aRequest->SetFailure(rv);
         return;
       }
 
       if (isRegistered) {
         aRequest->SetFailure(NS_ERROR_DOM_NOT_ALLOWED_ERR);
         return;
@@ -553,16 +554,18 @@ WebAuthentication::U2FAuthGetAssertion(c
   // 4.1.2.8.a If the timer for adjustedTimeout expires, then for each entry
   // in issuedRequests invoke the authenticatorCancel operation on that
   // authenticator and remove its entry from the list.
 
   for (CryptoBuffer& allowedCredential : aAllowList) {
     bool isRegistered = false;
     nsresult rv = aToken->IsRegistered(allowedCredential.Elements(),
                                        allowedCredential.Length(),
+                                       aRpIdHash.Elements(),
+                                       aRpIdHash.Length(),
                                        &isRegistered);
 
     // 4.1.2.8.b If any authenticator returns a status indicating that the user
     // cancelled the operation, delete that authenticator’s entry from
     // issuedRequests. For each remaining entry in issuedRequests invoke the
     // authenticatorCancel operation on that authenticator, and remove its entry
     // from the list.
 
--- a/security/manager/ssl/nsIU2FToken.idl
+++ b/security/manager/ssl/nsIU2FToken.idl
@@ -18,20 +18,23 @@ interface nsIU2FToken : nsISupports {
    * @return True if the offered version is compatible
    */
   void isCompatibleVersion(in AString version, [retval] out boolean result);
 
   /**
    * Return whether the provided KeyHandle belongs to this Token
    *
    * @param keyHandle Key Handle to evaluate.
+   * @param application The FIDO Application data to associate with the key.
    * @return True if the Key Handle is ours.
    */
   void isRegistered([array, size_is(keyHandleLen)] in octet keyHandle,
                     in uint32_t keyHandleLen,
+                    [array, size_is(applicationLen)] in octet application,
+                    in uint32_t applicationLen,
                     [retval] out boolean result);
 
   /**
    * Generates a public/private keypair for the provided application
    * and challenge, returning the pubkey, challenge response, and
    * key handle in the registration data.
    *
    * @param application The FIDO Application data to associate with the key.
--- a/security/manager/ssl/nsNSSU2FToken.cpp
+++ b/security/manager/ssl/nsNSSU2FToken.cpp
@@ -41,25 +41,31 @@ NS_NAMED_LITERAL_CSTRING(kAttestCertSubj
 // 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;
 NS_NAMED_LITERAL_STRING(kEcAlgorithm, 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;
 
+enum SoftTokenHandle {
+  Version1 = 0,
+};
+
 static mozilla::LazyLogModule gNSSTokenLog("webauth_u2f");
 
 nsNSSU2FToken::nsNSSU2FToken()
   : mInitialized(false)
 {}
 
 nsNSSU2FToken::~nsNSSU2FToken()
 {
@@ -375,84 +381,181 @@ nsNSSU2FToken::Init()
   }
 
   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
-// and aWrappingKey to convert aPrivKey.
+// 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& aWrappingKey,
+                        const UniquePK11SymKey& aPersistentKey,
+                        uint8_t* aAppParam, uint32_t aAppParamLen,
                         const UniqueSECKEYPrivateKey& aPrivKey,
                         const nsNSSShutDownPreventionLock&)
 {
   MOZ_ASSERT(aSlot);
-  MOZ_ASSERT(aWrappingKey);
+  MOZ_ASSERT(aPersistentKey);
+  MOZ_ASSERT(aAppParam);
   MOZ_ASSERT(aPrivKey);
-  if (!aSlot || !aWrappingKey || !aPrivKey) {
+  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 (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 (!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 (!wrappedKey) {
-      MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
-              ("Failed to allocate memory, NSS error #%d", PORT_GetError()));
+    MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory"));
     return nullptr;
   }
 
   UniqueSECItem param(PK11_ParamFromIV(CKM_NSS_AES_KEY_WRAP_PAD,
                                        /* default IV */ nullptr ));
 
-  SECStatus srv = PK11_WrapPrivKey(aSlot.get(), aWrappingKey.get(),
-                                   aPrivKey.get(), CKM_NSS_AES_KEY_WRAP_PAD,
-                                   param.get(), wrappedKey.get(),
-                                   /* wincx */ nullptr);
+  srv = PK11_WrapPrivKey(aSlot.get(), wrapKey.get(), aPrivKey.get(),
+                         CKM_NSS_AES_KEY_WRAP_PAD, param.get(), wrappedKey.get(),
+                         /* wincx */ nullptr);
   if (srv != SECSuccess) {
-      MOZ_LOG(gNSSTokenLog, LogLevel::Warning,
-              ("Failed to wrap U2F key, NSS error #%d", PORT_GetError()));
+    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 (!keyHandleBuf.SetCapacity(wrappedKey.get()->len + sizeof(saltParam) + 2,
+                                 mozilla::fallible)) {
+    MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory"));
     return nullptr;
   }
 
-  return wrappedKey;
+  // It's OK to ignore the return values here because we're writing into
+  // pre-allocated space
+  keyHandleBuf.AppendElement(SoftTokenHandle::Version1, mozilla::fallible);
+  keyHandleBuf.AppendElement(sizeof(saltParam), mozilla::fallible);
+  keyHandleBuf.AppendElements(saltParam, sizeof(saltParam), mozilla::fallible);
+  keyHandleBuf.AppendSECItem(wrappedKey.get());
+
+  UniqueSECItem keyHandle(SECITEM_AllocItem(nullptr, nullptr, 0));
+  if (!keyHandle) {
+    MOZ_LOG(gNSSTokenLog, LogLevel::Warning, ("Failed to allocate memory"));
+    return nullptr;
+  }
+
+  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
-// aWrappingKey and the AES Key Wrap algorithm.
+// the long-lived aPersistentKey mixed with aAppParam and the AES Key Wrap
+// algorithm.
 static UniqueSECKEYPrivateKey
 PrivateKeyFromKeyHandle(const UniquePK11SlotInfo& aSlot,
-                        const UniquePK11SymKey& aWrappingKey,
+                        const UniquePK11SymKey& aPersistentKey,
                         uint8_t* aKeyHandle, uint32_t aKeyHandleLen,
+                        uint8_t* aAppParam, uint32_t aAppParamLen,
                         const nsNSSShutDownPreventionLock&)
 {
   MOZ_ASSERT(aSlot);
-  MOZ_ASSERT(aWrappingKey);
+  MOZ_ASSERT(aPersistentKey);
   MOZ_ASSERT(aKeyHandle);
-  if (!aSlot || !aWrappingKey || !aKeyHandle) {
+  MOZ_ASSERT(aAppParam);
+  MOZ_ASSERT(aAppParamLen == SHA256_LENGTH);
+  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 (aKeyHandleLen != kVersion1KeyHandleLen) {
+    return nullptr;
+  }
+
+  if (aKeyHandle[0] != SoftTokenHandle::Version1) {
+    // Unrecognized version
+    return nullptr;
+  }
+
+  uint8_t saltLen = aKeyHandle[1];
+  uint8_t* saltPtr = aKeyHandle + 2;
+  if (saltLen != kSaltByteLen) {
     return nullptr;
   }
 
-  ScopedAutoSECItem pubKey(kPublicKeyLen);
+  // 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) };
 
-  ScopedAutoSECItem keyHandleItem(aKeyHandleLen);
-  memcpy(keyHandleItem.data, aKeyHandle, keyHandleItem.len);
+  // 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 (!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(), aWrappingKey.get(), CKM_NSS_AES_KEY_WRAP_PAD,
-                       param.get(), &keyHandleItem,
+    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 (!unwrappedKey) {
     // Not our key.
@@ -472,19 +575,21 @@ nsNSSU2FToken::IsCompatibleVersion(const
   MOZ_ASSERT(mInitialized);
   *aResult = (mVersion == aVersion);
   return NS_OK;
 }
 
 // IsRegistered determines if the provided key handle is usable by this token.
 NS_IMETHODIMP
 nsNSSU2FToken::IsRegistered(uint8_t* aKeyHandle, uint32_t aKeyHandleLen,
+                            uint8_t* aAppParam, uint32_t aAppParamLen,
                             bool* aResult)
 {
   NS_ENSURE_ARG_POINTER(aKeyHandle);
+  NS_ENSURE_ARG_POINTER(aAppParam);
   NS_ENSURE_ARG_POINTER(aResult);
 
   if (!NS_IsMainThread()) {
     NS_ERROR("nsNSSU2FToken::IsRegistered called off the main thread");
     return NS_ERROR_NOT_SAME_THREAD;
   }
 
   nsNSSShutDownPreventionLock locker;
@@ -499,16 +604,18 @@ nsNSSU2FToken::IsRegistered(uint8_t* aKe
 
   UniquePK11SlotInfo slot(PK11_GetInternalSlot());
   MOZ_ASSERT(slot.get());
 
   // Decode the key handle
   UniqueSECKEYPrivateKey privKey = PrivateKeyFromKeyHandle(slot, mWrappingKey,
                                                            aKeyHandle,
                                                            aKeyHandleLen,
+                                                           aAppParam,
+                                                           aAppParamLen,
                                                            locker);
   *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
@@ -578,16 +685,18 @@ nsNSSU2FToken::Register(uint8_t* aApplic
   UniqueSECKEYPublicKey pubKey;
   rv = GenEcKeypair(slot, privKey, pubKey, locker);
   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,
+                                                        aApplication,
+                                                        aApplicationLen,
                                                         privKey, locker);
   if (!keyHandleItem.get()) {
     return NS_ERROR_FAILURE;
   }
 
   // Sign the challenge using the Attestation privkey (from attestCert)
   mozilla::dom::CryptoBuffer signedDataBuf;
   if (!signedDataBuf.SetCapacity(1 + aApplicationLen + aChallengeLen +
@@ -690,16 +799,18 @@ nsNSSU2FToken::Sign(uint8_t* aApplicatio
 
     return NS_ERROR_ILLEGAL_VALUE;
   }
 
   // Decode the key handle
   UniqueSECKEYPrivateKey privKey = PrivateKeyFromKeyHandle(slot, mWrappingKey,
                                                            aKeyHandle,
                                                            aKeyHandleLen,
+                                                           aApplication,
+                                                           aApplicationLen,
                                                            locker);
   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
   uint32_t counter = Preferences::GetUint(PREF_U2F_NSSTOKEN_COUNTER) + 1;