Bug 1200341 - Implement HKDF for WebCrypto r=rbarnes,smaug
authorTim Taubert <tim@timtaubert.de>
Tue, 26 Jan 2016 14:57:52 +0100
changeset 318107 9e347384b8db
parent 318106 60d7b2fb75cb
child 318108 8b255b3ff8d3
push id5913
push userjlund@mozilla.com
push dateMon, 25 Apr 2016 16:57:49 +0000
treeherdermozilla-beta@dcaf0a6fa115 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrbarnes, smaug
bugs1200341
milestone47.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 1200341 - Implement HKDF for WebCrypto r=rbarnes,smaug
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;
 };