Bug 1200341 - Implement HKDF for WebCrypto r=rbarnes,smaug a=lizzard
authorTim Taubert <tim@timtaubert.de>
Tue, 26 Jan 2016 14:57:52 +0100
changeset 316620 f05cc5ce1dc7ca7305df5598e7c40386c090b6c2
parent 316619 a28f8b557d3dccbc55d9f6f0b4459589372d9558
child 316621 fc92f3613e415a30b9b5701d5419aaa185f897d0
push id5703
push userraliiev@mozilla.com
push dateMon, 07 Mar 2016 14:18:41 +0000
treeherdermozilla-beta@31e373ad5b5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrbarnes, smaug, lizzard
bugs1200341
milestone46.0a2
Bug 1200341 - Implement HKDF for WebCrypto r=rbarnes,smaug a=lizzard
dom/crypto/WebCryptoCommon.h
dom/crypto/WebCryptoTask.cpp
dom/crypto/test/mochitest.ini
dom/crypto/test/test-vectors.js
dom/crypto/test/test_WebCrypto_HKDF.html
dom/crypto/test/test_WebCrypto_PBKDF2.html
dom/crypto/test/util.js
dom/webidl/SubtleCrypto.webidl
--- a/dom/crypto/WebCryptoCommon.h
+++ b/dom/crypto/WebCryptoCommon.h
@@ -18,16 +18,17 @@
 #define WEBCRYPTO_ALG_AES_CTR       "AES-CTR"
 #define WEBCRYPTO_ALG_AES_GCM       "AES-GCM"
 #define WEBCRYPTO_ALG_AES_KW        "AES-KW"
 #define WEBCRYPTO_ALG_SHA1          "SHA-1"
 #define WEBCRYPTO_ALG_SHA256        "SHA-256"
 #define WEBCRYPTO_ALG_SHA384        "SHA-384"
 #define WEBCRYPTO_ALG_SHA512        "SHA-512"
 #define WEBCRYPTO_ALG_HMAC          "HMAC"
+#define WEBCRYPTO_ALG_HKDF          "HKDF"
 #define WEBCRYPTO_ALG_PBKDF2        "PBKDF2"
 #define WEBCRYPTO_ALG_RSASSA_PKCS1  "RSASSA-PKCS1-v1_5"
 #define WEBCRYPTO_ALG_RSA_OAEP      "RSA-OAEP"
 #define WEBCRYPTO_ALG_ECDH          "ECDH"
 #define WEBCRYPTO_ALG_ECDSA         "ECDSA"
 #define WEBCRYPTO_ALG_DH            "DH"
 
 // WebCrypto key formats
@@ -233,16 +234,18 @@ NormalizeToken(const nsString& aName, ns
   } else if (NORMALIZED_EQUALS(aName, WEBCRYPTO_ALG_SHA256)) {
     aDest.AssignLiteral(WEBCRYPTO_ALG_SHA256);
   } else if (NORMALIZED_EQUALS(aName, WEBCRYPTO_ALG_SHA384)) {
     aDest.AssignLiteral(WEBCRYPTO_ALG_SHA384);
   } else if (NORMALIZED_EQUALS(aName, WEBCRYPTO_ALG_SHA512)) {
     aDest.AssignLiteral(WEBCRYPTO_ALG_SHA512);
   } else if (NORMALIZED_EQUALS(aName, WEBCRYPTO_ALG_HMAC)) {
     aDest.AssignLiteral(WEBCRYPTO_ALG_HMAC);
+  } else if (NORMALIZED_EQUALS(aName, WEBCRYPTO_ALG_HKDF)) {
+    aDest.AssignLiteral(WEBCRYPTO_ALG_HKDF);
   } else if (NORMALIZED_EQUALS(aName, WEBCRYPTO_ALG_PBKDF2)) {
     aDest.AssignLiteral(WEBCRYPTO_ALG_PBKDF2);
   } else if (NORMALIZED_EQUALS(aName, WEBCRYPTO_ALG_RSASSA_PKCS1)) {
     aDest.AssignLiteral(WEBCRYPTO_ALG_RSASSA_PKCS1);
   } else if (NORMALIZED_EQUALS(aName, WEBCRYPTO_ALG_RSA_OAEP)) {
     aDest.AssignLiteral(WEBCRYPTO_ALG_RSA_OAEP);
   } else if (NORMALIZED_EQUALS(aName, WEBCRYPTO_ALG_ECDH)) {
     aDest.AssignLiteral(WEBCRYPTO_ALG_ECDH);
--- a/dom/crypto/WebCryptoTask.cpp
+++ b/dom/crypto/WebCryptoTask.cpp
@@ -65,16 +65,17 @@ enum TelemetryAlgorithm {
   TA_SHA_256         = 16,
   TA_SHA_384         = 17,
   TA_SHA_512         = 18,
   // Later additions
   TA_AES_KW          = 19,
   TA_ECDH            = 20,
   TA_PBKDF2          = 21,
   TA_ECDSA           = 22,
+  TA_HKDF            = 23,
 };
 
 // Convenience functions for extracting / converting information
 
 // OOM-safe CryptoBuffer initialization, suitable for constructors
 #define ATTEMPT_BUFFER_INIT(dst, src) \
   if (!dst.Assign(src)) { \
     mEarlyRv = NS_ERROR_DOM_UNKNOWN_ERR; \
@@ -834,19 +835,16 @@ public:
 
       if (params.mLabel.WasPassed()) {
         ATTEMPT_BUFFER_INIT(mLabel, params.mLabel.Value());
       }
     }
     // Otherwise mLabel remains the empty octet string, as intended
 
     // Look up the MGF based on the KeyAlgorithm.
-    // static_cast is safe because we only get here if the algorithm name
-    // is RSA-OAEP, and that only happens if we've constructed
-    // an RsaHashedKeyAlgorithm.
     mHashMechanism = KeyAlgorithmProxy::GetMechanism(aKey.Algorithm().mRsa.mHash);
 
     switch (mHashMechanism) {
       case CKM_SHA_1:
         mMgfMechanism = CKG_MGF1_SHA1; break;
       case CKM_SHA256:
         mMgfMechanism = CKG_MGF1_SHA256; break;
       case CKM_SHA384:
@@ -1438,16 +1436,23 @@ public:
       const ObjectOrString& aAlgorithm, bool aExtractable,
       const Sequence<nsString>& aKeyUsages)
   {
     ImportKeyTask::Init(aCx, aFormat, aAlgorithm, aExtractable, aKeyUsages);
     if (NS_FAILED(mEarlyRv)) {
       return;
     }
 
+    // This task only supports raw and JWK format.
+    if (!mFormat.EqualsLiteral(WEBCRYPTO_KEY_FORMAT_JWK) &&
+        !mFormat.EqualsLiteral(WEBCRYPTO_KEY_FORMAT_RAW)) {
+      mEarlyRv = NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+      return;
+    }
+
     // If this is an HMAC key, import the hash name
     if (mAlgName.EqualsLiteral(WEBCRYPTO_ALG_HMAC)) {
       RootedDictionary<HmacImportParams> params(aCx);
       mEarlyRv = Coerce(aCx, params, aAlgorithm);
       if (NS_FAILED(mEarlyRv)) {
         mEarlyRv = NS_ERROR_DOM_SYNTAX_ERR;
         return;
       }
@@ -1502,24 +1507,25 @@ public:
         return NS_ERROR_DOM_DATA_ERR;
       }
       mKey->Algorithm().MakeAes(mAlgName, length);
 
       if (mDataIsJwk && mJwk.mUse.WasPassed() &&
           !mJwk.mUse.Value().EqualsLiteral(JWK_USE_ENC)) {
         return NS_ERROR_DOM_DATA_ERR;
       }
-    } else if (mAlgName.EqualsLiteral(WEBCRYPTO_ALG_PBKDF2)) {
+    } else if (mAlgName.EqualsLiteral(WEBCRYPTO_ALG_HKDF) ||
+               mAlgName.EqualsLiteral(WEBCRYPTO_ALG_PBKDF2)) {
       if (mKey->HasUsageOtherThan(CryptoKey::DERIVEKEY | CryptoKey::DERIVEBITS)) {
         return NS_ERROR_DOM_DATA_ERR;
       }
       mKey->Algorithm().MakeAes(mAlgName, length);
 
       if (mDataIsJwk && mJwk.mUse.WasPassed()) {
-        // There is not a 'use' value consistent with PBKDF
+        // There is not a 'use' value consistent with PBKDF or HKDF
         return NS_ERROR_DOM_DATA_ERR;
       };
     } else if (mAlgName.EqualsLiteral(WEBCRYPTO_ALG_HMAC)) {
       if (mKey->HasUsageOtherThan(CryptoKey::SIGN | CryptoKey::VERIFY)) {
         return NS_ERROR_DOM_DATA_ERR;
       }
 
       mKey->Algorithm().MakeHmac(length, mHashName);
@@ -2492,16 +2498,166 @@ GenerateAsymmetricKeyTask::Resolve()
 }
 
 void
 GenerateAsymmetricKeyTask::Cleanup()
 {
   mKeyPair = nullptr;
 }
 
+class DeriveHkdfBitsTask : public ReturnArrayBufferViewTask
+{
+public:
+  DeriveHkdfBitsTask(JSContext* aCx,
+      const ObjectOrString& aAlgorithm, CryptoKey& aKey, uint32_t aLength)
+    : mSymKey(aKey.GetSymKey())
+  {
+    Init(aCx, aAlgorithm, aKey, aLength);
+  }
+
+  DeriveHkdfBitsTask(JSContext* aCx, const ObjectOrString& aAlgorithm,
+                      CryptoKey& aKey, const ObjectOrString& aTargetAlgorithm)
+    : mSymKey(aKey.GetSymKey())
+  {
+    size_t length;
+    mEarlyRv = GetKeyLengthForAlgorithm(aCx, aTargetAlgorithm, length);
+
+    if (NS_SUCCEEDED(mEarlyRv)) {
+      Init(aCx, aAlgorithm, aKey, length);
+    }
+  }
+
+  void Init(JSContext* aCx, const ObjectOrString& aAlgorithm, CryptoKey& aKey,
+            uint32_t aLength)
+  {
+    Telemetry::Accumulate(Telemetry::WEBCRYPTO_ALG, TA_HKDF);
+    CHECK_KEY_ALGORITHM(aKey.Algorithm(), WEBCRYPTO_ALG_HKDF);
+
+    // Check that we have a key.
+    if (mSymKey.Length() == 0) {
+      mEarlyRv = NS_ERROR_DOM_INVALID_ACCESS_ERR;
+      return;
+    }
+
+    RootedDictionary<HkdfParams> params(aCx);
+    mEarlyRv = Coerce(aCx, params, aAlgorithm);
+    if (NS_FAILED(mEarlyRv)) {
+      mEarlyRv = NS_ERROR_DOM_SYNTAX_ERR;
+      return;
+    }
+
+    // length must be greater than zero.
+    if (aLength == 0) {
+      mEarlyRv = NS_ERROR_DOM_DATA_ERR;
+      return;
+    }
+
+    // Extract the hash algorithm.
+    nsString hashName;
+    mEarlyRv = GetAlgorithmName(aCx, params.mHash, hashName);
+    if (NS_FAILED(mEarlyRv)) {
+      return;
+    }
+
+    // Check the given hash algorithm.
+    switch (MapAlgorithmNameToMechanism(hashName)) {
+      case CKM_SHA_1: mMechanism = CKM_NSS_HKDF_SHA1; break;
+      case CKM_SHA256: mMechanism = CKM_NSS_HKDF_SHA256; break;
+      case CKM_SHA384: mMechanism = CKM_NSS_HKDF_SHA384; break;
+      case CKM_SHA512: mMechanism = CKM_NSS_HKDF_SHA512; break;
+      default:
+        mEarlyRv = NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+        return;
+    }
+
+    ATTEMPT_BUFFER_INIT(mSalt, params.mSalt)
+    ATTEMPT_BUFFER_INIT(mInfo, params.mInfo)
+    mLengthInBytes = ceil((double)aLength / 8);
+    mLengthInBits = aLength;
+  }
+
+private:
+  size_t mLengthInBits;
+  size_t mLengthInBytes;
+  CryptoBuffer mSalt;
+  CryptoBuffer mInfo;
+  CryptoBuffer mSymKey;
+  CK_MECHANISM_TYPE mMechanism;
+
+  virtual nsresult DoCrypto() override
+  {
+    ScopedPLArenaPool arena(PORT_NewArena(DER_DEFAULT_CHUNKSIZE));
+    if (!arena) {
+      return NS_ERROR_DOM_OPERATION_ERR;
+    }
+
+    // Import the key
+    SECItem keyItem = { siBuffer, nullptr, 0 };
+    ATTEMPT_BUFFER_TO_SECITEM(arena, &keyItem, mSymKey);
+
+    ScopedPK11SlotInfo slot(PK11_GetInternalSlot());
+    if (!slot.get()) {
+      return NS_ERROR_DOM_OPERATION_ERR;
+    }
+
+    ScopedPK11SymKey baseKey(PK11_ImportSymKey(slot, mMechanism,
+                                               PK11_OriginUnwrap, CKA_WRAP,
+                                               &keyItem, nullptr));
+    if (!baseKey) {
+      return NS_ERROR_DOM_INVALID_ACCESS_ERR;
+    }
+
+    SECItem salt = { siBuffer, nullptr, 0 };
+    SECItem info = { siBuffer, nullptr, 0 };
+    ATTEMPT_BUFFER_TO_SECITEM(arena, &salt, mSalt);
+    ATTEMPT_BUFFER_TO_SECITEM(arena, &info, mInfo);
+
+    CK_NSS_HKDFParams hkdfParams = { true, salt.data, salt.len,
+                                     true, info.data, info.len };
+    SECItem params = { siBuffer, (unsigned char*)&hkdfParams,
+                       sizeof(hkdfParams) };
+
+    // CKM_SHA512_HMAC and CKA_SIGN are key type and usage attributes of the
+    // derived symmetric key and don't matter because we ignore them anyway.
+    ScopedPK11SymKey symKey(PK11_Derive(baseKey, mMechanism, &params,
+                                        CKM_SHA512_HMAC, CKA_SIGN,
+                                        mLengthInBytes));
+
+    if (!symKey.get()) {
+      return NS_ERROR_DOM_OPERATION_ERR;
+    }
+
+    nsresult rv = MapSECStatus(PK11_ExtractKeyValue(symKey));
+    if (NS_FAILED(rv)) {
+      return NS_ERROR_DOM_OPERATION_ERR;
+    }
+
+    // This doesn't leak, because the SECItem* returned by PK11_GetKeyData
+    // just refers to a buffer managed by symKey. The assignment copies the
+    // data, so mResult manages one copy, while symKey manages another.
+    ATTEMPT_BUFFER_ASSIGN(mResult, PK11_GetKeyData(symKey));
+
+    if (mLengthInBytes > mResult.Length()) {
+      return NS_ERROR_DOM_DATA_ERR;
+    }
+
+    if (!mResult.SetLength(mLengthInBytes, fallible)) {
+      return NS_ERROR_DOM_UNKNOWN_ERR;
+    }
+
+    // If the number of bits to derive is not a multiple of 8 we need to
+    // zero out the remaining bits that were derived but not requested.
+    if (mLengthInBits % 8) {
+      mResult[mResult.Length() - 1] &= 0xff << (mLengthInBits % 8);
+    }
+
+    return NS_OK;
+  }
+};
+
 class DerivePbkdfBitsTask : public ReturnArrayBufferViewTask
 {
 public:
   DerivePbkdfBitsTask(JSContext* aCx,
       const ObjectOrString& aAlgorithm, CryptoKey& aKey, uint32_t aLength)
     : mSymKey(aKey.GetSymKey())
   {
     Init(aCx, aAlgorithm, aKey, aLength);
@@ -3098,16 +3254,17 @@ WebCryptoTask::CreateImportKeyTask(JSCon
 
   // SPEC-BUG: PBKDF2 is not supposed to be supported for this operation.
   // However, the spec should be updated to allow it.
   if (algName.EqualsLiteral(WEBCRYPTO_ALG_AES_CBC) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_AES_CTR) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_AES_GCM) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_AES_KW) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_PBKDF2) ||
+      algName.EqualsLiteral(WEBCRYPTO_ALG_HKDF) ||
       algName.EqualsLiteral(WEBCRYPTO_ALG_HMAC)) {
     return new ImportSymmetricKeyTask(aCx, aFormat, aKeyData, aAlgorithm,
                                       aExtractable, aKeyUsages);
   } else if (algName.EqualsLiteral(WEBCRYPTO_ALG_RSASSA_PKCS1) ||
              algName.EqualsLiteral(WEBCRYPTO_ALG_RSA_OAEP)) {
     return new ImportRsaKeyTask(aCx, aFormat, aKeyData, aAlgorithm,
                                 aExtractable, aKeyUsages);
   } else if (algName.EqualsLiteral(WEBCRYPTO_ALG_ECDH) ||
@@ -3222,16 +3379,22 @@ WebCryptoTask::CreateDeriveKeyTask(JSCon
   }
 
   nsString algName;
   nsresult rv = GetAlgorithmName(aCx, aAlgorithm, algName);
   if (NS_FAILED(rv)) {
     return new FailureTask(rv);
   }
 
+  if (algName.EqualsASCII(WEBCRYPTO_ALG_HKDF)) {
+    return new DeriveKeyTask<DeriveHkdfBitsTask>(aCx, aAlgorithm, aBaseKey,
+                                                 aDerivedKeyType, aExtractable,
+                                                 aKeyUsages);
+  }
+
   if (algName.EqualsASCII(WEBCRYPTO_ALG_PBKDF2)) {
     return new DeriveKeyTask<DerivePbkdfBitsTask>(aCx, aAlgorithm, aBaseKey,
                                                   aDerivedKeyType, aExtractable,
                                                   aKeyUsages);
   }
 
   if (algName.EqualsASCII(WEBCRYPTO_ALG_ECDH)) {
     return new DeriveKeyTask<DeriveEcdhBitsTask>(aCx, aAlgorithm, aBaseKey,
@@ -3268,16 +3431,20 @@ WebCryptoTask::CreateDeriveBitsTask(JSCo
   if (algName.EqualsASCII(WEBCRYPTO_ALG_ECDH)) {
     return new DeriveEcdhBitsTask(aCx, aAlgorithm, aKey, aLength);
   }
 
   if (algName.EqualsASCII(WEBCRYPTO_ALG_DH)) {
     return new DeriveDhBitsTask(aCx, aAlgorithm, aKey, aLength);
   }
 
+  if (algName.EqualsASCII(WEBCRYPTO_ALG_HKDF)) {
+    return new DeriveHkdfBitsTask(aCx, aAlgorithm, aKey, aLength);
+  }
+
   return new FailureTask(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
 }
 
 WebCryptoTask*
 WebCryptoTask::CreateWrapKeyTask(JSContext* aCx,
                                  const nsAString& aFormat,
                                  CryptoKey& aKey,
                                  CryptoKey& aWrappingKey,
@@ -3353,16 +3520,17 @@ WebCryptoTask::CreateUnwrapKeyTask(JSCon
     return new FailureTask(rv);
   }
 
   CryptoOperationData dummy;
   RefPtr<ImportKeyTask> importTask;
   if (keyAlgName.EqualsASCII(WEBCRYPTO_ALG_AES_CBC) ||
       keyAlgName.EqualsASCII(WEBCRYPTO_ALG_AES_CTR) ||
       keyAlgName.EqualsASCII(WEBCRYPTO_ALG_AES_GCM) ||
+      keyAlgName.EqualsASCII(WEBCRYPTO_ALG_HKDF) ||
       keyAlgName.EqualsASCII(WEBCRYPTO_ALG_HMAC)) {
     importTask = new ImportSymmetricKeyTask(aCx, aFormat,
                                             aUnwrappedKeyAlgorithm,
                                             aExtractable, aKeyUsages);
   } else if (keyAlgName.EqualsASCII(WEBCRYPTO_ALG_RSASSA_PKCS1) ||
              keyAlgName.EqualsASCII(WEBCRYPTO_ALG_RSA_OAEP)) {
     importTask = new ImportRsaKeyTask(aCx, aFormat,
                                       aUnwrappedKeyAlgorithm,
--- a/dom/crypto/test/mochitest.ini
+++ b/dom/crypto/test/mochitest.ini
@@ -9,14 +9,15 @@ support-files =
   util.js
 
 [test_indexedDB.html]
 skip-if = toolkit == 'android' # bug 1200570
 [test_WebCrypto.html]
 [test_WebCrypto_DH.html]
 [test_WebCrypto_ECDH.html]
 [test_WebCrypto_ECDSA.html]
+[test_WebCrypto_HKDF.html]
 [test_WebCrypto_JWK.html]
 [test_WebCrypto_Normalize.html]
 [test_WebCrypto_PBKDF2.html]
 [test_WebCrypto_Reject_Generating_Keys_Without_Usages.html]
 [test_WebCrypto_RSA_OAEP.html]
 [test_WebCrypto_Wrap_Unwrap.html]
--- a/dom/crypto/test/test-vectors.js
+++ b/dom/crypto/test/test-vectors.js
@@ -447,17 +447,22 @@ tv = {
   pbkdf2_sha1: {
     password: new TextEncoder("utf-8").encode("passwordPASSWORDpassword"),
     salt: new TextEncoder("utf-8").encode("saltSALTsaltSALTsaltSALTsaltSALTsalt"),
     iterations: 4096,
     length: 25 * 8,
 
     derived: util.hex2abv(
       "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038"
-    )
+    ),
+
+    jwk: {
+      kty: "oct",
+      k: "cGFzc3dvcmRQQVNTV09SRHBhc3N3b3Jk"
+    }
   },
 
   // https://stackoverflow.com/questions/5130513/pbkdf2-hmac-sha2-test-vectors
   pbkdf2_sha256: {
     password: new TextEncoder("utf-8").encode("passwordPASSWORDpassword"),
     salt: new TextEncoder("utf-8").encode("saltSALTsaltSALTsaltSALTsaltSALTsalt"),
     iterations: 4096,
     length: 40 * 8,
@@ -748,9 +753,114 @@ tv = {
       "3826dcef3a1586956105ebea805d871f34c46c25bc30fc66b2db26cb0a930000" +
       "038184000281804fc9904887ac7fabff87f054003547c2d9458c1f6f584c140d" +
       "7271f8b266bb390af7e3f625a629bec9c6a057a4cbe1a556d5e3eb2ff1c6ff67" +
       "7a08b0c7c509110b9e7c6dbc961ca4360362d3dbcffc5bf2bb7207e0a5922f77" +
       "cf5464b316aa49fb62b338ebcdb30bf573d07b663bb7777b69d6317df0a4f636" +
       "ba3d9acbf9e8ac"
     )
   },
+
+  // Taken from appendix A of RFC 5869.
+  // <https://tools.ietf.org/html/rfc5869>
+  hkdf: [
+    {
+      prf: "SHA-256",
+      key: util.hex2abv("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"),
+      salt: util.hex2abv("000102030405060708090a0b0c"),
+      info: util.hex2abv("f0f1f2f3f4f5f6f7f8f9"),
+      data: util.hex2abv(
+        "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf" +
+        "34007208d5b887185865"
+      ),
+      jwk: {
+        kty: "oct",
+        k: "CwsLCwsLCwsLCwsLCwsLCwsLCwsLCw"
+      }
+    },
+    {
+      prf: "SHA-256",
+      key: util.hex2abv(
+        "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" +
+        "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" +
+        "404142434445464748494a4b4c4d4e4f"
+      ),
+      salt: util.hex2abv(
+        "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f" +
+        "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f" +
+        "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"
+      ),
+      info: util.hex2abv(
+        "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf" +
+        "d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef" +
+        "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
+      ),
+      data: util.hex2abv(
+        "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c" +
+        "59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71" +
+        "cc30c58179ec3e87c14c01d5c1f3434f1d87"
+      )
+    },
+    {
+      prf: "SHA-256",
+      key: util.hex2abv("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"),
+      salt: util.hex2abv(""),
+      info: util.hex2abv(""),
+      data: util.hex2abv(
+        "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d" +
+        "9d201395faa4b61a96c8"
+      )
+    },
+    {
+      prf: "SHA-1",
+      key: util.hex2abv("0b0b0b0b0b0b0b0b0b0b0b"),
+      salt: util.hex2abv("000102030405060708090a0b0c"),
+      info: util.hex2abv("f0f1f2f3f4f5f6f7f8f9"),
+      data: util.hex2abv(
+        "085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2" +
+        "c22e422478d305f3f896"
+      )
+    },
+    {
+      prf: "SHA-1",
+      key: util.hex2abv(
+        "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" +
+        "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" +
+        "404142434445464748494a4b4c4d4e4f"
+      ),
+      salt: util.hex2abv(
+        "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f" +
+        "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f" +
+        "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"
+      ),
+      info: util.hex2abv(
+        "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf" +
+        "d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef" +
+        "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
+      ),
+      data: util.hex2abv(
+        "0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe" +
+        "8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e" +
+        "927336d0441f4c4300e2cff0d0900b52d3b4"
+      )
+    },
+    {
+      prf: "SHA-1",
+      key: util.hex2abv("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"),
+      salt: util.hex2abv(""),
+      info: util.hex2abv(""),
+      data: util.hex2abv(
+        "0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0" +
+        "ea00033de03984d34918"
+      )
+    },
+    {
+      prf: "SHA-1",
+      key: util.hex2abv("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c"),
+      salt: util.hex2abv(""),
+      info: util.hex2abv(""),
+      data: util.hex2abv(
+        "2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5" +
+        "673a081d70cce7acfc48"
+      )
+    }
+  ]
 }
new file mode 100644
--- /dev/null
+++ b/dom/crypto/test/test_WebCrypto_HKDF.html
@@ -0,0 +1,351 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+<title>WebCrypto Test Suite</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+<link rel="stylesheet" href="./test_WebCrypto.css"/>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+
+<!-- Utilities for manipulating ABVs -->
+<script src="util.js"></script>
+
+<!-- A simple wrapper around IndexedDB -->
+<script src="simpledb.js"></script>
+
+<!-- Test vectors drawn from the literature -->
+<script src="./test-vectors.js"></script>
+
+<!-- General testing framework -->
+<script src="./test-array.js"></script>
+
+<script>/*<![CDATA[*/
+"use strict";
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "Deriving zero bits should fail",
+  function() {
+    var that = this;
+    var key = util.hex2abv("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+
+    var alg = {
+      name: "HKDF",
+      hash: "SHA-256",
+      salt: new Uint8Array(),
+      info: new Uint8Array()
+    };
+
+    crypto.subtle.importKey("raw", key, "HKDF", false, ["deriveBits"])
+      .then(x => crypto.subtle.deriveBits(alg, x, 0), error(that))
+      .then(error(that), complete(that));
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "Derive four bits with HKDF, no salt or info given",
+  function() {
+    var that = this;
+    var key = util.hex2abv("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+
+    var alg = {
+      name: "HKDF",
+      hash: "SHA-256",
+      salt: new Uint8Array(),
+      info: new Uint8Array()
+    };
+
+    crypto.subtle.importKey("raw", key, "HKDF", false, ["deriveBits"])
+      .then(x => crypto.subtle.deriveBits(alg, x, 4))
+      // The last 4 bits should be zeroes (1000 1101 => 1000 0000).
+      .then(memcmp_complete(that, new Uint8Array([0x80])))
+      .catch(error(that));
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "Deriving too many bits should fail",
+  function() {
+    var that = this;
+    var key = util.hex2abv("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+
+    var alg = {
+      name: "HKDF",
+      hash: "SHA-256",
+      salt: new Uint8Array(),
+      info: new Uint8Array()
+    };
+
+    function deriveBits(x) {
+      // The maximum length (in bytes) of output material for HKDF is 255 times
+      // the digest length. In this case, the digest length (in bytes) of
+      // SHA-256 is 32; 32*255 = 8160. deriveBits expects the length to be in
+      // bits, so 8160*8=65280 and add 1 to exceed the maximum length.
+      return crypto.subtle.deriveBits(alg, x, 65281);
+    }
+
+    crypto.subtle.importKey("raw", key, "HKDF", false, ["deriveBits"])
+      .then(deriveBits, error(that))
+      .then(error(that), complete(that));
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "Deriving with an unsupported PRF should fail",
+  function() {
+    var that = this;
+    var key = util.hex2abv("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+
+    var alg = {
+      name: "HKDF",
+      hash: "HMAC",
+      salt: new Uint8Array(),
+      info: new Uint8Array()
+    };
+
+    function deriveBits(x) {
+      return crypto.subtle.deriveBits(alg, x, 4);
+    }
+
+    crypto.subtle.importKey("raw", key, "HKDF", false, ["deriveBits"])
+      .then(deriveBits, error(that))
+      .then(error(that), complete(that));
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "Deriving with a non-HKDF key should fail",
+  function() {
+    var that = this;
+
+    var alg = {
+      name: "HKDF",
+      hash: "HMAC",
+      salt: new Uint8Array(),
+      info: new Uint8Array()
+    };
+
+    function deriveBits(x) {
+      return crypto.subtle.deriveBits(alg, x, 4);
+    }
+
+    var ecAlg = {name: "ECDH", namedCurve: "P-256"};
+    crypto.subtle.generateKey(ecAlg, false, ["deriveBits"])
+      .then(deriveBits, error(that))
+      .then(error(that), complete(that));
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "Derive known values from test vectors (SHA-1 and SHA-256)",
+  function() {
+    var that = this;
+    var tests = tv.hkdf.slice();
+
+    function next() {
+      if (!tests.length) {
+        return;
+      }
+
+      var test = tests.shift();
+      var {key, data} = test;
+
+      return crypto.subtle.importKey("raw", key, "HKDF", false, ["deriveBits"])
+        .then(function (key) {
+          return crypto.subtle.deriveBits({
+            name: "HKDF",
+            hash: test.prf,
+            salt: test.salt,
+            info: test.info
+          }, key, test.data.byteLength * 8);
+        })
+        .then(function (data) {
+          if (!util.memcmp(data, test.data)) {
+            throw new Error("derived bits don't match expected value");
+          }
+
+          // Next test vector.
+          return next();
+        });
+    }
+
+    next().then(complete(that), error(that));
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "Derive known values from test vectors (JWK, SHA-256)",
+  function() {
+    var that = this;
+    var test = tv.hkdf[0];
+    var alg = {
+      name: "HKDF",
+      hash: test.prf,
+      salt: test.salt,
+      info: test.info
+    };
+
+    crypto.subtle.importKey("jwk", test.jwk, "HKDF", false, ["deriveBits"])
+      .then(x => crypto.subtle.deriveBits(alg, x, test.data.byteLength * 8))
+      .then(memcmp_complete(that, test.data), error(that));
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "Test wrapping/unwrapping an HKDF key",
+  function() {
+    var that = this;
+    var hkdfKey = util.hex2abv("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+    var alg = {name: "AES-GCM", length: 256, iv: new Uint8Array(16)};
+    var wrappingKey;
+
+    function wrap(x) {
+      wrappingKey = x;
+      return crypto.subtle.encrypt(alg, wrappingKey, hkdfKey);
+    }
+
+    function unwrap(wrappedKey) {
+      return crypto.subtle.unwrapKey(
+        "raw", wrappedKey, wrappingKey, alg, "HKDF", false, ["deriveBits"])
+        .then(rawKey => {
+          return crypto.subtle.deriveBits({
+            name: "HKDF",
+            hash: "SHA-256",
+            salt: new Uint8Array(),
+            info: new Uint8Array()
+          }, rawKey, 4);
+        })
+        .then(derivedBits => {
+          if (!util.memcmp(derivedBits, new Uint8Array([0x80]))) {
+            throw new Error("deriving bits failed");
+          }
+
+          // Forward to reuse.
+          return wrappedKey;
+        });
+    }
+
+    crypto.subtle.generateKey(alg, false, ["encrypt", "unwrapKey"])
+      .then(wrap)
+      .then(unwrap)
+      .then(complete(that), error(that));
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "Unwrapping an HKDF key in PKCS8 format should fail",
+  function() {
+    var that = this;
+    var hkdfKey = util.hex2abv("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+    var alg = {name: "AES-GCM", length: 256, iv: new Uint8Array(16)};
+    var wrappingKey;
+
+    function wrap(x) {
+      wrappingKey = x;
+      return crypto.subtle.encrypt(alg, wrappingKey, hkdfKey);
+    }
+
+    function unwrap(x) {
+      return crypto.subtle.unwrapKey(
+        "pkcs8", x, wrappingKey, alg, "HKDF", false, ["deriveBits"]);
+    }
+
+    crypto.subtle.generateKey(alg, false, ["encrypt", "unwrapKey"])
+      .then(wrap, error(that))
+      .then(unwrap, error(that))
+      .then(error(that), complete(that));
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "Derive an AES key using with HKDF",
+  function() {
+    var that = this;
+    var key = util.hex2abv("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+
+    var alg = {
+      name: "HKDF",
+      hash: "SHA-256",
+      salt: new Uint8Array(),
+      info: new Uint8Array()
+    };
+
+    function deriveKey(x) {
+      var targetAlg = {name: "AES-GCM", length: 256};
+      return crypto.subtle.deriveKey(alg, x, targetAlg, false, ["encrypt"]);
+    }
+
+    crypto.subtle.importKey("raw", key, "HKDF", false, ["deriveKey"])
+      .then(deriveKey)
+      .then(complete(that), error(that))
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
+  "Deriving an HKDF key with HKDF should fail",
+  function() {
+    var that = this;
+    var key = util.hex2abv("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+
+    var alg = {
+      name: "HKDF",
+      hash: "SHA-256",
+      salt: new Uint8Array(),
+      info: new Uint8Array()
+    };
+
+    function deriveKey(x) {
+      return crypto.subtle.deriveKey(alg, x, "HKDF", false, ["deriveBits"]);
+    }
+
+    crypto.subtle.importKey("raw", key, "HKDF", false, ["deriveKey"])
+      .then(deriveKey)
+      .then(error(that), complete(that))
+  }
+);
+
+/*]]>*/</script>
+</head>
+
+<body>
+
+<div id="content">
+	<div id="head">
+		<b>Web</b>Crypto<br>
+	</div>
+
+    <div id="start" onclick="start();">RUN ALL</div>
+
+    <div id="resultDiv" class="content">
+    Summary:
+    <span class="pass"><span id="passN">0</span> passed, </span>
+    <span class="fail"><span id="failN">0</span> failed, </span>
+    <span class="pending"><span id="pendingN">0</span> pending.</span>
+    <br/>
+    <br/>
+
+    <table id="results">
+        <tr>
+            <th>Test</th>
+            <th>Result</th>
+            <th>Time</th>
+        </tr>
+    </table>
+
+    </div>
+
+    <div id="foot"></div>
+</div>
+
+</body>
+</html>
--- a/dom/crypto/test/test_WebCrypto_PBKDF2.html
+++ b/dom/crypto/test/test_WebCrypto_PBKDF2.html
@@ -34,16 +34,42 @@ TestArray.addTest(
       complete(that, hasKeyFields),
       error(that)
     );
   }
 );
 
 // -----------------------------------------------------------------------------
 TestArray.addTest(
+  "Unwrapping a PBKDF2 key in PKCS8 format should fail",
+  function() {
+    var that = this;
+    var pbkdf2Key = new TextEncoder("utf-8").encode("password");
+    var alg = {name: "AES-GCM", length: 256, iv: new Uint8Array(16)};
+    var wrappingKey;
+
+    function wrap(x) {
+      wrappingKey = x;
+      return crypto.subtle.encrypt(alg, wrappingKey, pbkdf2Key);
+    }
+
+    function unwrap(x) {
+      return crypto.subtle.unwrapKey(
+        "pkcs8", x, wrappingKey, alg, "PBKDF2", false, ["deriveBits"]);
+    }
+
+    crypto.subtle.generateKey(alg, false, ["encrypt", "unwrapKey"])
+      .then(wrap, error(that))
+      .then(unwrap, error(that))
+      .then(error(that), complete(that));
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
   "Import raw PBKDF2 key and derive bits using HMAC-SHA-1",
   function() {
     var that = this;
     var alg = "PBKDF2";
     var key = tv.pbkdf2_sha1.password;
 
     function doDerive(x) {
       if (!hasKeyFields(x)) {
@@ -63,16 +89,44 @@ TestArray.addTest(
     crypto.subtle.importKey("raw", key, alg, false, ["deriveBits"])
       .then( doDerive, fail )
       .then( memcmp_complete(that, tv.pbkdf2_sha1.derived), fail );
   }
 );
 
 // -----------------------------------------------------------------------------
 TestArray.addTest(
+  "Import a PBKDF2 key in JWK format and derive bits using HMAC-SHA-1",
+  function() {
+    var that = this;
+    var alg = "PBKDF2";
+
+    function doDerive(x) {
+      if (!hasKeyFields(x)) {
+        throw "Invalid key; missing field(s)";
+      }
+
+      var alg = {
+        name: "PBKDF2",
+        hash: "SHA-1",
+        salt: tv.pbkdf2_sha1.salt,
+        iterations: tv.pbkdf2_sha1.iterations
+      };
+      return crypto.subtle.deriveBits(alg, x, tv.pbkdf2_sha1.length);
+    }
+    function fail(x) { console.log("failing"); error(that)(x); }
+
+    crypto.subtle.importKey("jwk", tv.pbkdf2_sha1.jwk, alg, false, ["deriveBits"])
+      .then( doDerive, fail )
+      .then( memcmp_complete(that, tv.pbkdf2_sha1.derived), fail );
+  }
+);
+
+// -----------------------------------------------------------------------------
+TestArray.addTest(
   "Import raw PBKDF2 key and derive a new key using HMAC-SHA-1",
   function() {
     var that = this;
     var alg = "PBKDF2";
     var key = tv.pbkdf2_sha1.password;
 
     function doDerive(x) {
       if (!hasKeyFields(x)) {
--- a/dom/crypto/test/util.js
+++ b/dom/crypto/test/util.js
@@ -16,17 +16,17 @@ var util = {
         return false;
       }
     }
     return true;
   },
 
   // Convert an ArrayBufferView to a hex string
   abv2hex: function util_abv2hex(abv) {
-    var b = new Uint8Array(abv.buffer, abv.byteOffset, abv.byteLength);
+    var b = new Uint8Array(abv);
     var hex = "";
     for (var i=0; i <b.length; ++i) {
       var zeropad = (b[i] < 0x10) ? "0" : "";
       hex += zeropad + b[i].toString(16);
     }
     return hex;
   },
 
--- a/dom/webidl/SubtleCrypto.webidl
+++ b/dom/webidl/SubtleCrypto.webidl
@@ -99,16 +99,22 @@ dictionary DhImportKeyParams : Algorithm
 dictionary EcdsaParams : Algorithm {
   required AlgorithmIdentifier hash;
 };
 
 dictionary EcKeyImportParams : Algorithm {
   NamedCurve namedCurve;
 };
 
+dictionary HkdfParams : Algorithm {
+  required AlgorithmIdentifier hash;
+  required CryptoOperationData salt;
+  required CryptoOperationData info;
+};
+
 /***** JWK *****/
 
 dictionary RsaOtherPrimesInfo {
   // The following fields are defined in Section 6.3.2.7 of JSON Web Algorithms
   required DOMString r;
   required DOMString d;
   required DOMString t;
 };