Import Cobalt 21.master.0.253153
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/META.yml b/src/third_party/web_platform_tests/WebCryptoAPI/META.yml
new file mode 100644
index 0000000..8f27e48
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/META.yml
@@ -0,0 +1,3 @@
+spec: https://w3c.github.io/webcrypto/
+suggested_reviewers:
+  - jimsch
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/ecdh_bits.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/ecdh_bits.https.any.js
new file mode 100644
index 0000000..37e3eb4
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/ecdh_bits.https.any.js
@@ -0,0 +1,9 @@
+// META: title=WebCryptoAPI: deriveBits() Using ECDH
+// META: script=ecdh_bits.js
+
+// Define subtests from a `promise_test` to ensure the harness does not
+// complete before the subtests are available. `explicit_done` cannot be used
+// for this purpose because the global `done` function is automatically invoked
+// by the WPT infrastructure in dedicated worker tests defined using the
+// "multi-global" pattern.
+promise_test(define_tests, 'setup - define tests');
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/ecdh_bits.js b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/ecdh_bits.js
new file mode 100644
index 0000000..5cc7193
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/ecdh_bits.js
@@ -0,0 +1,268 @@
+
+function define_tests() {
+    // May want to test prefixed implementations.
+    var subtle = self.crypto.subtle;
+
+    var pkcs8 = {
+        "P-521": new Uint8Array([48, 129, 238, 2, 1, 0, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 35, 4, 129, 214, 48, 129, 211, 2, 1, 1, 4, 66, 1, 166, 126, 211, 33, 145, 90, 100, 170, 53, 155, 125, 100, 141, 220, 38, 24, 250, 142, 141, 24, 103, 232, 247, 24, 48, 177, 13, 37, 237, 40, 145, 250, 241, 47, 60, 126, 117, 66, 26, 46, 162, 100, 249, 169, 21, 50, 13, 39, 79, 225, 71, 7, 66, 185, 132, 233, 107, 152, 145, 32, 129, 250, 205, 71, 141, 161, 129, 137, 3, 129, 134, 0, 4, 0, 32, 157, 72, 63, 40, 102, 104, 129, 198, 100, 31, 58, 18, 111, 64, 15, 81, 228, 101, 17, 112, 254, 103, 140, 117, 232, 87, 18, 226, 134, 138, 220, 133, 8, 36, 153, 123, 235, 240, 188, 130, 180, 48, 40, 166, 210, 236, 23, 119, 202, 69, 39, 159, 114, 6, 163, 234, 139, 92, 210, 7, 63, 73, 62, 69, 0, 12, 181, 76, 58, 90, 202, 162, 104, 197, 103, 16, 66, 136, 120, 217, 139, 138, 251, 246, 138, 97, 33, 83, 99, 40, 70, 216, 7, 233, 38, 114, 105, 143, 27, 156, 97, 29, 231, 211, 142, 52, 205, 108, 115, 136, 144, 146, 197, 110, 82, 214, 128, 241, 223, 208, 146, 184, 122, 200, 239, 159, 243, 200, 251, 72]),
+        "P-256": new Uint8Array([48, 129, 135, 2, 1, 0, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 4, 109, 48, 107, 2, 1, 1, 4, 32, 15, 247, 79, 232, 241, 202, 175, 97, 92, 206, 241, 29, 217, 53, 114, 87, 98, 217, 216, 65, 236, 186, 185, 94, 170, 38, 68, 123, 52, 100, 245, 113, 161, 68, 3, 66, 0, 4, 140, 96, 11, 44, 102, 25, 45, 97, 158, 39, 210, 37, 107, 59, 151, 118, 178, 141, 30, 5, 246, 13, 234, 189, 98, 174, 123, 154, 211, 157, 224, 217, 59, 4, 102, 109, 199, 119, 14, 126, 207, 13, 211, 203, 203, 211, 110, 221, 107, 94, 220, 153, 81, 7, 55, 161, 237, 104, 46, 205, 112, 244, 10, 47]),
+        "P-384": new Uint8Array([48, 129, 182, 2, 1, 0, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 34, 4, 129, 158, 48, 129, 155, 2, 1, 1, 4, 48, 248, 113, 165, 102, 101, 137, 193, 74, 87, 71, 38, 62, 248, 91, 49, 156, 192, 35, 219, 110, 53, 103, 108, 61, 120, 30, 239, 139, 5, 95, 207, 190, 134, 250, 13, 6, 208, 86, 181, 25, 95, 177, 50, 58, 248, 222, 37, 179, 161, 100, 3, 98, 0, 4, 241, 25, 101, 223, 125, 212, 89, 77, 4, 25, 197, 8, 100, 130, 163, 184, 38, 185, 121, 127, 155, 224, 189, 13, 16, 156, 158, 30, 153, 137, 193, 185, 169, 43, 143, 38, 159, 152, 225, 122, 209, 132, 186, 115, 193, 247, 151, 98, 175, 69, 175, 129, 65, 96, 38, 66, 218, 39, 26, 107, 176, 255, 235, 12, 180, 71, 143, 207, 112, 126, 102, 26, 166, 214, 205, 245, 21, 73, 200, 140, 63, 19, 11, 233, 232, 32, 31, 111, 106, 9, 244, 24, 90, 175, 149, 196])
+    };
+
+    var spki = {
+        "P-521": new Uint8Array([48, 129, 155, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 35, 3, 129, 134, 0, 4, 0, 238, 105, 249, 71, 21, 215, 1, 233, 226, 1, 19, 51, 212, 244, 249, 108, 186, 125, 145, 248, 139, 17, 43, 175, 117, 207, 9, 204, 31, 138, 202, 151, 97, 141, 169, 56, 152, 34, 210, 155, 111, 233, 153, 106, 97, 32, 62, 247, 82, 183, 113, 232, 149, 143, 196, 103, 123, 179, 119, 133, 101, 171, 96, 214, 237, 0, 222, 171, 103, 97, 137, 91, 147, 94, 58, 211, 37, 251, 133, 73, 229, 111, 19, 120, 106, 167, 63, 136, 162, 236, 254, 64, 147, 52, 115, 216, 174, 242, 64, 196, 223, 215, 213, 6, 242, 44, 221, 14, 85, 85, 143, 63, 191, 5, 235, 247, 239, 239, 122, 114, 215, 143, 70, 70, 155, 132, 72, 242, 110, 39, 18]),
+        "P-256": new Uint8Array([48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 154, 116, 32, 120, 126, 95, 77, 105, 211, 232, 34, 114, 115, 1, 109, 56, 224, 71, 129, 133, 223, 127, 238, 156, 142, 103, 60, 202, 211, 79, 126, 128, 254, 49, 141, 182, 221, 107, 119, 218, 99, 32, 165, 246, 151, 89, 9, 68, 23, 177, 52, 239, 138, 139, 116, 193, 101, 4, 57, 198, 115, 0, 90, 61]),
+        "P-384": new Uint8Array([48, 118, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 34, 3, 98, 0, 4, 145, 130, 45, 194, 175, 89, 193, 143, 91, 103, 248, 13, 246, 26, 38, 3, 194, 168, 240, 179, 192, 175, 130, 45, 99, 194, 121, 112, 26, 130, 69, 96, 64, 68, 1, 221, 233, 165, 110, 229, 39, 87, 234, 139, 199, 72, 212, 200, 43, 83, 55, 180, 141, 123, 101, 88, 58, 61, 87, 36, 56, 136, 0, 54, 186, 198, 115, 15, 66, 202, 82, 120, 150, 107, 213, 242, 30, 134, 226, 29, 48, 197, 166, 208, 70, 62, 197, 19, 221, 80, 159, 252, 220, 175, 31, 245])
+    };
+
+    var sizes = {
+        "P-521": 66,
+        "P-256": 32,
+        "P-384": 48
+    };
+
+    var derivations = {
+        "P-521": new Uint8Array([0, 156, 43, 206, 87, 190, 128, 173, 171, 59, 7, 56, 91, 142, 89, 144, 235, 125, 111, 222, 189, 176, 27, 243, 83, 113, 164, 246, 7, 94, 157, 40, 138, 193, 42, 109, 254, 3, 170, 87, 67, 188, 129, 112, 157, 73, 168, 34, 148, 2, 25, 182, 75, 118, 138, 205, 82, 15, 161, 54, 142, 160, 175, 141, 71, 93]),
+        "P-256": new Uint8Array([14, 143, 60, 77, 177, 178, 162, 131, 115, 90, 0, 220, 87, 31, 26, 232, 151, 28, 227, 35, 250, 17, 131, 137, 203, 95, 65, 196, 59, 61, 181, 161]),
+        "P-384": new Uint8Array([224, 189, 107, 206, 10, 239, 140, 164, 136, 56, 166, 226, 252, 197, 126, 103, 185, 197, 232, 134, 12, 95, 11, 233, 218, 190, 197, 62, 69, 78, 24, 160, 161, 116, 196, 136, 136, 162, 100, 136, 17, 91, 45, 201, 241, 223, 165, 45])
+    };
+
+    return importKeys(pkcs8, spki, sizes)
+    .then(function(results) {
+        publicKeys = results.publicKeys;
+        privateKeys = results.privateKeys;
+        ecdsaKeyPairs = results.ecdsaKeyPairs;
+        noDeriveBitsKeys = results.noDeriveBitsKeys;
+
+        Object.keys(sizes).forEach(function(namedCurve) {
+            // Basic success case
+            promise_test(function(test) {
+                return subtle.deriveBits({name: "ECDH", public: publicKeys[namedCurve]}, privateKeys[namedCurve], 8 * sizes[namedCurve])
+                .then(function(derivation) {
+                    assert_true(equalBuffers(derivation, derivations[namedCurve]), "Derived correct bits");
+                }, function(err) {
+                    assert_unreached("deriveBits failed with error " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " good parameters");
+
+            // Case insensitivity check
+            promise_test(function(test) {
+                return subtle.deriveBits({name: "EcDh", public: publicKeys[namedCurve]}, privateKeys[namedCurve], 8 * sizes[namedCurve])
+                .then(function(derivation) {
+                    assert_true(equalBuffers(derivation, derivations[namedCurve]), "Derived correct bits");
+                }, function(err) {
+                    assert_unreached("deriveBits failed with error " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " mixed case parameters");
+
+            // Null length
+            promise_test(function(test) {
+                return subtle.deriveBits({name: "ECDH", public: publicKeys[namedCurve]}, privateKeys[namedCurve], null)
+                .then(function(derivation) {
+                    assert_true(equalBuffers(derivation, derivations[namedCurve]), "Derived correct bits");
+                }, function(err) {
+                    assert_unreached("deriveBits failed with error " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " with null length");
+
+            // Shorter than entire derivation per algorithm
+            promise_test(function(test) {
+                return subtle.deriveBits({name: "ECDH", public: publicKeys[namedCurve]}, privateKeys[namedCurve], 8 * sizes[namedCurve] - 32)
+                .then(function(derivation) {
+                    assert_true(equalBuffers(derivation, derivations[namedCurve], 8 * sizes[namedCurve] - 32), "Derived correct bits");
+                }, function(err) {
+                    assert_unreached("deriveBits failed with error " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " short result");
+
+            // Non-multiple of 8
+            promise_test(function(test) {
+                return subtle.deriveBits({name: "ECDH", public: publicKeys[namedCurve]}, privateKeys[namedCurve], 8 * sizes[namedCurve] - 11)
+                .then(function(derivation) {
+                    assert_true(equalBuffers(derivation, derivations[namedCurve], 8 * sizes[namedCurve] - 11), "Derived correct bits");
+                }, function(err) {
+                    assert_unreached("deriveBits failed with error " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " non-multiple of 8 bits");
+
+            // Errors to test:
+
+            // - missing public property TypeError
+            promise_test(function(test) {
+                return subtle.deriveBits({name: "ECDH"}, privateKeys[namedCurve], 8 * sizes[namedCurve])
+                .then(function(derivation) {
+                    assert_unreached("deriveBits succeeded but should have failed with TypeError");
+                }, function(err) {
+                    assert_equals(err.name, "TypeError", "Should throw correct error, not " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " missing public curve");
+
+            // - Non CryptoKey public property TypeError
+            promise_test(function(test) {
+                return subtle.deriveBits({name: "ECDH", public: {message: "Not a CryptoKey"}}, privateKeys[namedCurve], 8 * sizes[namedCurve])
+                .then(function(derivation) {
+                    assert_unreached("deriveBits succeeded but should have failed with TypeError");
+                }, function(err) {
+                    assert_equals(err.name, "TypeError", "Should throw correct error, not " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " public property of algorithm is not a CryptoKey");
+
+            // - wrong named curve
+            promise_test(function(test) {
+                publicKey = publicKeys["P-256"];
+                if (namedCurve === "P-256") {
+                    publicKey = publicKeys["P-384"];
+                }
+                return subtle.deriveBits({name: "ECDH", public: publicKey}, privateKeys[namedCurve], 8 * sizes[namedCurve])
+                .then(function(derivation) {
+                    assert_unreached("deriveBits succeeded but should have failed with InvalidAccessError");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw correct error, not " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " mismatched curves");
+
+            // - not ECDH public property InvalidAccessError
+            promise_test(function(test) {
+                return subtle.deriveBits({name: "ECDH", public: ecdsaKeyPairs[namedCurve].publicKey}, privateKeys[namedCurve], 8 * sizes[namedCurve])
+                .then(function(derivation) {
+                    assert_unreached("deriveBits succeeded but should have failed with InvalidAccessError");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw correct error, not " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " public property of algorithm is not an ECDSA public key");
+
+            // - No deriveBits usage in baseKey InvalidAccessError
+            promise_test(function(test) {
+                return subtle.deriveBits({name: "ECDH", public: publicKeys[namedCurve]}, noDeriveBitsKeys[namedCurve], 8 * sizes[namedCurve])
+                .then(function(derivation) {
+                    assert_unreached("deriveBits succeeded but should have failed with InvalidAccessError");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw correct error, not " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " no deriveBits usage for base key");
+
+            // - Use public key for baseKey InvalidAccessError
+            promise_test(function(test) {
+                return subtle.deriveBits({name: "ECDH", public: publicKeys[namedCurve]}, publicKeys[namedCurve], 8 * sizes[namedCurve])
+                .then(function(derivation) {
+                    assert_unreached("deriveBits succeeded but should have failed with InvalidAccessError");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw correct error, not " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " base key is not a private key");
+
+            // - Use private key for public property InvalidAccessError
+            promise_test(function(test) {
+                return subtle.deriveBits({name: "ECDH", public: privateKeys[namedCurve]}, privateKeys[namedCurve], 8 * sizes[namedCurve])
+                .then(function(derivation) {
+                    assert_unreached("deriveBits succeeded but should have failed with InvalidAccessError");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw correct error, not " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " public property value is a private key");
+
+            // - Use secret key for public property InvalidAccessError
+            promise_test(function(test) {
+                return subtle.generateKey({name: "AES-CBC", length: 128}, true, ["encrypt", "decrypt"])
+                .then(function(secretKey) {
+                    subtle.deriveBits({name: "ECDH", public: secretKey}, privateKeys[namedCurve], 8 * sizes[namedCurve])
+                    .then(function(derivation) {
+                        assert_unreached("deriveBits succeeded but should have failed with InvalidAccessError");
+                    }, function(err) {
+                        assert_equals(err.name, "InvalidAccessError", "Should throw correct error, not " + err.name + ": " + err.message);
+                    });
+                });
+            }, namedCurve + " public property value is a secret key");
+
+            // - Length greater than 256, 384, 521 for particular curves OperationError
+            promise_test(function(test) {
+                return subtle.deriveBits({name: "ECDH", public: publicKeys[namedCurve]}, privateKeys[namedCurve], 8 * sizes[namedCurve] + 8)
+                .then(function(derivation) {
+                    assert_unreached("deriveBits succeeded but should have failed with OperationError");
+                }, function(err) {
+                    assert_equals(err.name, "OperationError", "Should throw correct error, not " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " asking for too many bits");
+        });
+    });
+
+    function importKeys(pkcs8, spki, sizes) {
+        var privateKeys = {};
+        var publicKeys = {};
+        var ecdsaKeyPairs = {};
+        var noDeriveBitsKeys = {};
+
+        var promises = [];
+        Object.keys(pkcs8).forEach(function(namedCurve) {
+            var operation = subtle.importKey("pkcs8", pkcs8[namedCurve],
+                                            {name: "ECDH", namedCurve: namedCurve},
+                                            false, ["deriveBits", "deriveKey"])
+                            .then(function(key) {
+                                privateKeys[namedCurve] = key;
+                            });
+            promises.push(operation);
+        });
+        Object.keys(pkcs8).forEach(function(namedCurve) {
+            var operation = subtle.importKey("pkcs8", pkcs8[namedCurve],
+                                            {name: "ECDH", namedCurve: namedCurve},
+                                            false, ["deriveKey"])
+                            .then(function(key) {
+                                noDeriveBitsKeys[namedCurve] = key;
+                            });
+            promises.push(operation);
+        });
+        Object.keys(spki).forEach(function(namedCurve) {
+            var operation = subtle.importKey("spki", spki[namedCurve],
+                                            {name: "ECDH", namedCurve: namedCurve},
+                                            false, [])
+                            .then(function(key) {
+                                publicKeys[namedCurve] = key;
+                            });
+            promises.push(operation);
+        });
+        Object.keys(sizes).forEach(function(namedCurve) {
+            var operation = subtle.generateKey({name: "ECDSA", namedCurve: namedCurve}, false, ["sign", "verify"])
+                            .then(function(keyPair) {
+                                ecdsaKeyPairs[namedCurve] = keyPair;
+                            });
+            promises.push(operation);
+        });
+
+        return Promise.all(promises)
+               .then(function(results) {return {privateKeys: privateKeys, publicKeys: publicKeys, ecdsaKeyPairs: ecdsaKeyPairs, noDeriveBitsKeys: noDeriveBitsKeys}});
+    }
+
+    // Compares two ArrayBuffer or ArrayBufferView objects. If bitCount is
+    // omitted, the two values must be the same length and have the same contents
+    // in every byte. If bitCount is included, only that leading number of bits
+    // have to match.
+    function equalBuffers(a, b, bitCount) {
+        var remainder;
+
+        if (typeof bitCount === "undefined" && a.byteLength !== b.byteLength) {
+            return false;
+        }
+
+        var aBytes = new Uint8Array(a);
+        var bBytes = new Uint8Array(b);
+
+        var length = a.byteLength;
+        if (typeof bitCount !== "undefined") {
+            length = Math.floor(bitCount / 8);
+        }
+
+        for (var i=0; i<length; i++) {
+            if (aBytes[i] !== bBytes[i]) {
+                return false;
+            }
+        }
+
+        if (typeof bitCount !== "undefined") {
+            remainder = bitCount % 8;
+            return aBytes[length] >> (8 - remainder) === bBytes[length] >> (8 - remainder);
+        }
+
+        return true;
+    }
+
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/ecdh_keys.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/ecdh_keys.https.any.js
new file mode 100644
index 0000000..d8235fc
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/ecdh_keys.https.any.js
@@ -0,0 +1,9 @@
+// META: title=WebCryptoAPI: deriveKey() Using ECDH
+// META: script=ecdh_keys.js
+
+// Define subtests from a `promise_test` to ensure the harness does not
+// complete before the subtests are available. `explicit_done` cannot be used
+// for this purpose because the global `done` function is automatically invoked
+// by the WPT infrastructure in dedicated worker tests defined using the
+// "multi-global" pattern.
+promise_test(define_tests, 'setup - define tests');
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/ecdh_keys.js b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/ecdh_keys.js
new file mode 100644
index 0000000..99008e0
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/ecdh_keys.js
@@ -0,0 +1,237 @@
+
+function define_tests() {
+    // May want to test prefixed implementations.
+    var subtle = self.crypto.subtle;
+
+    var pkcs8 = {
+        "P-521": new Uint8Array([48, 129, 238, 2, 1, 0, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 35, 4, 129, 214, 48, 129, 211, 2, 1, 1, 4, 66, 1, 166, 126, 211, 33, 145, 90, 100, 170, 53, 155, 125, 100, 141, 220, 38, 24, 250, 142, 141, 24, 103, 232, 247, 24, 48, 177, 13, 37, 237, 40, 145, 250, 241, 47, 60, 126, 117, 66, 26, 46, 162, 100, 249, 169, 21, 50, 13, 39, 79, 225, 71, 7, 66, 185, 132, 233, 107, 152, 145, 32, 129, 250, 205, 71, 141, 161, 129, 137, 3, 129, 134, 0, 4, 0, 32, 157, 72, 63, 40, 102, 104, 129, 198, 100, 31, 58, 18, 111, 64, 15, 81, 228, 101, 17, 112, 254, 103, 140, 117, 232, 87, 18, 226, 134, 138, 220, 133, 8, 36, 153, 123, 235, 240, 188, 130, 180, 48, 40, 166, 210, 236, 23, 119, 202, 69, 39, 159, 114, 6, 163, 234, 139, 92, 210, 7, 63, 73, 62, 69, 0, 12, 181, 76, 58, 90, 202, 162, 104, 197, 103, 16, 66, 136, 120, 217, 139, 138, 251, 246, 138, 97, 33, 83, 99, 40, 70, 216, 7, 233, 38, 114, 105, 143, 27, 156, 97, 29, 231, 211, 142, 52, 205, 108, 115, 136, 144, 146, 197, 110, 82, 214, 128, 241, 223, 208, 146, 184, 122, 200, 239, 159, 243, 200, 251, 72]),
+        "P-256": new Uint8Array([48, 129, 135, 2, 1, 0, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 4, 109, 48, 107, 2, 1, 1, 4, 32, 15, 247, 79, 232, 241, 202, 175, 97, 92, 206, 241, 29, 217, 53, 114, 87, 98, 217, 216, 65, 236, 186, 185, 94, 170, 38, 68, 123, 52, 100, 245, 113, 161, 68, 3, 66, 0, 4, 140, 96, 11, 44, 102, 25, 45, 97, 158, 39, 210, 37, 107, 59, 151, 118, 178, 141, 30, 5, 246, 13, 234, 189, 98, 174, 123, 154, 211, 157, 224, 217, 59, 4, 102, 109, 199, 119, 14, 126, 207, 13, 211, 203, 203, 211, 110, 221, 107, 94, 220, 153, 81, 7, 55, 161, 237, 104, 46, 205, 112, 244, 10, 47]),
+        "P-384": new Uint8Array([48, 129, 182, 2, 1, 0, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 34, 4, 129, 158, 48, 129, 155, 2, 1, 1, 4, 48, 248, 113, 165, 102, 101, 137, 193, 74, 87, 71, 38, 62, 248, 91, 49, 156, 192, 35, 219, 110, 53, 103, 108, 61, 120, 30, 239, 139, 5, 95, 207, 190, 134, 250, 13, 6, 208, 86, 181, 25, 95, 177, 50, 58, 248, 222, 37, 179, 161, 100, 3, 98, 0, 4, 241, 25, 101, 223, 125, 212, 89, 77, 4, 25, 197, 8, 100, 130, 163, 184, 38, 185, 121, 127, 155, 224, 189, 13, 16, 156, 158, 30, 153, 137, 193, 185, 169, 43, 143, 38, 159, 152, 225, 122, 209, 132, 186, 115, 193, 247, 151, 98, 175, 69, 175, 129, 65, 96, 38, 66, 218, 39, 26, 107, 176, 255, 235, 12, 180, 71, 143, 207, 112, 126, 102, 26, 166, 214, 205, 245, 21, 73, 200, 140, 63, 19, 11, 233, 232, 32, 31, 111, 106, 9, 244, 24, 90, 175, 149, 196])
+    };
+
+    var spki = {
+        "P-521": new Uint8Array([48, 129, 155, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 35, 3, 129, 134, 0, 4, 0, 238, 105, 249, 71, 21, 215, 1, 233, 226, 1, 19, 51, 212, 244, 249, 108, 186, 125, 145, 248, 139, 17, 43, 175, 117, 207, 9, 204, 31, 138, 202, 151, 97, 141, 169, 56, 152, 34, 210, 155, 111, 233, 153, 106, 97, 32, 62, 247, 82, 183, 113, 232, 149, 143, 196, 103, 123, 179, 119, 133, 101, 171, 96, 214, 237, 0, 222, 171, 103, 97, 137, 91, 147, 94, 58, 211, 37, 251, 133, 73, 229, 111, 19, 120, 106, 167, 63, 136, 162, 236, 254, 64, 147, 52, 115, 216, 174, 242, 64, 196, 223, 215, 213, 6, 242, 44, 221, 14, 85, 85, 143, 63, 191, 5, 235, 247, 239, 239, 122, 114, 215, 143, 70, 70, 155, 132, 72, 242, 110, 39, 18]),
+        "P-256": new Uint8Array([48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 154, 116, 32, 120, 126, 95, 77, 105, 211, 232, 34, 114, 115, 1, 109, 56, 224, 71, 129, 133, 223, 127, 238, 156, 142, 103, 60, 202, 211, 79, 126, 128, 254, 49, 141, 182, 221, 107, 119, 218, 99, 32, 165, 246, 151, 89, 9, 68, 23, 177, 52, 239, 138, 139, 116, 193, 101, 4, 57, 198, 115, 0, 90, 61]),
+        "P-384": new Uint8Array([48, 118, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 34, 3, 98, 0, 4, 145, 130, 45, 194, 175, 89, 193, 143, 91, 103, 248, 13, 246, 26, 38, 3, 194, 168, 240, 179, 192, 175, 130, 45, 99, 194, 121, 112, 26, 130, 69, 96, 64, 68, 1, 221, 233, 165, 110, 229, 39, 87, 234, 139, 199, 72, 212, 200, 43, 83, 55, 180, 141, 123, 101, 88, 58, 61, 87, 36, 56, 136, 0, 54, 186, 198, 115, 15, 66, 202, 82, 120, 150, 107, 213, 242, 30, 134, 226, 29, 48, 197, 166, 208, 70, 62, 197, 19, 221, 80, 159, 252, 220, 175, 31, 245])
+    };
+
+    var sizes = {
+        "P-521": 66,
+        "P-256": 32,
+        "P-384": 48
+    };
+
+    var derivations = {
+        "P-521": new Uint8Array([0, 156, 43, 206, 87, 190, 128, 173, 171, 59, 7, 56, 91, 142, 89, 144, 235, 125, 111, 222, 189, 176, 27, 243, 83, 113, 164, 246, 7, 94, 157, 40, 138, 193, 42, 109, 254, 3, 170, 87, 67, 188, 129, 112, 157, 73, 168, 34, 148, 2, 25, 182, 75, 118, 138, 205, 82, 15, 161, 54, 142, 160, 175, 141, 71, 93]),
+        "P-256": new Uint8Array([14, 143, 60, 77, 177, 178, 162, 131, 115, 90, 0, 220, 87, 31, 26, 232, 151, 28, 227, 35, 250, 17, 131, 137, 203, 95, 65, 196, 59, 61, 181, 161]),
+        "P-384": new Uint8Array([224, 189, 107, 206, 10, 239, 140, 164, 136, 56, 166, 226, 252, 197, 126, 103, 185, 197, 232, 134, 12, 95, 11, 233, 218, 190, 197, 62, 69, 78, 24, 160, 161, 116, 196, 136, 136, 162, 100, 136, 17, 91, 45, 201, 241, 223, 165, 45])
+    };
+
+    return importKeys(pkcs8, spki, sizes)
+    .then(function(results) {
+        publicKeys = results.publicKeys;
+        privateKeys = results.privateKeys;
+        ecdsaKeyPairs = results.ecdsaKeyPairs;
+        noDeriveKeyKeys = results.noDeriveKeyKeys;
+
+        Object.keys(sizes).forEach(function(namedCurve) {
+            // Basic success case
+            promise_test(function(test) {
+                return subtle.deriveKey({name: "ECDH", public: publicKeys[namedCurve]}, privateKeys[namedCurve], {name: "HMAC", hash: "SHA-256", length: 256}, true, ["sign", "verify"])
+                .then(function(key) {return crypto.subtle.exportKey("raw", key);})
+                .then(function(exportedKey) {
+                    assert_true(equalBuffers(exportedKey, derivations[namedCurve], 8 * exportedKey.length), "Derived correct key");
+                }, function(err) {
+                    assert_unreached("deriveKey failed with error " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " good parameters");
+
+            // Case insensitivity check
+            promise_test(function(test) {
+                return subtle.deriveKey({name: "EcDh", public: publicKeys[namedCurve]}, privateKeys[namedCurve], {name: "HMAC", hash: "SHA-256", length: 256}, true, ["sign", "verify"])
+                .then(function(key) {return crypto.subtle.exportKey("raw", key);})
+                .then(function(exportedKey) {
+                    assert_true(equalBuffers(exportedKey, derivations[namedCurve], 8 * exportedKey.length), "Derived correct key");
+                }, function(err) {
+                    assert_unreached("deriveKey failed with error " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " mixed case parameters");
+            // Errors to test:
+
+            // - missing public property TypeError
+            promise_test(function(test) {
+                return subtle.deriveKey({name: "ECDH"}, privateKeys[namedCurve], {name: "HMAC", hash: "SHA-256", length: 256}, true, ["sign", "verify"])
+                .then(function(key) {return crypto.subtle.exportKey("raw", key);})
+                .then(function(exportedKey) {
+                    assert_unreached("deriveKey succeeded but should have failed with TypeError");
+                }, function(err) {
+                    assert_equals(err.name, "TypeError", "Should throw correct error, not " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " missing public curve");
+
+            // - Non CryptoKey public property TypeError
+            promise_test(function(test) {
+                return subtle.deriveKey({name: "ECDH", public: {message: "Not a CryptoKey"}}, privateKeys[namedCurve], {name: "HMAC", hash: "SHA-256", length: 256}, true, ["sign", "verify"])
+                .then(function(key) {return crypto.subtle.exportKey("raw", key);})
+                .then(function(exportedKey) {
+                    assert_unreached("deriveKey succeeded but should have failed with TypeError");
+                }, function(err) {
+                    assert_equals(err.name, "TypeError", "Should throw correct error, not " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " public property of algorithm is not a CryptoKey");
+
+            // - wrong named curve
+            promise_test(function(test) {
+                publicKey = publicKeys["P-256"];
+                if (namedCurve === "P-256") {
+                    publicKey = publicKeys["P-384"];
+                }
+                return subtle.deriveKey({name: "ECDH", public: publicKey}, privateKeys[namedCurve], {name: "HMAC", hash: "SHA-256", length: 256}, true, ["sign", "verify"])
+                .then(function(key) {return crypto.subtle.exportKey("raw", key);})
+                .then(function(exportedKey) {
+                    assert_unreached("deriveKey succeeded but should have failed with InvalidAccessError");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw correct error, not " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " mismatched curves");
+
+            // - not ECDH public property InvalidAccessError
+            promise_test(function(test) {
+                return subtle.deriveKey({name: "ECDH", public: ecdsaKeyPairs[namedCurve].publicKey}, privateKeys[namedCurve], {name: "HMAC", hash: "SHA-256", length: 256}, true, ["sign", "verify"])
+                .then(function(key) {return crypto.subtle.exportKey("raw", key);})
+                .then(function(exportedKey) {
+                    assert_unreached("deriveKey succeeded but should have failed with InvalidAccessError");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw correct error, not " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " public property of algorithm is not an ECDSA public key");
+
+            // - No deriveKey usage in baseKey InvalidAccessError
+            promise_test(function(test) {
+                return subtle.deriveKey({name: "ECDH", public: publicKeys[namedCurve]}, noDeriveKeyKeys[namedCurve], {name: "HMAC", hash: "SHA-256", length: 256}, true, ["sign", "verify"])
+                .then(function(key) {return crypto.subtle.exportKey("raw", key);})
+                .then(function(exportedKey) {
+                    assert_unreached("deriveKey succeeded but should have failed with InvalidAccessError");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw correct error, not " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " no deriveKey usage for base key");
+
+            // - Use public key for baseKey InvalidAccessError
+            promise_test(function(test) {
+                return subtle.deriveKey({name: "ECDH", public: publicKeys[namedCurve]}, publicKeys[namedCurve], {name: "HMAC", hash: "SHA-256", length: 256}, true, ["sign", "verify"])
+                .then(function(key) {return crypto.subtle.exportKey("raw", key);})
+                .then(function(exportedKey) {
+                    assert_unreached("deriveKey succeeded but should have failed with InvalidAccessError");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw correct error, not " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " base key is not a private key");
+
+            // - Use private key for public property InvalidAccessError
+            promise_test(function(test) {
+                return subtle.deriveKey({name: "ECDH", public: privateKeys[namedCurve]}, privateKeys[namedCurve], {name: "HMAC", hash: "SHA-256", length: 256}, true, ["sign", "verify"])
+                .then(function(key) {return crypto.subtle.exportKey("raw", key);})
+                .then(function(exportedKey) {
+                    assert_unreached("deriveKey succeeded but should have failed with InvalidAccessError");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw correct error, not " + err.name + ": " + err.message);
+                });
+            }, namedCurve + " public property value is a private key");
+
+            // - Use secret key for public property InvalidAccessError
+            promise_test(function(test) {
+                return subtle.generateKey({name: "HMAC", hash: "SHA-256", length: 256}, true, ["sign", "verify"])
+                .then(function(secretKey) {
+                    subtle.deriveKey({name: "ECDH", public: secretKey}, privateKeys[namedCurve], {name: "AES-CBC", length: 256}, true, ["sign", "verify"])
+                    .then(function(key) {return crypto.subtle.exportKey("raw", key);})
+                    .then(function(exportedKey) {
+                        assert_unreached("deriveKey succeeded but should have failed with InvalidAccessError");
+                    }, function(err) {
+                        assert_equals(err.name, "InvalidAccessError", "Should throw correct error, not " + err.name + ": " + err.message);
+                    });
+                });
+            }, namedCurve + " public property value is a secret key");
+        });
+    });
+
+    function importKeys(pkcs8, spki, sizes) {
+        var privateKeys = {};
+        var publicKeys = {};
+        var ecdsaKeyPairs = {};
+        var noDeriveKeyKeys = {};
+
+        var promises = [];
+        Object.keys(pkcs8).forEach(function(namedCurve) {
+            var operation = subtle.importKey("pkcs8", pkcs8[namedCurve],
+                                            {name: "ECDH", namedCurve: namedCurve},
+                                            false, ["deriveBits", "deriveKey"])
+                            .then(function(key) {
+                                privateKeys[namedCurve] = key;
+                            });
+            promises.push(operation);
+        });
+        Object.keys(pkcs8).forEach(function(namedCurve) {
+            var operation = subtle.importKey("pkcs8", pkcs8[namedCurve],
+                                            {name: "ECDH", namedCurve: namedCurve},
+                                            false, ["deriveBits"])
+                            .then(function(key) {
+                                noDeriveKeyKeys[namedCurve] = key;
+                            });
+            promises.push(operation);
+        });
+        Object.keys(spki).forEach(function(namedCurve) {
+            var operation = subtle.importKey("spki", spki[namedCurve],
+                                            {name: "ECDH", namedCurve: namedCurve},
+                                            false, [])
+                            .then(function(key) {
+                                publicKeys[namedCurve] = key;
+                            });
+            promises.push(operation);
+        });
+        Object.keys(sizes).forEach(function(namedCurve) {
+            var operation = subtle.generateKey({name: "ECDSA", namedCurve: namedCurve}, false, ["sign", "verify"])
+                            .then(function(keyPair) {
+                                ecdsaKeyPairs[namedCurve] = keyPair;
+                            });
+            promises.push(operation);
+        });
+
+        return Promise.all(promises)
+               .then(function(results) {return {privateKeys: privateKeys, publicKeys: publicKeys, ecdsaKeyPairs: ecdsaKeyPairs, noDeriveKeyKeys: noDeriveKeyKeys}});
+    }
+
+    // Compares two ArrayBuffer or ArrayBufferView objects. If bitCount is
+    // omitted, the two values must be the same length and have the same contents
+    // in every byte. If bitCount is included, only that leading number of bits
+    // have to match.
+    function equalBuffers(a, b, bitCount) {
+        var remainder;
+
+        if (typeof bitCount === "undefined" && a.byteLength !== b.byteLength) {
+            return false;
+        }
+
+        var aBytes = new Uint8Array(a);
+        var bBytes = new Uint8Array(b);
+
+        var length = a.byteLength;
+        if (typeof bitCount !== "undefined") {
+            length = Math.floor(bitCount / 8);
+        }
+
+        for (var i=0; i<length; i++) {
+            if (aBytes[i] !== bBytes[i]) {
+                return false;
+            }
+        }
+
+        if (typeof bitCount !== "undefined") {
+            remainder = bitCount % 8;
+            return aBytes[length] >> (8 - remainder) === bBytes[length] >> (8 - remainder);
+        }
+
+        return true;
+    }
+
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/hkdf.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/hkdf.https.any.js
new file mode 100644
index 0000000..02492c3
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/hkdf.https.any.js
@@ -0,0 +1,15 @@
+// META: title=WebCryptoAPI: deriveBits() and deriveKey() Using HKDF
+// META: variant=?1-1000
+// META: variant=?1001-2000
+// META: variant=?2001-3000
+// META: variant=?3001-last
+// META: script=/common/subset-tests.js
+// META: script=hkdf_vectors.js
+// META: script=hkdf.js
+
+// Define subtests from a `promise_test` to ensure the harness does not
+// complete before the subtests are available. `explicit_done` cannot be used
+// for this purpose because the global `done` function is automatically invoked
+// by the WPT infrastructure in dedicated worker tests defined using the
+// "multi-global" pattern.
+promise_test(define_tests, 'setup - define tests');
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/hkdf.js b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/hkdf.js
new file mode 100644
index 0000000..2bb5853
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/hkdf.js
@@ -0,0 +1,305 @@
+
+function define_tests() {
+    // May want to test prefixed implementations.
+    var subtle = self.crypto.subtle;
+
+    // hkdf2_vectors sets up test data with the correct derivations for each
+    // test case.
+    var testData = getTestData();
+    var derivedKeys = testData.derivedKeys;
+    var salts = testData.salts;
+    var derivations = testData.derivations;
+    var infos = testData.infos;
+
+    // What kinds of keys can be created with deriveKey? The following:
+    var derivedKeyTypes = testData.derivedKeyTypes;
+
+    return setUpBaseKeys(derivedKeys)
+    .then(function(allKeys) {
+        // We get several kinds of base keys. Normal ones that can be used for
+        // derivation operations, ones that lack the deriveBits usage, ones
+        // that lack the deriveKeys usage, and one key that is for the wrong
+        // algorithm (not HKDF in this case).
+        var baseKeys = allKeys.baseKeys;
+        var noBits = allKeys.noBits;
+        var noKey = allKeys.noKey;
+        var wrongKey = allKeys.wrongKey;
+
+        // Test each combination of derivedKey size, salt size, hash function,
+        // and number of iterations. The derivations object is structured in
+        // that way, so navigate it to run tests and compare with correct results.
+        Object.keys(derivations).forEach(function(derivedKeySize) {
+            Object.keys(derivations[derivedKeySize]).forEach(function(saltSize) {
+                Object.keys(derivations[derivedKeySize][saltSize]).forEach(function(hashName) {
+                    Object.keys(derivations[derivedKeySize][saltSize][hashName]).forEach(function(infoSize) {
+                        var testName = derivedKeySize + " derivedKey, " + saltSize + " salt, " + hashName + ", with " + infoSize + " info";
+                        var algorithm = {name: "HKDF", salt: salts[saltSize], info: infos[infoSize], hash: hashName};
+
+                        // Check for correct deriveBits result
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveBits(algorithm, baseKeys[derivedKeySize], 256)
+                            .then(function(derivation) {
+                                assert_true(equalBuffers(derivation, derivations[derivedKeySize][saltSize][hashName][infoSize]), "Derived correct key");
+                            }, function(err) {
+                                assert_unreached("deriveBits failed with error " + err.name + ": " + err.message);
+                            });
+                        }, testName);
+
+                        // 0 length (OperationError)
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveBits(algorithm, baseKeys[derivedKeySize], 0)
+                            .then(function(derivation) {
+                                assert_equals(derivation.byteLength, 0, "Derived correctly empty key");
+                            }, function(err) {
+                                assert_equals(err.name, "OperationError", "deriveBits with 0 length correctly threw OperationError: " + err.message);
+                            });
+                        }, testName + " with 0 length");
+
+                        // Check for correct deriveKey results for every kind of
+                        // key that can be created by the deriveKeys operation.
+                        derivedKeyTypes.forEach(function(derivedKeyType) {
+                            var testName = "Derived key of type ";
+                            Object.keys(derivedKeyType.algorithm).forEach(function(prop) {
+                                testName += prop + ": " + derivedKeyType.algorithm[prop] + " ";
+                            });
+                            testName += " using " + derivedKeySize + " derivedKey, " + saltSize + " salt, " + hashName + ", with " + infoSize + " info";
+
+                            // Test the particular key derivation.
+                            subsetTest(promise_test, function(test) {
+                                return subtle.deriveKey(algorithm, baseKeys[derivedKeySize], derivedKeyType.algorithm, true, derivedKeyType.usages)
+                                .then(function(key) {
+                                    // Need to export the key to see that the correct bits were set.
+                                    return subtle.exportKey("raw", key)
+                                    .then(function(buffer) {
+                                        assert_true(equalBuffers(buffer, derivations[derivedKeySize][saltSize][hashName][infoSize].slice(0, derivedKeyType.algorithm.length/8)), "Exported key matches correct value");
+                                    }, function(err) {
+                                        assert_unreached("Exporting derived key failed with error " + err.name + ": " + err.message);
+                                    });
+                                }, function(err) {
+                                    assert_unreached("deriveKey failed with error " + err.name + ": " + err.message);
+
+                                });
+                            }, testName);
+
+                            // Test various error conditions for deriveKey:
+
+                            // - illegal name for hash algorithm (NotSupportedError)
+                            var badHash = hashName.substring(0, 3) + hashName.substring(4);
+                            subsetTest(promise_test, function(test) {
+                                var badAlgorithm = {name: "HKDF", salt: salts[saltSize], hash: badHash, info: algorithm.info};
+                                return subtle.deriveKey(badAlgorithm, baseKeys[derivedKeySize], derivedKeyType.algorithm, true, derivedKeyType.usages)
+                                .then(function(key) {
+                                    assert_unreached("bad hash name should have thrown an NotSupportedError");
+                                }, function(err) {
+                                    assert_equals(err.name, "NotSupportedError", "deriveKey with bad hash name correctly threw NotSupportedError: " + err.message);
+                                });
+                            }, testName + " with bad hash name " + badHash);
+
+                            // - baseKey usages missing "deriveKey" (InvalidAccessError)
+                            subsetTest(promise_test, function(test) {
+                                return subtle.deriveKey(algorithm, noKey[derivedKeySize], derivedKeyType.algorithm, true, derivedKeyType.usages)
+                                .then(function(key) {
+                                    assert_unreached("missing deriveKey usage should have thrown an InvalidAccessError");
+                                }, function(err) {
+                                    assert_equals(err.name, "InvalidAccessError", "deriveKey with missing deriveKey usage correctly threw InvalidAccessError: " + err.message);
+                                });
+                            }, testName + " with missing deriveKey usage");
+
+                            // - baseKey algorithm does not match HKDF (InvalidAccessError)
+                            subsetTest(promise_test, function(test) {
+                                return subtle.deriveKey(algorithm, wrongKey, derivedKeyType.algorithm, true, derivedKeyType.usages)
+                                .then(function(key) {
+                                    assert_unreached("wrong (ECDH) key should have thrown an InvalidAccessError");
+                                }, function(err) {
+                                    assert_equals(err.name, "InvalidAccessError", "deriveKey with wrong (ECDH) key correctly threw InvalidAccessError: " + err.message);
+                                });
+                            }, testName + " with wrong (ECDH) key");
+
+                        });
+
+                        // Test various error conditions for deriveBits below:
+
+                        // missing salt (TypeError)
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveBits({name: "HKDF", info: infos[infoSize], hash: hashName}, baseKeys[derivedKeySize], 0)
+                            .then(function(derivation) {
+                                assert_equals(derivation.byteLength, 0, "Derived even with missing salt");
+                            }, function(err) {
+                                assert_equals(err.name, "TypeError", "deriveBits missing salt correctly threw OperationError: " + err.message);
+                            });
+                        }, testName + " with missing salt");
+
+                        // missing info (TypeError)
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveBits({name: "HKDF", salt: salts[saltSize], hash: hashName}, baseKeys[derivedKeySize], 0)
+                            .then(function(derivation) {
+                                assert_equals(derivation.byteLength, 0, "Derived even with missing info");
+                            }, function(err) {
+                                assert_equals(err.name, "TypeError", "deriveBits missing info correctly threw OperationError: " + err.message);
+                            });
+                        }, testName + " with missing info");
+
+                        // length null (OperationError)
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveBits(algorithm, baseKeys[derivedKeySize], null)
+                            .then(function(derivation) {
+                                assert_unreached("null length should have thrown an OperationError");
+                            }, function(err) {
+                                assert_equals(err.name, "OperationError", "deriveBits with null length correctly threw OperationError: " + err.message);
+                            });
+                        }, testName + " with null length");
+
+                        // length not multiple of 8 (OperationError)
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveBits(algorithm, baseKeys[derivedKeySize], 44)
+                            .then(function(derivation) {
+                                assert_unreached("non-multiple of 8 length should have thrown an OperationError");
+                            }, function(err) {
+                                assert_equals(err.name, "OperationError", "deriveBits with non-multiple of 8 length correctly threw OperationError: " + err.message);
+                            });
+                        }, testName + " with non-multiple of 8 length");
+
+                        // - illegal name for hash algorithm (NotSupportedError)
+                        var badHash = hashName.substring(0, 3) + hashName.substring(4);
+                        subsetTest(promise_test, function(test) {
+                            var badAlgorithm = {name: "HKDF", salt: salts[saltSize], hash: badHash, info: algorithm.info};
+                            return subtle.deriveBits(badAlgorithm, baseKeys[derivedKeySize], 256)
+                            .then(function(derivation) {
+                                assert_unreached("bad hash name should have thrown an NotSupportedError");
+                            }, function(err) {
+                                assert_equals(err.name, "NotSupportedError", "deriveBits with bad hash name correctly threw NotSupportedError: " + err.message);
+                            });
+                        }, testName + " with bad hash name " + badHash);
+
+                        // - baseKey usages missing "deriveBits" (InvalidAccessError)
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveBits(algorithm, noBits[derivedKeySize], 256)
+                            .then(function(derivation) {
+                                assert_unreached("missing deriveBits usage should have thrown an InvalidAccessError");
+                            }, function(err) {
+                                assert_equals(err.name, "InvalidAccessError", "deriveBits with missing deriveBits usage correctly threw InvalidAccessError: " + err.message);
+                            });
+                        }, testName + " with missing deriveBits usage");
+
+                        // - baseKey algorithm does not match HKDF (InvalidAccessError)
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveBits(algorithm, wrongKey, 256)
+                            .then(function(derivation) {
+                                assert_unreached("wrong (ECDH) key should have thrown an InvalidAccessError");
+                            }, function(err) {
+                                assert_equals(err.name, "InvalidAccessError", "deriveBits with wrong (ECDH) key correctly threw InvalidAccessError: " + err.message);
+                            });
+                        }, testName + " with wrong (ECDH) key");
+                    });
+                });
+
+                // - legal algorithm name but not digest one (e.g., PBKDF2) (NotSupportedError)
+                var nonDigestHash = "PBKDF2";
+                Object.keys(infos).forEach(function(infoSize) {
+                    var testName = derivedKeySize + " derivedKey, " + saltSize + " salt, " + nonDigestHash + ", with " + infoSize + " info";
+                    var algorithm = {name: "HKDF", salt: salts[saltSize], hash: nonDigestHash};
+                    if (infoSize !== "missing") {
+                        algorithm.info = infos[infoSize];
+                    }
+
+                    subsetTest(promise_test, function(test) {
+                        return subtle.deriveBits(algorithm, baseKeys[derivedKeySize], 256)
+                        .then(function(derivation) {
+                            assert_unreached("non-digest algorithm should have thrown an NotSupportedError");
+                        }, function(err) {
+                            assert_equals(err.name, "NotSupportedError", "deriveBits with non-digest algorithm correctly threw NotSupportedError: " + err.message);
+                        });
+                    }, testName + " with non-digest algorithm " + nonDigestHash);
+
+                    derivedKeyTypes.forEach(function(derivedKeyType) {
+                        var testName = "Derived key of type ";
+                        Object.keys(derivedKeyType.algorithm).forEach(function(prop) {
+                            testName += prop + ": " + derivedKeyType.algorithm[prop] + " ";
+                        });
+                        testName += " using " + derivedKeySize + " derivedKey, " + saltSize + " salt, " + nonDigestHash + ", with " + infoSize + " info";
+
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveKey(algorithm, baseKeys[derivedKeySize], derivedKeyType.algorithm, true, derivedKeyType.usages)
+                            .then(function(derivation) {
+                                assert_unreached("non-digest algorithm should have thrown an NotSupportedError");
+                            }, function(err) {
+                                assert_equals(err.name, "NotSupportedError", "derivekey with non-digest algorithm correctly threw NotSupportedError: " + err.message);
+                            });
+                        }, testName);
+                    });
+
+                });
+
+            });
+        });
+    });
+
+    // Deriving bits and keys requires starting with a base key, which is created
+    // by importing a derivedKey. setUpBaseKeys returns a promise that yields the
+    // necessary base keys.
+    function setUpBaseKeys(derivedKeys) {
+        var promises = [];
+
+        var baseKeys = {};
+        var noBits = {};
+        var noKey = {};
+        var wrongKey = null;
+
+        Object.keys(derivedKeys).forEach(function(derivedKeySize) {
+            var promise = subtle.importKey("raw", derivedKeys[derivedKeySize], {name: "HKDF"}, false, ["deriveKey", "deriveBits"])
+            .then(function(baseKey) {
+                baseKeys[derivedKeySize] = baseKey;
+            }, function(err) {
+                baseKeys[derivedKeySize] = null;
+            });
+            promises.push(promise);
+
+            promise = subtle.importKey("raw", derivedKeys[derivedKeySize], {name: "HKDF"}, false, ["deriveBits"])
+            .then(function(baseKey) {
+                noKey[derivedKeySize] = baseKey;
+            }, function(err) {
+                noKey[derivedKeySize] = null;
+            });
+            promises.push(promise);
+
+            promise = subtle.importKey("raw", derivedKeys[derivedKeySize], {name: "HKDF"}, false, ["deriveKey"])
+            .then(function(baseKey) {
+                noBits[derivedKeySize] = baseKey;
+            }, function(err) {
+                noBits[derivedKeySize] = null;
+            });
+            promises.push(promise);
+        });
+
+        var promise = subtle.generateKey({name: "ECDH", namedCurve: "P-256"}, false, ["deriveKey", "deriveBits"])
+        .then(function(baseKey) {
+            wrongKey = baseKey.privateKey;
+        }, function(err) {
+            wrongKey = null;
+        });
+        promises.push(promise);
+
+
+        return Promise.all(promises).then(function() {
+            return {baseKeys: baseKeys, noBits: noBits, noKey: noKey, wrongKey: wrongKey};
+        });
+    }
+
+    function equalBuffers(a, b) {
+        if (a.byteLength !== b.byteLength) {
+            return false;
+        }
+
+        var aBytes = new Uint8Array(a);
+        var bBytes = new Uint8Array(b);
+
+        for (var i=0; i<a.byteLength; i++) {
+            if (aBytes[i] !== bBytes[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/hkdf_vectors.js b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/hkdf_vectors.js
new file mode 100644
index 0000000..ef448ec
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/hkdf_vectors.js
@@ -0,0 +1,184 @@
+
+function getTestData() {
+
+    // deriveBits and deriveKey take as input:
+    // - derivedKey (actually, a CryptoKey representing a derivedKey)
+    // - salt (BufferSource)
+    // - hash (which one to use)
+    // - iterations (how many times to use it)
+
+    // deriveBits also takes a length. deriveKey uses the length of the output key
+    // - length is the number of bits, NOT octets, but it MUST be a multiple of 8
+    // - note that result of length(n) is first n bits of length(m) if m>n
+
+    // Variations to test:
+    // - empty, short, and fairly long derivedKey
+    // - empty, short, and fairly long salt
+    // - SHA-1, SHA-256, SHA-384, SHA-512 hash
+    // - 1, 1000, and 100000 million iterations
+
+    // Test cases to generate: 3 * 3 * 4 * 3 = 108
+
+    // Error conditions to test:
+    // - length null (OperationError)
+    // - length not a multiple of 8 (OperationError)
+    // - illegal name for hash algorithm (NotSupportedError)
+    // - legal algorithm name but not digest one (e.g., AES-CBC) (NotSupportedError)
+    // - baseKey usages missing "deriveBits" (InvalidAccessError)
+    // - baseKey algorithm does not match HKDF (InvalidAccessError)
+    // - 0 iterations
+
+    var derivedKeyTypes = [
+        {algorithm: {name: "AES-CBC", length: 128}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-CBC", length: 192}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-CBC", length: 256}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-CTR", length: 128}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-CTR", length: 192}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-CTR", length: 256}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-GCM", length: 128}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-GCM", length: 192}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-GCM", length: 256}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-KW", length: 128}, usages: ["wrapKey", "unwrapKey"]},
+        {algorithm: {name: "AES-KW", length: 192}, usages: ["wrapKey", "unwrapKey"]},
+        {algorithm: {name: "AES-KW", length: 256}, usages: ["wrapKey", "unwrapKey"]},
+        {algorithm: {name: "HMAC", hash: "SHA-1", length: 256}, usages: ["sign", "verify"]},
+        {algorithm: {name: "HMAC", hash: "SHA-256", length: 256}, usages: ["sign", "verify"]},
+        {algorithm: {name: "HMAC", hash: "SHA-384", length: 256}, usages: ["sign", "verify"]},
+        {algorithm: {name: "HMAC", hash: "SHA-512", length: 256}, usages: ["sign", "verify"]}
+    ];
+
+    var derivedKeys = {
+        "short": new Uint8Array([80, 64, 115, 115, 119, 48, 114, 100]),
+        "long": new Uint8Array([85, 115, 101, 114, 115, 32, 115, 104, 111, 117, 108, 100, 32, 112, 105, 99, 107, 32, 108, 111, 110, 103, 32, 112, 97, 115, 115, 112, 104, 114, 97, 115, 101, 115, 32, 40, 110, 111, 116, 32, 117, 115, 101, 32, 115, 104, 111, 114, 116, 32, 112, 97, 115, 115, 119, 111, 114, 100, 115, 41, 33]),
+        "empty": new Uint8Array([])
+    };
+
+    var salts = {
+        "normal": new Uint8Array([83, 111, 100, 105, 117, 109, 32, 67, 104, 108, 111, 114, 105, 100, 101, 32, 99, 111, 109, 112, 111, 117, 110, 100]),
+        "empty": new Uint8Array([])
+    };
+
+    var infos = {
+        "normal": new Uint8Array([72, 75, 68, 70, 32, 101, 120, 116, 114, 97, 32, 105, 110, 102, 111]),
+        "empty": new Uint8Array([])
+    };
+
+    var derivations = {
+        "short": {
+            "normal": {
+                "SHA-384": {
+                    "normal": new Uint8Array([25, 186, 116, 54, 142, 107, 153, 51, 144, 242, 127, 233, 167, 208, 43, 195, 56, 23, 63, 114, 190, 113, 161, 159, 199, 68, 252, 219, 63, 212, 184, 75]),
+                    "empty": new Uint8Array([151, 96, 31, 78, 12, 83, 165, 211, 243, 162, 129, 0, 153, 188, 104, 32, 236, 80, 8, 52, 52, 118, 155, 89, 252, 36, 164, 23, 169, 84, 55, 52])
+                },
+                "SHA-512": {
+                    "normal": new Uint8Array([75, 189, 109, 178, 67, 95, 182, 150, 21, 127, 96, 137, 201, 119, 195, 199, 63, 62, 172, 94, 243, 221, 107, 170, 230, 4, 203, 83, 191, 187, 21, 62]),
+                    "empty": new Uint8Array([47, 49, 87, 231, 254, 12, 16, 176, 18, 152, 200, 240, 136, 106, 144, 237, 207, 128, 171, 222, 245, 219, 193, 223, 43, 20, 130, 83, 43, 82, 185, 52])
+                },
+                "SHA-1": {
+                    "normal": new Uint8Array([5, 173, 34, 237, 33, 56, 201, 96, 14, 77, 158, 39, 37, 222, 211, 1, 245, 210, 135, 251, 251, 87, 2, 249, 153, 188, 101, 54, 211, 237, 239, 152]),
+                    "empty": new Uint8Array([213, 27, 111, 183, 229, 153, 202, 48, 197, 238, 38, 69, 147, 228, 184, 95, 34, 32, 199, 195, 171, 0, 49, 87, 191, 248, 203, 79, 54, 156, 117, 96])
+                },
+                "SHA-256": {
+                    "normal": new Uint8Array([42, 245, 144, 30, 40, 132, 156, 40, 68, 56, 87, 56, 106, 161, 172, 59, 177, 39, 233, 38, 49, 193, 192, 81, 72, 45, 102, 144, 148, 23, 114, 180]),
+                    "empty": new Uint8Array([158, 75, 113, 144, 51, 116, 33, 1, 233, 15, 26, 214, 30, 47, 243, 180, 37, 104, 99, 102, 114, 150, 215, 67, 137, 241, 240, 42, 242, 196, 230, 166])
+                }
+            },
+            "empty": {
+                "SHA-384": {
+                    "normal": new Uint8Array([251, 72, 47, 242, 44, 79, 141, 70, 108, 77, 254, 110, 41, 242, 204, 46, 205, 171, 245, 136, 67, 40, 251, 240, 138, 115, 143, 217, 69, 241, 102, 203]),
+                    "empty": new Uint8Array([30, 2, 60, 23, 179, 64, 83, 60, 234, 239, 57, 35, 12, 184, 179, 187, 219, 246, 99, 161, 61, 96, 117, 208, 221, 50, 108, 4, 148, 120, 251, 165])
+                },
+                "SHA-512": {
+                    "normal": new Uint8Array([241, 123, 91, 220, 216, 215, 211, 212, 96, 16, 54, 161, 148, 54, 49, 125, 22, 68, 249, 164, 224, 149, 110, 252, 14, 55, 43, 131, 172, 218, 207, 219]),
+                    "empty": new Uint8Array([199, 180, 116, 148, 47, 49, 248, 63, 175, 93, 20, 115, 24, 2, 177, 189, 73, 71, 133, 73, 203, 58, 143, 61, 191, 237, 196, 211, 32, 156, 245, 182])
+                },
+                "SHA-1": {
+                    "normal": new Uint8Array([193, 38, 241, 230, 242, 90, 157, 228, 44, 247, 212, 39, 5, 154, 82, 237, 150, 1, 242, 154, 88, 21, 203, 251, 198, 75, 199, 246, 104, 198, 163, 65]),
+                    "empty": new Uint8Array([50, 21, 195, 240, 141, 231, 5, 73, 176, 81, 183, 3, 55, 69, 168, 24, 79, 140, 186, 166, 177, 115, 83, 48, 210, 188, 182, 177, 111, 70, 66, 239])
+                },
+                "SHA-256": {
+                    "normal": new Uint8Array([115, 60, 139, 107, 207, 172, 135, 92, 127, 8, 152, 42, 110, 63, 251, 86, 10, 206, 166, 241, 101, 71, 110, 184, 52, 96, 185, 53, 62, 212, 29, 254]),
+                    "empty": new Uint8Array([200, 225, 39, 116, 19, 83, 5, 201, 20, 127, 44, 196, 118, 110, 94, 173, 37, 216, 244, 87, 185, 161, 149, 61, 82, 103, 115, 97, 206, 213, 88, 251])
+                }
+            }
+        },
+        "long": {
+            "normal": {
+                "SHA-384": {
+                    "normal": new Uint8Array([249, 21, 113, 181, 33, 247, 238, 241, 62, 87, 58, 164, 99, 120, 101, 158, 243, 183, 243, 111, 253, 209, 187, 5, 93, 178, 205, 119, 210, 96, 196, 103]),
+                    "empty": new Uint8Array([104, 175, 28, 44, 246, 185, 55, 13, 32, 84, 52, 71, 152, 189, 187, 24, 71, 204, 244, 7, 183, 101, 43, 121, 61, 209, 54, 212, 100, 14, 3, 72])
+                },
+                "SHA-512": {
+                    "normal": new Uint8Array([113, 10, 174, 47, 223, 136, 158, 69, 254, 15, 185, 149, 178, 194, 107, 51, 235, 152, 134, 80, 236, 15, 174, 241, 103, 2, 138, 122, 108, 203, 54, 56]),
+                    "empty": new Uint8Array([229, 222, 86, 128, 129, 199, 30, 86, 39, 80, 130, 152, 113, 195, 66, 117, 129, 4, 118, 94, 214, 243, 6, 240, 97, 60, 157, 75, 179, 54, 242, 170])
+                },
+                "SHA-1": {
+                    "normal": new Uint8Array([127, 149, 126, 220, 188, 227, 203, 11, 112, 86, 110, 30, 182, 14, 253, 30, 64, 90, 19, 48, 76, 102, 29, 54, 99, 119, 129, 9, 191, 6, 137, 156]),
+                    "empty": new Uint8Array([48, 98, 243, 207, 26, 115, 11, 156, 239, 81, 240, 44, 29, 250, 200, 94, 217, 30, 75, 0, 101, 235, 80, 202, 159, 216, 176, 16, 126, 114, 135, 51])
+                },
+                "SHA-256": {
+                    "normal": new Uint8Array([49, 183, 214, 133, 48, 168, 99, 231, 23, 192, 129, 202, 105, 23, 182, 134, 80, 179, 221, 154, 41, 243, 6, 6, 226, 202, 209, 153, 190, 193, 77, 19]),
+                    "empty": new Uint8Array([229, 121, 209, 249, 231, 240, 142, 111, 153, 15, 252, 252, 206, 30, 210, 1, 197, 227, 126, 98, 205, 246, 6, 240, 186, 74, 202, 128, 66, 127, 188, 68])
+                }
+            },
+            "empty": {
+                "SHA-384": {
+                    "normal": new Uint8Array([97, 158, 182, 249, 40, 115, 149, 187, 213, 237, 106, 103, 201, 104, 70, 90, 216, 43, 108, 85, 159, 60, 56, 182, 4, 187, 176, 143, 88, 50, 11, 3]),
+                    "empty": new Uint8Array([255, 68, 123, 66, 61, 131, 254, 118, 131, 108, 50, 51, 114, 40, 181, 107, 91, 217, 191, 104, 213, 142, 125, 202, 75, 124, 202, 132, 42, 69, 225, 26])
+                },
+                "SHA-512": {
+                    "normal": new Uint8Array([19, 62, 138, 127, 127, 244, 51, 105, 12, 200, 132, 50, 194, 163, 56, 194, 119, 229, 193, 55, 86, 255, 135, 143, 70, 117, 63, 230, 165, 100, 227, 229]),
+                    "empty": new Uint8Array([222, 84, 247, 238, 200, 12, 156, 198, 109, 52, 159, 201, 135, 248, 13, 70, 29, 178, 239, 79, 244, 225, 133, 5, 210, 139, 216, 12, 180, 44, 125, 118])
+                },
+                "SHA-1": {
+                    "normal": new Uint8Array([173, 185, 60, 219, 206, 121, 183, 213, 17, 89, 182, 192, 19, 26, 43, 98, 242, 56, 40, 210, 106, 205, 104, 94, 52, 192, 101, 53, 230, 247, 116, 150]),
+                    "empty": new Uint8Array([71, 113, 13, 42, 117, 7, 224, 90, 29, 220, 200, 122, 124, 47, 144, 97, 119, 162, 102, 239, 185, 230, 34, 81, 12, 204, 179, 113, 60, 208, 141, 88])
+                },
+                "SHA-256": {
+                    "normal": new Uint8Array([164, 1, 215, 201, 21, 138, 41, 229, 199, 25, 58, 185, 115, 15, 7, 72, 133, 28, 197, 186, 173, 180, 44, 173, 2, 75, 98, 144, 254, 33, 52, 54]),
+                    "empty": new Uint8Array([180, 247, 231, 85, 118, 116, 213, 1, 203, 251, 192, 20, 138, 216, 0, 192, 117, 1, 137, 254, 41, 90, 42, 202, 94, 27, 244, 18, 44, 133, 237, 249])
+                }
+            }
+        },
+        "empty": {
+            "normal": {
+                "SHA-384": {
+                    "normal": new Uint8Array([106, 134, 50, 228, 134, 137, 157, 194, 100, 241, 161, 249, 32, 89, 63, 40, 128, 128, 78, 14, 26, 218, 207, 148, 235, 78, 213, 229, 248, 61, 13, 18]),
+                    "empty": new Uint8Array([234, 80, 18, 254, 181, 135, 81, 213, 188, 142, 182, 78, 13, 234, 205, 89, 126, 215, 16, 201, 243, 82, 88, 174, 107, 154, 8, 122, 237, 7, 37, 174])
+                },
+                "SHA-512": {
+                    "normal": new Uint8Array([199, 151, 225, 209, 242, 202, 183, 242, 138, 95, 67, 69, 92, 16, 89, 127, 148, 51, 133, 237, 251, 66, 140, 254, 43, 152, 190, 212, 169, 85, 215, 161]),
+                    "empty": new Uint8Array([224, 140, 220, 196, 197, 166, 170, 121, 157, 134, 188, 3, 169, 84, 117, 39, 110, 187, 128, 29, 154, 222, 1, 110, 20, 168, 250, 91, 100, 5, 22, 81])
+                },
+                "SHA-1": {
+                    "normal": new Uint8Array([171, 103, 158, 103, 188, 180, 48, 95, 238, 66, 239, 148, 14, 80, 156, 221, 212, 6, 227, 73, 143, 133, 116, 24, 169, 121, 171, 57, 207, 49, 95, 81]),
+                    "empty": new Uint8Array([254, 66, 33, 135, 24, 140, 134, 54, 211, 109, 170, 213, 142, 242, 132, 49, 164, 51, 191, 15, 239, 114, 209, 202, 231, 53, 160, 75, 219, 190, 185, 211])
+                },
+                "SHA-256": {
+                    "normal": new Uint8Array([223, 146, 185, 169, 250, 156, 1, 184, 152, 206, 234, 161, 49, 52, 131, 46, 49, 203, 28, 8, 29, 22, 165, 35, 92, 105, 216, 86, 81, 227, 23, 172]),
+                    "empty": new Uint8Array([230, 13, 67, 43, 6, 238, 136, 157, 250, 183, 41, 154, 32, 236, 35, 105, 117, 49, 209, 25, 252, 247, 102, 208, 152, 141, 10, 203, 12, 0, 199, 247])
+                }
+            },
+            "empty": {
+                "SHA-384": {
+                    "normal": new Uint8Array([234, 203, 157, 102, 112, 255, 59, 25, 4, 119, 154, 65, 145, 1, 177, 255, 170, 189, 109, 101, 16, 189, 80, 133, 104, 1, 116, 106, 135, 31, 123, 49]),
+                    "empty": new Uint8Array([71, 12, 198, 83, 135, 202, 74, 16, 199, 166, 138, 59, 81, 72, 200, 229, 19, 218, 166, 49, 1, 0, 7, 57, 196, 198, 101, 155, 134, 17, 136, 132])
+                },
+                "SHA-512": {
+                    "normal": new Uint8Array([87, 3, 145, 116, 241, 111, 84, 24, 168, 104, 86, 218, 235, 119, 246, 157, 75, 77, 80, 0, 51, 75, 109, 209, 244, 244, 179, 231, 179, 220, 185, 211]),
+                    "empty": new Uint8Array([157, 115, 201, 142, 121, 30, 128, 235, 229, 180, 203, 69, 105, 58, 163, 47, 221, 68, 181, 250, 62, 218, 179, 236, 130, 249, 208, 244, 214, 105, 5, 226])
+                },
+                "SHA-1": {
+                    "normal": new Uint8Array([161, 189, 216, 195, 50, 198, 70, 74, 75, 182, 162, 242, 49, 174, 201, 164, 68, 35, 126, 171, 224, 77, 47, 85, 242, 171, 37, 212, 12, 84, 235, 238]),
+                    "empty": new Uint8Array([136, 95, 192, 41, 179, 34, 75, 137, 110, 9, 224, 187, 229, 235, 52, 126, 197, 158, 104, 39, 200, 232, 87, 179, 148, 245, 79, 244, 155, 136, 168, 246])
+                },
+                "SHA-256": {
+                    "normal": new Uint8Array([183, 184, 110, 66, 42, 209, 200, 165, 113, 253, 165, 40, 218, 22, 160, 102, 244, 36, 134, 221, 64, 86, 121, 47, 217, 51, 98, 8, 142, 93, 212, 194]),
+                    "empty": new Uint8Array([235, 112, 240, 29, 237, 233, 175, 175, 164, 73, 238, 225, 177, 40, 101, 4, 225, 246, 35, 136, 179, 247, 221, 79, 149, 102, 151, 176, 232, 40, 254, 24])
+                }
+            }
+        }
+    };
+
+    return {derivedKeys: derivedKeys, salts: salts, derivations: derivations, derivedKeyTypes: derivedKeyTypes, infos: infos};
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/pbkdf2.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/pbkdf2.https.any.js
new file mode 100644
index 0000000..2efbe52
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/pbkdf2.https.any.js
@@ -0,0 +1,21 @@
+// META: title=WebCryptoAPI: deriveBits() and deriveKey() Using PBKDF2
+// META: timeout=long
+// META: variant=?1-1000
+// META: variant=?1001-2000
+// META: variant=?2001-3000
+// META: variant=?3001-4000
+// META: variant=?4001-5000
+// META: variant=?5001-6000
+// META: variant=?6001-7000
+// META: variant=?7001-8000
+// META: variant=?8001-last
+// META: script=/common/subset-tests.js
+// META: script=pbkdf2_vectors.js
+// META: script=pbkdf2.js
+
+// Define subtests from a `promise_test` to ensure the harness does not
+// complete before the subtests are available. `explicit_done` cannot be used
+// for this purpose because the global `done` function is automatically invoked
+// by the WPT infrastructure in dedicated worker tests defined using the
+// "multi-global" pattern.
+promise_test(define_tests, 'setup - define tests');
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/pbkdf2.js b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/pbkdf2.js
new file mode 100644
index 0000000..0403f38
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/pbkdf2.js
@@ -0,0 +1,302 @@
+function define_tests() {
+    // May want to test prefixed implementations.
+    var subtle = self.crypto.subtle;
+
+    // pbkdf2_vectors sets up test data with the correct derivations for each
+    // test case.
+    var testData = getTestData();
+    var passwords = testData.passwords;
+    var salts = testData.salts;
+    var derivations = testData.derivations;
+
+    // What kinds of keys can be created with deriveKey? The following:
+    var derivedKeyTypes = testData.derivedKeyTypes;
+
+    return setUpBaseKeys(passwords)
+    .then(function(allKeys) {
+        // We get several kinds of base keys. Normal ones that can be used for
+        // derivation operations, ones that lack the deriveBits usage, ones
+        // that lack the deriveKeys usage, and one key that is for the wrong
+        // algorithm (not PBKDF2 in this case).
+        var baseKeys = allKeys.baseKeys;
+        var noBits = allKeys.noBits;
+        var noKey = allKeys.noKey;
+        var wrongKey = allKeys.wrongKey;
+
+        // Test each combination of password size, salt size, hash function,
+        // and number of iterations. The derivations object is structured in
+        // that way, so navigate it to run tests and compare with correct results.
+        Object.keys(derivations).forEach(function(passwordSize) {
+            Object.keys(derivations[passwordSize]).forEach(function(saltSize) {
+                Object.keys(derivations[passwordSize][saltSize]).forEach(function(hashName) {
+                    Object.keys(derivations[passwordSize][saltSize][hashName]).forEach(function(iterations) {
+                        var testName = passwordSize + " password, " + saltSize + " salt, " + hashName + ", with " + iterations + " iterations";
+
+                        // Check for correct deriveBits result
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveBits({name: "PBKDF2", salt: salts[saltSize], hash: hashName, iterations: parseInt(iterations)}, baseKeys[passwordSize], 256)
+                            .then(function(derivation) {
+                                assert_true(equalBuffers(derivation, derivations[passwordSize][saltSize][hashName][iterations]), "Derived correct key");
+                            }, function(err) {
+                                assert_unreached("deriveBits failed with error " + err.name + ": " + err.message);
+                            });
+                        }, testName);
+
+                        // Check for correct deriveKey results for every kind of
+                        // key that can be created by the deriveKeys operation.
+                        derivedKeyTypes.forEach(function(derivedKeyType) {
+                            var testName = "Derived key of type ";
+                            Object.keys(derivedKeyType.algorithm).forEach(function(prop) {
+                                testName += prop + ": " + derivedKeyType.algorithm[prop] + " ";
+                            });
+                            testName += " using " + passwordSize + " password, " + saltSize + " salt, " + hashName + ", with " + iterations + " iterations";
+
+                            // Test the particular key derivation.
+                            subsetTest(promise_test, function(test) {
+                                return subtle.deriveKey({name: "PBKDF2", salt: salts[saltSize], hash: hashName, iterations: parseInt(iterations)}, baseKeys[passwordSize], derivedKeyType.algorithm, true, derivedKeyType.usages)
+                                .then(function(key) {
+                                    // Need to export the key to see that the correct bits were set.
+                                    return subtle.exportKey("raw", key)
+                                    .then(function(buffer) {
+                                        assert_true(equalBuffers(buffer, derivations[passwordSize][saltSize][hashName][iterations].slice(0, derivedKeyType.algorithm.length/8)), "Exported key matches correct value");
+                                    }, function(err) {
+                                        assert_unreached("Exporting derived key failed with error " + err.name + ": " + err.message);
+                                    });
+                                }, function(err) {
+                                    assert_unreached("deriveKey failed with error " + err.name + ": " + err.message);
+
+                                });
+                            }, testName);
+
+                            // Test various error conditions for deriveKey:
+
+                            // - illegal name for hash algorithm (NotSupportedError)
+                            var badHash = hashName.substring(0, 3) + hashName.substring(4);
+                            subsetTest(promise_test, function(test) {
+                                return subtle.deriveKey({name: "PBKDF2", salt: salts[saltSize], hash: badHash, iterations: parseInt(iterations)}, baseKeys[passwordSize], derivedKeyType.algorithm, true, derivedKeyType.usages)
+                                .then(function(key) {
+                                    assert_unreached("bad hash name should have thrown an NotSupportedError");
+                                }, function(err) {
+                                    assert_equals(err.name, "NotSupportedError", "deriveKey with bad hash name correctly threw NotSupportedError: " + err.message);
+                                });
+                            }, testName + " with bad hash name " + badHash);
+
+                            // - baseKey usages missing "deriveKey" (InvalidAccessError)
+                            subsetTest(promise_test, function(test) {
+                                return subtle.deriveKey({name: "PBKDF2", salt: salts[saltSize], hash: hashName, iterations: parseInt(iterations)}, noKey[passwordSize], derivedKeyType.algorithm, true, derivedKeyType.usages)
+                                .then(function(key) {
+                                    assert_unreached("missing deriveKey usage should have thrown an InvalidAccessError");
+                                }, function(err) {
+                                    assert_equals(err.name, "InvalidAccessError", "deriveKey with missing deriveKey usage correctly threw InvalidAccessError: " + err.message);
+                                });
+                            }, testName + " with missing deriveKey usage");
+
+                            // - baseKey algorithm does not match PBKDF2 (InvalidAccessError)
+                            subsetTest(promise_test, function(test) {
+                                return subtle.deriveKey({name: "PBKDF2", salt: salts[saltSize], hash: hashName, iterations: parseInt(iterations)}, wrongKey, derivedKeyType.algorithm, true, derivedKeyType.usages)
+                                .then(function(key) {
+                                    assert_unreached("wrong (ECDH) key should have thrown an InvalidAccessError");
+                                }, function(err) {
+                                    assert_equals(err.name, "InvalidAccessError", "deriveKey with wrong (ECDH) key correctly threw InvalidAccessError: " + err.message);
+                                });
+                            }, testName + " with wrong (ECDH) key");
+
+                        });
+
+                        // Test various error conditions for deriveBits below:
+                        // length null (OperationError)
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveBits({name: "PBKDF2", salt: salts[saltSize], hash: hashName, iterations: parseInt(iterations)}, baseKeys[passwordSize], null)
+                            .then(function(derivation) {
+                                assert_unreached("null length should have thrown an OperationError");
+                            }, function(err) {
+                                assert_equals(err.name, "OperationError", "deriveBits with null length correctly threw OperationError: " + err.message);
+                            });
+                        }, testName + " with null length");
+
+                        // 0 length (OperationError)
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveBits({name: "PBKDF2", salt: salts[saltSize], hash: hashName, iterations: parseInt(iterations)}, baseKeys[passwordSize], 0)
+                            .then(function(derivation) {
+                                assert_unreached("0 length should have thrown an OperationError");
+                            }, function(err) {
+                                assert_equals(err.name, "OperationError", "deriveBits with 0 length correctly threw OperationError: " + err.message);
+                            });
+                        }, testName + " with 0 length");
+
+                        // length not multiple of 8 (OperationError)
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveBits({name: "PBKDF2", salt: salts[saltSize], hash: hashName, iterations: parseInt(iterations)}, baseKeys[passwordSize], 44)
+                            .then(function(derivation) {
+                                assert_unreached("non-multiple of 8 length should have thrown an OperationError");
+                            }, function(err) {
+                                assert_equals(err.name, "OperationError", "deriveBits with non-multiple of 8 length correctly threw OperationError: " + err.message);
+                            });
+                        }, testName + " with non-multiple of 8 length");
+
+                        // - illegal name for hash algorithm (NotSupportedError)
+                        var badHash = hashName.substring(0, 3) + hashName.substring(4);
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveBits({name: "PBKDF2", salt: salts[saltSize], hash: badHash, iterations: parseInt(iterations)}, baseKeys[passwordSize], 256)
+                            .then(function(derivation) {
+                                assert_unreached("bad hash name should have thrown an NotSupportedError");
+                            }, function(err) {
+                                assert_equals(err.name, "NotSupportedError", "deriveBits with bad hash name correctly threw NotSupportedError: " + err.message);
+                            });
+                        }, testName + " with bad hash name " + badHash);
+
+                        // - baseKey usages missing "deriveBits" (InvalidAccessError)
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveBits({name: "PBKDF2", salt: salts[saltSize], hash: hashName, iterations: parseInt(iterations)}, noBits[passwordSize], 256)
+                            .then(function(derivation) {
+                                assert_unreached("missing deriveBits usage should have thrown an InvalidAccessError");
+                            }, function(err) {
+                                assert_equals(err.name, "InvalidAccessError", "deriveBits with missing deriveBits usage correctly threw InvalidAccessError: " + err.message);
+                            });
+                        }, testName + " with missing deriveBits usage");
+
+                        // - baseKey algorithm does not match PBKDF2 (InvalidAccessError)
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveBits({name: "PBKDF2", salt: salts[saltSize], hash: hashName, iterations: parseInt(iterations)}, wrongKey, 256)
+                            .then(function(derivation) {
+                                assert_unreached("wrong (ECDH) key should have thrown an InvalidAccessError");
+                            }, function(err) {
+                                assert_equals(err.name, "InvalidAccessError", "deriveBits with wrong (ECDH) key correctly threw InvalidAccessError: " + err.message);
+                            });
+                        }, testName + " with wrong (ECDH) key");
+                    });
+
+                    // Check that 0 iterations throws proper error
+                    subsetTest(promise_test, function(test) {
+                        return subtle.deriveBits({name: "PBKDF2", salt: salts[saltSize], hash: hashName, iterations: 0}, baseKeys[passwordSize], 256)
+                        .then(function(derivation) {
+                            assert_unreached("0 iterations should have thrown an error");
+                        }, function(err) {
+                            assert_equals(err.name, "OperationError", "deriveBits with 0 iterations correctly threw OperationError: " + err.message);
+                        });
+                    }, passwordSize + " password, " + saltSize + " salt, " + hashName + ", with 0 iterations");
+
+                    derivedKeyTypes.forEach(function(derivedKeyType) {
+                        var testName = "Derived key of type ";
+                        Object.keys(derivedKeyType.algorithm).forEach(function(prop) {
+                            testName += prop + ": " + derivedKeyType.algorithm[prop] + " ";
+                        });
+                        testName += " using " + passwordSize + " password, " + saltSize + " salt, " + hashName + ", with 0 iterations";
+
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveKey({name: "PBKDF2", salt: salts[saltSize], hash: hashName, iterations: 0}, baseKeys[passwordSize], derivedKeyType.algorithm, true, derivedKeyType.usages)
+                            .then(function(derivation) {
+                                assert_unreached("0 iterations should have thrown an error");
+                            }, function(err) {
+                                assert_equals(err.name, "OperationError", "derivekey with 0 iterations correctly threw OperationError: " + err.message);
+                            });
+                        }, testName);
+                    });
+                });
+
+                // - legal algorithm name but not digest one (e.g., PBKDF2) (NotSupportedError)
+                var nonDigestHash = "PBKDF2";
+                [1, 1000, 100000].forEach(function(iterations) {
+                    var testName = passwordSize + " password, " + saltSize + " salt, " + nonDigestHash + ", with " + iterations + " iterations";
+
+                    subsetTest(promise_test, function(test) {
+                        return subtle.deriveBits({name: "PBKDF2", salt: salts[saltSize], hash: nonDigestHash, iterations: parseInt(iterations)}, baseKeys[passwordSize], 256)
+                        .then(function(derivation) {
+                            assert_unreached("non-digest algorithm should have thrown an NotSupportedError");
+                        }, function(err) {
+                            assert_equals(err.name, "NotSupportedError", "deriveBits with non-digest algorithm correctly threw NotSupportedError: " + err.message);
+                        });
+                    }, testName + " with non-digest algorithm " + nonDigestHash);
+
+                    derivedKeyTypes.forEach(function(derivedKeyType) {
+                        var testName = "Derived key of type ";
+                        Object.keys(derivedKeyType.algorithm).forEach(function(prop) {
+                            testName += prop + ": " + derivedKeyType.algorithm[prop] + " ";
+                        });
+                        testName += " using " + passwordSize + " password, " + saltSize + " salt, " + nonDigestHash + ", with " + iterations + " iterations";
+
+                        subsetTest(promise_test, function(test) {
+                            return subtle.deriveKey({name: "PBKDF2", salt: salts[saltSize], hash: nonDigestHash, iterations: parseInt(iterations)}, baseKeys[passwordSize], derivedKeyType.algorithm, true, derivedKeyType.usages)
+                            .then(function(derivation) {
+                                assert_unreached("non-digest algorithm should have thrown an NotSupportedError");
+                            }, function(err) {
+                                assert_equals(err.name, "NotSupportedError", "derivekey with non-digest algorithm correctly threw NotSupportedError: " + err.message);
+                            });
+                        }, testName);
+                    });
+
+                });
+
+            });
+        });
+    });
+
+    // Deriving bits and keys requires starting with a base key, which is created
+    // by importing a password. setUpBaseKeys returns a promise that yields the
+    // necessary base keys.
+    function setUpBaseKeys(passwords) {
+        var promises = [];
+
+        var baseKeys = {};
+        var noBits = {};
+        var noKey = {};
+        var wrongKey = null;
+
+        Object.keys(passwords).forEach(function(passwordSize) {
+            var promise = subtle.importKey("raw", passwords[passwordSize], {name: "PBKDF2"}, false, ["deriveKey", "deriveBits"])
+            .then(function(baseKey) {
+                baseKeys[passwordSize] = baseKey;
+            }, function(err) {
+                baseKeys[passwordSize] = null;
+            });
+            promises.push(promise);
+
+            promise = subtle.importKey("raw", passwords[passwordSize], {name: "PBKDF2"}, false, ["deriveBits"])
+            .then(function(baseKey) {
+                noKey[passwordSize] = baseKey;
+            }, function(err) {
+                noKey[passwordSize] = null;
+            });
+            promises.push(promise);
+
+            promise = subtle.importKey("raw", passwords[passwordSize], {name: "PBKDF2"}, false, ["deriveKey"])
+            .then(function(baseKey) {
+                noBits[passwordSize] = baseKey;
+            }, function(err) {
+                noBits[passwordSize] = null;
+            });
+            promises.push(promise);
+        });
+
+        var promise = subtle.generateKey({name: "ECDH", namedCurve: "P-256"}, false, ["deriveKey", "deriveBits"])
+        .then(function(baseKey) {
+            wrongKey = baseKey.privateKey;
+        }, function(err) {
+            wrongKey = null;
+        });
+        promises.push(promise);
+
+
+        return Promise.all(promises).then(function() {
+            return {baseKeys: baseKeys, noBits: noBits, noKey: noKey, wrongKey: wrongKey};
+        });
+    }
+
+    function equalBuffers(a, b) {
+        if (a.byteLength !== b.byteLength) {
+            return false;
+        }
+
+        var aBytes = new Uint8Array(a);
+        var bBytes = new Uint8Array(b);
+
+        for (var i=0; i<a.byteLength; i++) {
+            if (aBytes[i] !== bBytes[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/pbkdf2_vectors.js b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/pbkdf2_vectors.js
new file mode 100644
index 0000000..b074f87
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/derive_bits_keys/pbkdf2_vectors.js
@@ -0,0 +1,269 @@
+function getTestData() {
+
+    // deriveBits and deriveKey take as input:
+    // - password (actually, a CryptoKey representing a password)
+    // - salt (BufferSource)
+    // - hash (which one to use)
+    // - iterations (how many times to use it)
+
+    // deriveBits also takes a length. deriveKey uses the length of the output key
+    // - length is the number of bits, NOT octets, but it MUST be a multiple of 8
+    // - note that result of length(n) is first n bits of length(m) if m>n
+
+    // Variations to test:
+    // - empty, short, and fairly long password
+    // - empty, short, and fairly long salt
+    // - SHA-1, SHA-256, SHA-384, SHA-512 hash
+    // - 1, 1000, and 100000 million iterations
+
+    // Test cases to generate: 3 * 3 * 4 * 3 = 108
+
+    // Error conditions to test:
+    // - length null (OperationError)
+    // - length not a multiple of 8 (OperationError)
+    // - illegal name for hash algorithm (NotSupportedError)
+    // - legal algorithm name but not digest one (e.g., AES-CBC) (NotSupportedError)
+    // - baseKey usages missing "deriveBits" (InvalidAccessError)
+    // - baseKey algorithm does not match PBKDF2 (InvalidAccessError)
+    // - 0 iterations
+
+    var derivedKeyTypes = [
+        {algorithm: {name: "AES-CBC", length: 128}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-CBC", length: 192}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-CBC", length: 256}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-CTR", length: 128}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-CTR", length: 192}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-CTR", length: 256}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-GCM", length: 128}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-GCM", length: 192}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-GCM", length: 256}, usages: ["encrypt", "decrypt"]},
+        {algorithm: {name: "AES-KW", length: 128}, usages: ["wrapKey", "unwrapKey"]},
+        {algorithm: {name: "AES-KW", length: 192}, usages: ["wrapKey", "unwrapKey"]},
+        {algorithm: {name: "AES-KW", length: 256}, usages: ["wrapKey", "unwrapKey"]},
+        {algorithm: {name: "HMAC", hash: "SHA-1", length: 256}, usages: ["sign", "verify"]},
+        {algorithm: {name: "HMAC", hash: "SHA-256", length: 256}, usages: ["sign", "verify"]},
+        {algorithm: {name: "HMAC", hash: "SHA-384", length: 256}, usages: ["sign", "verify"]},
+        {algorithm: {name: "HMAC", hash: "SHA-512", length: 256}, usages: ["sign", "verify"]}
+    ];
+
+    var passwords = {
+        "short": new Uint8Array([80, 64, 115, 115, 119, 48, 114, 100]),
+        "long": new Uint8Array([85, 115, 101, 114, 115, 32, 115, 104, 111, 117, 108, 100, 32, 112, 105, 99, 107, 32, 108, 111, 110, 103, 32, 112, 97, 115, 115, 112, 104, 114, 97, 115, 101, 115, 32, 40, 110, 111, 116, 32, 117, 115, 101, 32, 115, 104, 111, 114, 116, 32, 112, 97, 115, 115, 119, 111, 114, 100, 115, 41, 33]),
+        "empty": new Uint8Array([])
+    };
+
+    var salts = {
+        "short": new Uint8Array([78, 97, 67, 108]),
+        "long": new Uint8Array([83, 111, 100, 105, 117, 109, 32, 67, 104, 108, 111, 114, 105, 100, 101, 32, 99, 111, 109, 112, 111, 117, 110, 100]),
+        "empty": new Uint8Array([])
+    };
+
+    var derivations = {
+        "short": {
+            "short": {
+                "SHA-384": {
+                    "1000": new Uint8Array([170, 236, 90, 151, 109, 77, 53, 203, 32, 36, 72, 111, 201, 249, 187, 154, 163, 234, 231, 206, 242, 188, 230, 38, 100, 181, 179, 117, 28, 245, 15, 241]),
+                    "1": new Uint8Array([128, 205, 15, 21, 54, 67, 102, 167, 37, 81, 195, 121, 117, 247, 182, 55, 186, 137, 194, 155, 70, 57, 236, 114, 15, 105, 167, 13, 187, 237, 81, 92]),
+                    "100000": new Uint8Array([111, 94, 163, 198, 198, 245, 228, 131, 52, 103, 180, 124, 58, 103, 30, 101, 113, 78, 135, 7, 27, 209, 227, 109, 113, 111, 132, 107, 92, 210, 137, 128])
+                },
+                "SHA-512": {
+                    "1000": new Uint8Array([134, 92, 89, 69, 225, 31, 91, 243, 221, 240, 2, 231, 203, 23, 72, 246, 34, 77, 38, 113, 232, 6, 218, 212, 170, 240, 144, 160, 67, 103, 218, 41]),
+                    "1": new Uint8Array([105, 244, 213, 206, 245, 199, 216, 186, 147, 142, 136, 3, 136, 200, 246, 59, 107, 36, 72, 178, 98, 109, 19, 67, 252, 92, 182, 139, 189, 127, 39, 178]),
+                    "100000": new Uint8Array([72, 59, 167, 242, 226, 254, 56, 44, 246, 29, 32, 178, 152, 18, 226, 212, 150, 16, 166, 0, 65, 174, 64, 236, 249, 252, 126, 241, 56, 233, 56, 118])
+                },
+                "SHA-1": {
+                    "1000": new Uint8Array([83, 136, 234, 94, 98, 225, 181, 87, 152, 26, 190, 92, 228, 19, 33, 39, 88, 170, 106, 157, 44, 91, 240, 140, 1, 157, 69, 157, 186, 102, 107, 144]),
+                    "1": new Uint8Array([70, 36, 219, 210, 19, 115, 238, 86, 89, 193, 37, 177, 132, 238, 218, 162, 106, 51, 183, 124, 161, 19, 20, 185, 240, 201, 218, 225, 228, 78, 155, 4]),
+                    "100000": new Uint8Array([245, 143, 67, 95, 188, 92, 5, 134, 92, 145, 79, 217, 114, 16, 138, 9, 69, 125, 95, 154, 72, 241, 78, 117, 228, 204, 2, 217, 137, 131, 3, 138])
+                },
+                "SHA-256": {
+                    "1000": new Uint8Array([78, 108, 165, 121, 87, 67, 155, 227, 167, 83, 112, 66, 66, 37, 226, 33, 29, 85, 240, 90, 240, 5, 97, 223, 63, 62, 254, 233, 17, 107, 195, 76]),
+                    "1": new Uint8Array([198, 188, 85, 164, 4, 173, 206, 163, 106, 26, 181, 103, 152, 8, 94, 10, 175, 105, 127, 107, 178, 193, 106, 80, 114, 248, 56, 241, 125, 254, 108, 182]),
+                    "100000": new Uint8Array([171, 37, 121, 101, 152, 231, 75, 41, 195, 36, 245, 186, 77, 144, 234, 125, 200, 159, 198, 137, 16, 65, 180, 213, 108, 148, 21, 101, 5, 247, 34, 192])
+                }
+            },
+            "long": {
+                "SHA-384": {
+                    "1000": new Uint8Array([163, 16, 239, 60, 107, 58, 149, 230, 216, 202, 102, 68, 227, 220, 253, 136, 34, 42, 89, 254, 142, 0, 197, 45, 106, 18, 99, 29, 130, 193, 210, 75]),
+                    "1": new Uint8Array([104, 7, 52, 108, 197, 62, 222, 209, 203, 150, 74, 114, 98, 133, 137, 166, 189, 72, 53, 89, 144, 191, 223, 231, 70, 81, 9, 113, 2, 7, 5, 157]),
+                    "100000": new Uint8Array([44, 140, 102, 116, 200, 121, 207, 24, 80, 188, 155, 127, 189, 204, 110, 167, 171, 176, 161, 82, 33, 150, 168, 102, 135, 83, 5, 222, 165, 116, 134, 243])
+                },
+                "SHA-512": {
+                    "1000": new Uint8Array([156, 23, 254, 150, 137, 94, 173, 191, 209, 204, 9, 95, 193, 187, 131, 79, 40, 229, 204, 201, 236, 150, 202, 129, 76, 255, 148, 26, 75, 244, 7, 39]),
+                    "1": new Uint8Array([87, 119, 2, 122, 255, 64, 81, 251, 155, 67, 193, 241, 239, 4, 99, 189, 103, 117, 17, 117, 212, 40, 161, 61, 163, 218, 132, 90, 89, 19, 50, 205]),
+                    "100000": new Uint8Array([180, 121, 201, 113, 92, 66, 22, 56, 220, 224, 167, 5, 252, 11, 123, 167, 213, 111, 163, 6, 49, 136, 6, 53, 128, 224, 112, 223, 241, 219, 73, 124])
+                },
+                "SHA-1": {
+                    "1000": new Uint8Array([137, 211, 178, 123, 95, 110, 138, 240, 21, 242, 248, 124, 243, 104, 161, 67, 138, 32, 108, 78, 207, 95, 230, 129, 252, 59, 249, 76, 86, 33, 62, 246]),
+                    "1": new Uint8Array([87, 111, 124, 22, 88, 37, 190, 249, 239, 20, 180, 188, 44, 130, 70, 157, 30, 64, 143, 248, 231, 186, 48, 102, 148, 121, 127, 158, 69, 183, 102, 237]),
+                    "100000": new Uint8Array([30, 57, 232, 191, 102, 118, 252, 211, 21, 102, 85, 69, 122, 250, 20, 190, 231, 113, 219, 203, 252, 208, 114, 65, 199, 206, 226, 9, 167, 203, 31, 233])
+                },
+                "SHA-256": {
+                    "1000": new Uint8Array([177, 167, 183, 220, 32, 223, 23, 74, 74, 14, 65, 13, 191, 175, 3, 180, 195, 117, 196, 80, 168, 157, 122, 158, 211, 73, 180, 229, 46, 100, 223, 216]),
+                    "1": new Uint8Array([18, 185, 15, 89, 79, 9, 8, 207, 145, 45, 101, 92, 148, 143, 156, 42, 30, 171, 133, 87, 101, 188, 18, 120, 94, 241, 138, 160, 43, 142, 126, 220]),
+                    "100000": new Uint8Array([212, 89, 77, 138, 27, 89, 82, 10, 72, 135, 137, 34, 166, 93, 102, 61, 40, 246, 165, 250, 73, 233, 49, 211, 0, 216, 249, 186, 249, 61, 10, 235])
+                }
+            },
+            "empty": {
+                "SHA-384": {
+                    "1000": new Uint8Array([174, 181, 249, 125, 102, 39, 238, 188, 222, 107, 19, 154, 0, 137, 85, 0, 48, 247, 64, 28, 103, 224, 28, 5, 122, 51, 56, 23, 94, 63, 58, 23]),
+                    "1": new Uint8Array([79, 16, 137, 192, 30, 67, 139, 222, 100, 154, 55, 159, 164, 24, 251, 195, 184, 86, 37, 135, 114, 223, 233, 17, 128, 111, 155, 208, 128, 159, 188, 126]),
+                    "100000": new Uint8Array([215, 104, 125, 246, 199, 129, 220, 136, 214, 78, 249, 203, 175, 149, 211, 213, 209, 21, 95, 102, 178, 48, 35, 158, 110, 129, 193, 85, 12, 136, 64, 207])
+                },
+                "SHA-512": {
+                    "1000": new Uint8Array([181, 172, 114, 11, 122, 190, 8, 50, 252, 81, 163, 27, 30, 197, 103, 59, 235, 30, 65, 132, 10, 223, 211, 214, 6, 232, 99, 143, 64, 6, 235, 72]),
+                    "1": new Uint8Array([143, 123, 125, 69, 156, 117, 47, 100, 191, 18, 190, 98, 91, 101, 212, 150, 172, 36, 234, 54, 81, 107, 22, 142, 22, 251, 2, 104, 69, 180, 232, 46]),
+                    "100000": new Uint8Array([186, 26, 15, 54, 186, 215, 113, 82, 101, 100, 5, 30, 185, 202, 32, 125, 161, 155, 98, 229, 55, 98, 52, 153, 118, 169, 163, 209, 176, 239, 126, 32])
+                },
+                "SHA-1": {
+                    "1000": new Uint8Array([115, 111, 60, 61, 110, 188, 194, 167, 185, 112, 64, 62, 38, 150, 192, 235, 76, 209, 119, 15, 85, 241, 150, 252, 112, 137, 230, 102, 193, 31, 119, 218]),
+                    "1": new Uint8Array([192, 207, 251, 12, 229, 219, 53, 31, 170, 36, 218, 213, 144, 37, 131, 207, 195, 10, 159, 84, 217, 170, 105, 145, 254, 130, 29, 3, 18, 33, 39, 233]),
+                    "100000": new Uint8Array([28, 80, 149, 172, 154, 123, 212, 16, 239, 15, 114, 201, 147, 236, 169, 27, 176, 229, 113, 233, 178, 251, 171, 112, 79, 140, 19, 17, 145, 250, 209, 108])
+                },
+                "SHA-256": {
+                    "1000": new Uint8Array([185, 210, 242, 33, 123, 78, 229, 168, 191, 3, 69, 243, 107, 44, 152, 135, 51, 245, 3, 169, 117, 223, 234, 199, 183, 19, 95, 84, 165, 242, 153, 113]),
+                    "1": new Uint8Array([1, 158, 84, 171, 66, 240, 4, 133, 211, 170, 27, 38, 252, 222, 33, 174, 95, 82, 203, 15, 9, 96, 255, 201, 118, 127, 37, 198, 94, 45, 178, 249]),
+                    "100000": new Uint8Array([167, 162, 134, 152, 41, 121, 120, 7, 179, 229, 118, 193, 120, 120, 180, 102, 68, 158, 137, 230, 4, 71, 213, 65, 119, 90, 150, 235, 124, 26, 93, 237])
+                }
+            }
+        },
+        "long": {
+            "short": {
+                "SHA-384": {
+                    "1000": new Uint8Array([250, 164, 66, 251, 171, 244, 5, 140, 198, 83, 104, 181, 61, 126, 197, 17, 60, 9, 234, 126, 94, 55, 67, 49, 47, 75, 235, 237, 217, 128, 186, 55]),
+                    "1": new Uint8Array([94, 222, 136, 54, 253, 171, 238, 197, 211, 115, 59, 67, 74, 186, 196, 67, 212, 21, 25, 59, 89, 158, 9, 38, 25, 59, 0, 15, 64, 106, 90, 125]),
+                    "100000": new Uint8Array([246, 42, 230, 199, 135, 27, 24, 26, 167, 18, 50, 245, 235, 136, 55, 36, 152, 239, 50, 172, 10, 125, 113, 81, 25, 232, 240, 82, 235, 16, 45, 41])
+                },
+                "SHA-512": {
+                    "1000": new Uint8Array([240, 146, 143, 80, 161, 85, 242, 106, 140, 156, 27, 199, 243, 181, 203, 83, 28, 83, 168, 245, 16, 64, 201, 206, 95, 199, 157, 67, 15, 240, 192, 244]),
+                    "1": new Uint8Array([62, 156, 18, 179, 246, 223, 182, 68, 21, 148, 236, 112, 99, 252, 169, 98, 255, 218, 16, 182, 207, 48, 184, 152, 163, 30, 249, 241, 48, 107, 17, 25]),
+                    "100000": new Uint8Array([151, 74, 207, 187, 15, 15, 32, 200, 30, 201, 40, 41, 243, 140, 61, 175, 8, 106, 125, 245, 139, 145, 43, 133, 109, 31, 94, 204, 147, 85, 239, 27])
+                },
+                "SHA-1": {
+                    "1000": new Uint8Array([83, 180, 33, 97, 19, 78, 21, 200, 113, 171, 215, 26, 186, 19, 144, 208, 31, 76, 106, 148, 12, 170, 245, 193, 121, 37, 141, 143, 27, 29, 104, 11]),
+                    "1": new Uint8Array([138, 231, 47, 148, 230, 252, 213, 79, 203, 252, 166, 98, 0, 162, 17, 165, 27, 47, 132, 103, 135, 210, 11, 104, 8, 190, 223, 21, 108, 228, 108, 160]),
+                    "100000": new Uint8Array([167, 253, 164, 199, 157, 211, 186, 26, 135, 95, 101, 233, 36, 139, 33, 8, 153, 202, 8, 20, 174, 56, 153, 93, 140, 229, 165, 53, 96, 203, 172, 49])
+                },
+                "SHA-256": {
+                    "1000": new Uint8Array([238, 235, 119, 20, 66, 10, 0, 177, 138, 206, 194, 181, 151, 157, 29, 166, 19, 115, 32, 43, 127, 139, 167, 27, 8, 98, 147, 170, 184, 89, 224, 160]),
+                    "1": new Uint8Array([255, 161, 233, 167, 39, 169, 44, 39, 174, 111, 116, 177, 199, 151, 143, 158, 26, 248, 96, 225, 6, 55, 99, 64, 172, 67, 217, 105, 209, 54, 64, 91]),
+                    "100000": new Uint8Array([222, 172, 112, 203, 227, 241, 114, 14, 53, 59, 78, 128, 22, 221, 181, 148, 117, 239, 183, 11, 106, 35, 133, 231, 53, 210, 214, 234, 109, 98, 74, 77])
+                }
+            },
+            "long": {
+                "SHA-384": {
+                    "1000": new Uint8Array([53, 101, 133, 81, 240, 236, 19, 57, 138, 123, 69, 224, 38, 28, 253, 101, 76, 30, 82, 65, 30, 110, 69, 125, 238, 104, 244, 174, 171, 233, 37, 167]),
+                    "1": new Uint8Array([207, 85, 66, 44, 239, 110, 27, 196, 158, 109, 8, 43, 34, 115, 212, 128, 232, 242, 232, 130, 45, 173, 209, 70, 156, 42, 50, 217, 101, 125, 18, 241]),
+                    "100000": new Uint8Array([26, 186, 181, 241, 228, 97, 223, 55, 139, 136, 192, 162, 43, 231, 110, 242, 241, 98, 125, 247, 74, 199, 203, 251, 132, 189, 204, 179, 84, 188, 136, 137])
+                },
+                "SHA-512": {
+                    "1000": new Uint8Array([67, 225, 32, 36, 196, 211, 84, 114, 127, 126, 88, 132, 44, 203, 96, 51, 161, 97, 214, 13, 197, 174, 81, 111, 7, 110, 74, 88, 161, 136, 13, 56]),
+                    "1": new Uint8Array([222, 74, 251, 192, 173, 211, 228, 211, 47, 75, 198, 225, 34, 168, 138, 228, 74, 43, 60, 207, 1, 72, 231, 118, 43, 172, 5, 196, 62, 148, 239, 127]),
+                    "100000": new Uint8Array([249, 169, 35, 132, 164, 234, 223, 195, 86, 6, 73, 179, 127, 182, 118, 232, 60, 69, 60, 187, 217, 159, 128, 187, 166, 240, 161, 14, 189, 21, 11, 82])
+                },
+                "SHA-1": {
+                    "1000": new Uint8Array([110, 144, 200, 110, 224, 123, 135, 62, 150, 80, 113, 2, 86, 115, 255, 5, 66, 159, 103, 140, 48, 249, 27, 55, 225, 226, 218, 81, 32, 54, 211, 32]),
+                    "1": new Uint8Array([29, 16, 78, 165, 210, 53, 0, 106, 18, 168, 15, 113, 184, 14, 229, 40, 4, 139, 100, 204, 26, 122, 15, 48, 247, 223, 75, 162, 107, 131, 32, 199]),
+                    "100000": new Uint8Array([20, 16, 48, 118, 59, 249, 131, 200, 86, 77, 93, 76, 147, 95, 227, 202, 53, 73, 96, 129, 89, 172, 25, 52, 193, 89, 144, 64, 102, 140, 35, 99])
+                },
+                "SHA-256": {
+                    "1000": new Uint8Array([63, 213, 135, 201, 75, 169, 70, 184, 185, 220, 205, 221, 42, 91, 116, 246, 119, 141, 79, 97, 230, 145, 248, 58, 196, 122, 47, 169, 88, 11, 253, 248]),
+                    "1": new Uint8Array([253, 92, 174, 184, 179, 171, 229, 137, 188, 21, 156, 78, 81, 248, 0, 87, 14, 116, 246, 67, 151, 166, 197, 238, 19, 29, 254, 217, 63, 5, 17, 170]),
+                    "100000": new Uint8Array([17, 153, 45, 139, 129, 51, 17, 36, 76, 84, 75, 98, 41, 41, 69, 226, 8, 212, 3, 206, 189, 107, 149, 82, 161, 165, 98, 6, 93, 153, 88, 234])
+                }
+            },
+            "empty": {
+                "SHA-384": {
+                    "1000": new Uint8Array([249, 202, 20, 139, 12, 4, 24, 144, 191, 248, 131, 29, 182, 23, 71, 25, 126, 148, 206, 104, 241, 144, 237, 242, 105, 105, 75, 77, 100, 72, 97, 202]),
+                    "1": new Uint8Array([73, 171, 63, 159, 136, 47, 219, 158, 82, 139, 77, 159, 27, 62, 140, 113, 210, 99, 154, 191, 23, 1, 213, 110, 185, 155, 213, 18, 1, 228, 32, 255]),
+                    "100000": new Uint8Array([23, 73, 223, 205, 119, 229, 37, 133, 25, 234, 34, 49, 186, 44, 214, 84, 59, 7, 51, 57, 172, 155, 21, 69, 187, 100, 49, 83, 250, 246, 209, 123])
+                },
+                "SHA-512": {
+                    "1000": new Uint8Array([69, 122, 121, 85, 235, 236, 236, 113, 165, 30, 251, 98, 55, 229, 177, 214, 47, 77, 234, 181, 201, 61, 123, 61, 17, 209, 231, 15, 175, 250, 65, 126]),
+                    "1": new Uint8Array([209, 191, 161, 166, 184, 169, 119, 131, 159, 140, 63, 157, 82, 221, 2, 16, 78, 32, 41, 192, 235, 42, 98, 8, 204, 64, 136, 22, 231, 118, 138, 140]),
+                    "100000": new Uint8Array([232, 5, 172, 156, 193, 216, 65, 44, 66, 68, 109, 35, 125, 27, 80, 79, 149, 64, 179, 98, 189, 27, 117, 228, 81, 83, 30, 133, 62, 36, 117, 61])
+                },
+                "SHA-1": {
+                    "1000": new Uint8Array([231, 55, 93, 229, 3, 103, 102, 196, 12, 184, 95, 67, 181, 63, 206, 79, 250, 64, 42, 182, 190, 53, 113, 0, 126, 245, 213, 84, 83, 253, 127, 10]),
+                    "1": new Uint8Array([164, 106, 98, 152, 109, 156, 57, 9, 244, 16, 20, 221, 114, 207, 227, 74, 38, 18, 71, 133, 77, 115, 18, 207, 79, 190, 173, 96, 185, 182, 158, 221]),
+                    "100000": new Uint8Array([122, 64, 61, 154, 19, 174, 216, 22, 78, 156, 7, 44, 84, 84, 98, 37, 31, 217, 66, 241, 115, 106, 107, 240, 60, 225, 200, 131, 48, 4, 142, 4])
+                },
+                "SHA-256": {
+                    "1000": new Uint8Array([126, 102, 200, 75, 234, 136, 143, 146, 195, 72, 217, 20, 85, 133, 24, 108, 174, 71, 43, 18, 251, 167, 240, 173, 40, 23, 149, 117, 193, 170, 129, 90]),
+                    "1": new Uint8Array([79, 81, 12, 81, 129, 172, 92, 44, 95, 212, 189, 20, 31, 151, 18, 73, 91, 236, 162, 121, 98, 71, 66, 180, 214, 211, 13, 8, 185, 108, 10, 105]),
+                    "100000": new Uint8Array([95, 26, 106, 196, 165, 109, 151, 150, 167, 48, 154, 120, 218, 170, 249, 24, 186, 218, 245, 237, 30, 236, 195, 240, 184, 163, 164, 76, 61, 56, 214, 84])
+                }
+            }
+        },
+        "empty": {
+            "short": {
+                "SHA-384": {
+                    "1000": new Uint8Array([127, 247, 149, 74, 237, 223, 65, 121, 95, 200, 48, 6, 102, 120, 109, 73, 116, 38, 154, 169, 28, 199, 233, 56, 17, 201, 83, 51, 29, 86, 214, 9]),
+                    "1": new Uint8Array([233, 240, 218, 30, 151, 223, 164, 85, 248, 88, 206, 107, 154, 241, 236, 192, 41, 159, 18, 95, 241, 168, 71, 235, 93, 73, 85, 134, 111, 67, 230, 4]),
+                    "100000": new Uint8Array([28, 115, 19, 43, 106, 85, 233, 217, 222, 44, 219, 254, 31, 85, 191, 10, 181, 159, 217, 31, 120, 241, 9, 197, 0, 150, 3, 139, 133, 87, 177, 71])
+                },
+                "SHA-512": {
+                    "1000": new Uint8Array([213, 97, 196, 200, 78, 156, 96, 186, 71, 82, 162, 211, 131, 191, 85, 239, 246, 67, 252, 158, 69, 34, 82, 214, 130, 30, 57, 68, 147, 80, 207, 114]),
+                    "1": new Uint8Array([231, 226, 180, 31, 72, 135, 66, 27, 203, 118, 78, 180, 165, 111, 99, 210, 80, 46, 51, 199, 100, 251, 223, 96, 98, 106, 212, 46, 217, 103, 35, 66]),
+                    "100000": new Uint8Array([239, 208, 7, 82, 188, 159, 250, 251, 90, 57, 157, 209, 213, 131, 78, 141, 44, 43, 103, 110, 205, 75, 32, 99, 251, 31, 229, 129, 208, 241, 56, 11])
+                },
+                "SHA-1": {
+                    "1000": new Uint8Array([114, 201, 43, 189, 61, 218, 180, 120, 158, 136, 228, 42, 209, 205, 168, 60, 192, 114, 158, 108, 181, 16, 106, 87, 126, 80, 213, 207, 97, 120, 36, 129]),
+                    "1": new Uint8Array([166, 103, 218, 71, 184, 248, 87, 183, 198, 95, 112, 166, 200, 231, 160, 108, 224, 210, 82, 17, 162, 182, 235, 175, 88, 220, 170, 242, 104, 180, 107, 29]),
+                    "100000": new Uint8Array([6, 225, 158, 27, 131, 230, 72, 11, 21, 84, 223, 43, 49, 162, 201, 45, 27, 252, 249, 188, 27, 219, 200, 117, 31, 248, 104, 91, 222, 239, 125, 201])
+                },
+                "SHA-256": {
+                    "1000": new Uint8Array([40, 53, 243, 237, 83, 86, 84, 32, 201, 9, 81, 80, 155, 12, 17, 115, 182, 69, 23, 79, 21, 70, 171, 58, 195, 230, 200, 92, 180, 113, 181, 59]),
+                    "1": new Uint8Array([45, 219, 73, 36, 62, 179, 181, 145, 44, 178, 96, 205, 216, 127, 176, 78, 240, 209, 17, 191, 164, 77, 64, 164, 94, 2, 168, 165, 195, 193, 81, 141]),
+                    "100000": new Uint8Array([128, 174, 217, 5, 202, 50, 174, 11, 178, 169, 216, 245, 50, 240, 72, 160, 230, 114, 70, 62, 239, 159, 131, 223, 167, 216, 139, 202, 114, 101, 83, 234])
+                }
+            },
+            "long": {
+                "SHA-384": {
+                    "1000": new Uint8Array([139, 184, 156, 247, 25, 114, 254, 90, 204, 22, 253, 197, 248, 207, 253, 44, 46, 113, 120, 192, 134, 179, 187, 230, 28, 193, 49, 70, 25, 19, 89, 88]),
+                    "1": new Uint8Array([123, 11, 204, 168, 29, 214, 55, 163, 179, 57, 134, 102, 97, 151, 22, 197, 242, 177, 244, 165, 194, 78, 133, 193, 138, 153, 85, 85, 158, 77, 118, 146]),
+                    "100000": new Uint8Array([38, 198, 168, 174, 75, 209, 251, 231, 21, 174, 71, 142, 255, 243, 236, 174, 131, 175, 166, 23, 237, 53, 189, 74, 63, 99, 195, 218, 118, 164, 45, 34])
+                },
+                "SHA-512": {
+                    "1000": new Uint8Array([92, 172, 193, 108, 223, 190, 5, 44, 253, 115, 169, 137, 27, 140, 14, 120, 177, 155, 46, 7, 234, 226, 66, 61, 72, 254, 213, 224, 138, 168, 73, 75]),
+                    "1": new Uint8Array([187, 115, 248, 22, 138, 143, 57, 29, 61, 84, 202, 137, 47, 183, 43, 142, 96, 53, 227, 127, 137, 30, 90, 112, 73, 27, 148, 220, 5, 81, 11, 196]),
+                    "100000": new Uint8Array([135, 253, 252, 41, 51, 146, 203, 243, 62, 204, 155, 81, 65, 162, 254, 250, 116, 209, 80, 73, 151, 86, 134, 60, 72, 76, 10, 120, 182, 39, 77, 127])
+                },
+                "SHA-1": {
+                    "1000": new Uint8Array([204, 87, 72, 236, 196, 18, 136, 160, 225, 51, 104, 84, 58, 170, 46, 246, 44, 151, 186, 117, 24, 250, 136, 246, 225, 28, 53, 118, 63, 201, 48, 180]),
+                    "1": new Uint8Array([31, 70, 180, 12, 242, 251, 61, 196, 26, 61, 156, 237, 136, 151, 184, 97, 5, 3, 104, 16, 226, 191, 172, 112, 64, 129, 75, 214, 93, 66, 141, 103]),
+                    "100000": new Uint8Array([51, 226, 153, 59, 244, 114, 157, 201, 147, 255, 246, 110, 105, 204, 85, 119, 113, 53, 235, 250, 188, 229, 51, 87, 91, 206, 74, 150, 100, 90, 116, 44])
+                },
+                "SHA-256": {
+                    "1000": new Uint8Array([19, 83, 247, 69, 130, 55, 171, 51, 46, 224, 82, 226, 159, 130, 154, 42, 185, 14, 114, 99, 14, 161, 4, 147, 180, 238, 207, 251, 159, 248, 158, 29]),
+                    "1": new Uint8Array([97, 201, 53, 196, 98, 195, 50, 28, 137, 102, 53, 69, 209, 58, 79, 107, 82, 181, 25, 28, 251, 116, 121, 229, 141, 207, 230, 68, 77, 67, 16, 108]),
+                    "100000": new Uint8Array([121, 186, 248, 14, 197, 130, 146, 5, 56, 128, 30, 157, 146, 156, 224, 112, 132, 39, 121, 135, 72, 141, 115, 58, 2, 104, 82, 196, 82, 240, 111, 180])
+                }
+            },
+            "empty": {
+                "SHA-384": {
+                    "1000": new Uint8Array([156, 191, 231, 45, 25, 77, 163, 78, 23, 200, 33, 221, 21, 105, 239, 80, 168, 110, 180, 216, 147, 89, 23, 118, 173, 198, 165, 194, 30, 0, 49, 207]),
+                    "1": new Uint8Array([75, 176, 66, 165, 194, 140, 238, 111, 102, 249, 145, 199, 23, 253, 119, 2, 103, 120, 126, 43, 179, 3, 30, 174, 39, 13, 135, 214, 58, 217, 149, 52]),
+                    "100000": new Uint8Array([237, 107, 215, 40, 37, 103, 171, 228, 141, 84, 45, 6, 125, 9, 244, 4, 189, 4, 74, 226, 206, 254, 17, 218, 204, 83, 28, 71, 100, 205, 53, 205])
+                },
+                "SHA-512": {
+                    "1000": new Uint8Array([203, 147, 9, 108, 58, 2, 190, 235, 28, 95, 172, 54, 118, 92, 144, 17, 254, 153, 248, 216, 234, 98, 54, 96, 72, 252, 152, 203, 152, 223, 234, 143]),
+                    "1": new Uint8Array([109, 46, 203, 187, 251, 46, 109, 205, 112, 86, 250, 249, 175, 106, 160, 110, 174, 89, 67, 145, 219, 152, 50, 121, 166, 191, 39, 224, 235, 34, 134, 20]),
+                    "100000": new Uint8Array([137, 225, 98, 84, 235, 173, 92, 186, 114, 224, 174, 190, 22, 20, 199, 249, 183, 149, 167, 80, 95, 38, 55, 32, 108, 225, 10, 52, 73, 162, 184, 187])
+                },
+                "SHA-1": {
+                    "1000": new Uint8Array([110, 64, 145, 10, 192, 46, 200, 156, 235, 185, 216, 152, 177, 58, 9, 209, 205, 122, 223, 111, 140, 192, 140, 196, 115, 48, 44, 137, 115, 170, 46, 25]),
+                    "1": new Uint8Array([30, 67, 122, 28, 121, 215, 91, 230, 30, 145, 20, 29, 174, 32, 175, 252, 72, 146, 204, 153, 171, 204, 63, 231, 83, 136, 123, 204, 200, 146, 1, 118]),
+                    "100000": new Uint8Array([169, 225, 190, 187, 54, 188, 38, 215, 201, 151, 213, 72, 60, 188, 141, 228, 164, 25, 209, 231, 6, 87, 19, 66, 99, 37, 134, 236, 51, 10, 114, 144])
+                },
+                "SHA-256": {
+                    "1000": new Uint8Array([79, 197, 138, 33, 193, 0, 206, 24, 53, 184, 249, 153, 29, 115, 139, 86, 150, 93, 20, 178, 78, 23, 97, 251, 223, 252, 105, 172, 94, 11, 102, 122]),
+                    "1": new Uint8Array([247, 206, 11, 101, 61, 45, 114, 164, 16, 140, 245, 171, 233, 18, 255, 221, 119, 118, 22, 219, 187, 39, 167, 14, 130, 4, 243, 174, 45, 15, 111, 173]),
+                    "100000": new Uint8Array([100, 168, 104, 212, 178, 58, 246, 150, 211, 115, 77, 11, 129, 77, 4, 205, 209, 172, 40, 1, 40, 233, 118, 83, 160, 95, 50, 180, 156, 19, 162, 154])
+                }
+            }
+        }
+    };
+
+    return {passwords: passwords, salts: salts, derivations: derivations, derivedKeyTypes: derivedKeyTypes};
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/digest/digest.https.worker.js b/src/third_party/web_platform_tests/WebCryptoAPI/digest/digest.https.worker.js
new file mode 100644
index 0000000..9b1072e
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/digest/digest.https.worker.js
@@ -0,0 +1,4 @@
+importScripts("/resources/testharness.js");
+importScripts("digest.js");
+
+run_test();
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/digest/digest.js b/src/third_party/web_platform_tests/WebCryptoAPI/digest/digest.js
new file mode 100644
index 0000000..c557d96
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/digest/digest.js
@@ -0,0 +1,154 @@
+
+function run_test() {
+    var subtle = crypto.subtle; // Change to test prefixed implementations
+
+    var sourceData = {
+        empty: new Uint8Array(0),
+        short: new Uint8Array([21, 110, 234, 124, 193, 76, 86, 203, 148, 219, 3, 10, 74, 157, 149, 255]),
+        medium: new Uint8Array([182, 200, 249, 223, 100, 140, 208, 136, 183, 15, 56, 231, 65, 151, 177, 140, 184, 30, 30, 67, 80, 213, 11, 204, 184, 251, 90, 115, 121, 200, 123, 178, 227, 214, 237, 84, 97, 237, 30, 159, 54, 243, 64, 163, 150, 42, 68, 107, 129, 91, 121, 75, 75, 212, 58, 68, 3, 80, 32, 119, 178, 37, 108, 200, 7, 131, 127, 58, 172, 209, 24, 235, 75, 156, 43, 174, 184, 151, 6, 134, 37, 171, 172, 161, 147])
+    };
+
+    sourceData.long = new Uint8Array(1024 * sourceData.medium.byteLength);
+    for (var i=0; i<1024; i++) {
+        sourceData.long.set(sourceData.medium, i * sourceData.medium.byteLength);
+    }
+
+    var digestedData = {
+        "sha-1": {
+            empty: new Uint8Array([218, 57, 163, 238, 94, 107, 75, 13, 50, 85, 191, 239, 149, 96, 24, 144, 175, 216, 7, 9]),
+            short: new Uint8Array([201, 19, 24, 205, 242, 57, 106, 1, 94, 63, 78, 106, 134, 160, 186, 101, 184, 99, 89, 68]),
+            medium: new Uint8Array([229, 65, 6, 8, 112, 235, 22, 191, 51, 182, 142, 81, 245, 19, 82, 104, 147, 152, 103, 41]),
+            long: new Uint8Array([48, 152, 181, 0, 55, 236, 208, 46, 189, 101, 118, 83, 178, 191, 160, 30, 238, 39, 162, 234])
+        },
+        "sha-256": {
+            empty: new Uint8Array([227, 176, 196, 66, 152, 252, 28, 20, 154, 251, 244, 200, 153, 111, 185, 36, 39, 174, 65, 228, 100, 155, 147, 76, 164, 149, 153, 27, 120, 82, 184, 85]),
+            short: new Uint8Array([162, 131, 17, 134, 152, 71, 146, 199, 211, 45, 89, 200, 151, 64, 104, 127, 25, 173, 220, 27, 149, 158, 113, 161, 204, 83, 138, 59, 126, 216, 67, 242]),
+            medium: new Uint8Array([83, 83, 103, 135, 126, 240, 20, 215, 252, 113, 126, 92, 183, 132, 62, 89, 182, 26, 238, 98, 199, 2, 156, 236, 126, 198, 193, 47, 217, 36, 224, 228]),
+            long: new Uint8Array([20, 205, 234, 157, 199, 95, 90, 98, 116, 217, 252, 30, 100, 0, 153, 18, 241, 220, 211, 6, 180, 143, 232, 233, 207, 18, 45, 230, 113, 87, 23, 129])
+        },
+        "sha-384": {
+            empty: new Uint8Array([56, 176, 96, 167, 81, 172, 150, 56, 76, 217, 50, 126, 177, 177, 227, 106, 33, 253, 183, 17, 20, 190, 7, 67, 76, 12, 199, 191, 99, 246, 225, 218, 39, 78, 222, 191, 231, 111, 101, 251, 213, 26, 210, 241, 72, 152, 185, 91]),
+            short: new Uint8Array([107, 245, 234, 101, 36, 209, 205, 220, 67, 247, 207, 59, 86, 238, 5, 146, 39, 64, 74, 47, 83, 143, 2, 42, 61, 183, 68, 122, 120, 44, 6, 193, 237, 5, 232, 171, 79, 94, 220, 23, 243, 113, 20, 64, 223, 233, 119, 49]),
+            medium: new Uint8Array([203, 194, 197, 136, 254, 91, 37, 249, 22, 218, 40, 180, 228, 122, 72, 74, 230, 252, 31, 228, 144, 45, 213, 201, 147, 154, 107, 253, 3, 74, 179, 180, 139, 57, 8, 116, 54, 1, 31, 106, 153, 135, 157, 39, 149, 64, 233, 119]),
+            long: new Uint8Array([73, 244, 253, 179, 152, 25, 104, 249, 125, 87, 55, 15, 133, 52, 80, 103, 205, 82, 150, 169, 125, 209, 161, 142, 6, 145, 30, 117, 110, 150, 8, 73, 37, 41, 135, 14, 26, 209, 48, 153, 141, 87, 203, 251, 183, 193, 208, 158])
+        },
+        "sha-512": {
+            empty: new Uint8Array([207, 131, 225, 53, 126, 239, 184, 189, 241, 84, 40, 80, 214, 109, 128, 7, 214, 32, 228, 5, 11, 87, 21, 220, 131, 244, 169, 33, 211, 108, 233, 206, 71, 208, 209, 60, 93, 133, 242, 176, 255, 131, 24, 210, 135, 126, 236, 47, 99, 185, 49, 189, 71, 65, 122, 129, 165, 56, 50, 122, 249, 39, 218, 62]),
+            short: new Uint8Array([55, 82, 72, 190, 95, 243, 75, 231, 76, 171, 79, 241, 195, 188, 141, 198, 139, 213, 248, 223, 244, 2, 62, 152, 248, 123, 134, 92, 255, 44, 114, 66, 146, 223, 24, 148, 67, 166, 79, 244, 19, 74, 101, 205, 70, 53, 185, 212, 245, 220, 13, 63, 182, 117, 40, 0, 42, 99, 172, 242, 108, 157, 165, 117]),
+            medium: new Uint8Array([185, 16, 159, 131, 158, 142, 164, 60, 137, 15, 41, 60, 225, 29, 198, 226, 121, 141, 30, 36, 49, 241, 228, 185, 25, 227, 178, 12, 79, 54, 48, 59, 163, 156, 145, 109, 179, 6, 196, 90, 59, 101, 118, 31, 245, 190, 133, 50, 142, 234, 244, 44, 56, 48, 241, 217, 94, 122, 65, 22, 91, 125, 45, 54]),
+            long: new Uint8Array([75, 2, 202, 246, 80, 39, 96, 48, 234, 86, 23, 229, 151, 197, 213, 63, 217, 218, 166, 139, 120, 191, 230, 11, 34, 170, 184, 211, 106, 76, 42, 58, 255, 219, 113, 35, 79, 73, 39, 103, 55, 197, 117, 221, 247, 77, 20, 5, 76, 189, 111, 219, 152, 253, 13, 220, 188, 180, 111, 145, 173, 118, 182, 238])
+        },
+    }
+
+    // Try every combination of hash with source data size. Variations tested are
+    // hash name in upper, lower, or mixed case, and upper-case version with the
+    // source buffer altered after call.
+    Object.keys(sourceData).forEach(function(size) {
+        Object.keys(digestedData).forEach(function(alg) {
+            var upCase = alg.toUpperCase();
+            var downCase = alg.toLowerCase();
+            var mixedCase = upCase.substr(0, 1) + downCase.substr(1);
+
+            promise_test(function(test) {
+                var promise = subtle.digest({name: upCase}, sourceData[size])
+                .then(function(result) {
+                    assert_true(equalBuffers(result, digestedData[alg][size]), "digest() yielded expected result for " + alg + ":" + size);
+                }, function(err) {
+                    assert_unreached("digest() threw an error for " + alg + ":" + size + " - " + err.message);
+                });
+
+                return promise;
+            }, upCase + " with " + size + " source data");
+
+            promise_test(function(test) {
+                var promise = subtle.digest({name: downCase}, sourceData[size])
+                .then(function(result) {
+                    assert_true(equalBuffers(result, digestedData[alg][size]), "digest() yielded expected result for " + alg + ":" + size);
+                }, function(err) {
+                    assert_unreached("digest() threw an error for " + alg + ":" + size + " - " + err.message);
+                });
+
+                return promise;
+            }, downCase + " with " + size + " source data");
+
+            promise_test(function(test) {
+                var promise = subtle.digest({name: mixedCase}, sourceData[size])
+                .then(function(result) {
+                    assert_true(equalBuffers(result, digestedData[alg][size]), "digest() yielded expected result for " + alg + ":" + size);
+                }, function(err) {
+                    assert_unreached("digest() threw an error for " + alg + ":" + size + " - " + err.message);
+                });
+
+                return promise;
+            }, mixedCase + " with " + size + " source data");
+
+            promise_test(function(test) {
+                var copiedBuffer = copyBuffer(sourceData[size]);
+                var promise = subtle.digest({name: upCase}, copiedBuffer)
+                .then(function(result) {
+                    assert_true(equalBuffers(result, digestedData[alg][size]), "digest() yielded expected result for " + alg + ":" + size);
+                }, function(err) {
+                    assert_unreached("digest() threw an error for " + alg + ":" + size + " - " + err.message);
+                });
+
+                copiedBuffer[0] = 255 - copiedBuffer;
+                return promise;
+            }, upCase + " with " + size + " source data and altered buffer after call");
+
+        });
+    });
+
+    // Call digest() with bad algorithm names to get an error
+    var badNames = ["AES-GCM", "RSA-OAEP", "PBKDF2", "AES-KW"];
+    Object.keys(sourceData).forEach(function(size) {
+        badNames.forEach(function(badName) {
+
+            promise_test(function(test) {
+                var promise = subtle.digest({name: badName}, sourceData[size])
+                .then(function(result) {
+                    assert_unreached("digest() should not have worked for " + alg + ":" + size);
+                }, function(err) {
+                    assert_equals(err.name, "NotSupportedError", "Bad algorithm name should cause NotSupportedError")
+                });
+
+                return promise;
+            }, badName + " with " + size);
+
+        });
+    });
+
+
+    done();
+
+
+    // Returns a copy of the sourceBuffer it is sent.
+    function copyBuffer(sourceBuffer) {
+        var source = new Uint8Array(sourceBuffer);
+        var copy = new Uint8Array(sourceBuffer.byteLength)
+
+        for (var i=0; i<source.byteLength; i++) {
+            copy[i] = source[i];
+        }
+
+        return copy;
+    }
+
+    function equalBuffers(a, b) {
+        if (a.byteLength !== b.byteLength) {
+            return false;
+        }
+
+        var aBytes = new Uint8Array(a);
+        var bBytes = new Uint8Array(b);
+
+        for (var i=0; i<a.byteLength; i++) {
+            if (aBytes[i] !== bBytes[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    return;
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/digest/test_digest.https.html b/src/third_party/web_platform_tests/WebCryptoAPI/digest/test_digest.https.html
new file mode 100644
index 0000000..e6e7aab
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/digest/test_digest.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>WebCryptoAPI: digest()</title>
+<link rel="author" title="Charles Engelke" href="mailto:w3c@engelke.com">
+<link rel="help" href="https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#dfn-SubtleCrypto-method-digest">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script src="digest.js"></script>
+
+<h1>encrypt Tests for digest method</h1>
+
+<div id="log"></div>
+<script>
+run_test();
+</script>
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes.js b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes.js
new file mode 100644
index 0000000..e064353
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes.js
@@ -0,0 +1,323 @@
+
+function run_test() {
+    var subtle = self.crypto.subtle; // Change to test prefixed implementations
+
+    // When are all these tests really done? When all the promises they use have resolved.
+    var all_promises = [];
+
+    // Source file aes_XXX_vectors.js provides the getTestVectors method
+    // for the AES-XXX algorithm that drives these tests.
+    var vectors = getTestVectors();
+    var passingVectors = vectors.passing;
+    var failingVectors = vectors.failing;
+    var decryptionFailingVectors = vectors.decryptionFailing;
+
+    // Check for successful encryption.
+    passingVectors.forEach(function(vector) {
+        var promise = importVectorKey(vector, ["encrypt", "decrypt"])
+        .then(function(vector) {
+            promise_test(function(test) {
+                return subtle.encrypt(vector.algorithm, vector.key, vector.plaintext)
+                .then(function(result) {
+                    assert_true(equalBuffers(result, vector.result), "Should return expected result");
+                }, function(err) {
+                    assert_unreached("encrypt error for test " + vector.name + ": " + err.message);
+                });
+            }, vector.name);
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importKey failed for " + vector.name);
+            }, "importKey step: " + vector.name);
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for successful encryption even if the buffer is changed after calling encrypt.
+    passingVectors.forEach(function(vector) {
+        var plaintext = copyBuffer(vector.plaintext);
+        var promise = importVectorKey(vector, ["encrypt", "decrypt"])
+        .then(function(vector) {
+            promise_test(function(test) {
+                var operation = subtle.encrypt(vector.algorithm, vector.key, plaintext)
+                .then(function(result) {
+                    assert_true(equalBuffers(result, vector.result), "Should return expected result");
+                }, function(err) {
+                    assert_unreached("encrypt error for test " + vector.name + ": " + err.message);
+                });
+                plaintext[0] = 255 - plaintext[0];
+                return operation;
+            }, vector.name + " with altered plaintext");
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importKey failed for " + vector.name);
+            }, "importKey step: " + vector.name + " with altered plaintext");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for successful decryption.
+    passingVectors.forEach(function(vector) {
+        var promise = importVectorKey(vector, ["encrypt", "decrypt"])
+        .then(function(vector) {
+            promise_test(function(test) {
+                return subtle.decrypt(vector.algorithm, vector.key, vector.result)
+                .then(function(result) {
+                    assert_true(equalBuffers(result, vector.plaintext), "Should return expected result");
+                }, function(err) {
+                    assert_unreached("decrypt error for test " + vector.name + ": " + err.message);
+                });
+            }, vector.name + " decryption");
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importKey failed for " + vector.name);
+            }, "importKey step for decryption: " + vector.name);
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for successful decryption even if ciphertext is altered.
+    passingVectors.forEach(function(vector) {
+        var ciphertext = copyBuffer(vector.result);
+        var promise = importVectorKey(vector, ["encrypt", "decrypt"])
+        .then(function(vector) {
+            promise_test(function(test) {
+                var operation = subtle.decrypt(vector.algorithm, vector.key, ciphertext)
+                .then(function(result) {
+                    assert_true(equalBuffers(result, vector.plaintext), "Should return expected result");
+                }, function(err) {
+                    assert_unreached("decrypt error for test " + vector.name + ": " + err.message);
+                });
+                ciphertext[0] = 255 - ciphertext[0];
+                return operation;
+            }, vector.name + " decryption with altered ciphertext");
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importKey failed for " + vector.name);
+            }, "importKey step for decryption: " + vector.name + " with altered ciphertext");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Everything that succeeded should fail if no "encrypt" usage.
+    passingVectors.forEach(function(vector) {
+        // Don't want to overwrite key being used for success tests!
+        var badVector = Object.assign({}, vector);
+        badVector.key = null;
+
+        var promise = importVectorKey(badVector, ["decrypt"])
+        .then(function(vector) {
+            promise_test(function(test) {
+                return subtle.encrypt(vector.algorithm, vector.key, vector.plaintext)
+                .then(function(result) {
+                    assert_unreached("should have thrown exception for test " + vector.name);
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw an InvalidAccessError instead of " + err.message)
+                });
+            }, vector.name + " without encrypt usage");
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importKey failed for " + vector.name);
+            }, "importKey step: " + vector.name + " without encrypt usage");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Encryption should fail if algorithm of key doesn't match algorithm of function call.
+    passingVectors.forEach(function(vector) {
+        var algorithm = Object.assign({}, vector.algorithm);
+        if (algorithm.name === "AES-CBC") {
+            algorithm.name = "AES-CTR";
+            algorithm.counter = new Uint8Array(16);
+            algorithm.length = 64;
+        } else {
+            algorithm.name = "AES-CBC";
+            algorithm.iv = new Uint8Array(16); // Need syntactically valid parameter to get to error being checked.
+        }
+
+        var promise = importVectorKey(vector, ["encrypt", "decrypt"])
+        .then(function(vector) {
+            promise_test(function(test) {
+                return subtle.encrypt(algorithm, vector.key, vector.plaintext)
+                .then(function(result) {
+                    assert_unreached("encrypt succeeded despite mismatch " + vector.name + ": " + err.message);
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Mismatch should cause InvalidAccessError instead of " + err.message);
+                });
+            }, vector.name + " with mismatched key and algorithm");
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importKey failed for " + vector.name);
+            }, "importKey step: " + vector.name + " with mismatched key and algorithm");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Everything that succeeded decrypting should fail if no "decrypt" usage.
+    passingVectors.forEach(function(vector) {
+        // Don't want to overwrite key being used for success tests!
+        var badVector = Object.assign({}, vector);
+        badVector.key = null;
+
+        var promise = importVectorKey(badVector, ["encrypt"])
+        .then(function(vector) {
+            promise_test(function(test) {
+                return subtle.decrypt(vector.algorithm, vector.key, vector.result)
+                .then(function(result) {
+                    assert_unreached("should have thrown exception for test " + vector.name);
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw an InvalidAccessError instead of " + err.message)
+                });
+            }, vector.name + " without decrypt usage");
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importKey failed for " + vector.name);
+            }, "importKey step: " + vector.name + " without decrypt usage");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for OperationError due to data lengths.
+    failingVectors.forEach(function(vector) {
+        var promise = importVectorKey(vector, ["encrypt", "decrypt"])
+        .then(function(vector) {
+            promise_test(function(test) {
+                return subtle.encrypt(vector.algorithm, vector.key, vector.plaintext)
+                .then(function(result) {
+                    assert_unreached("should have thrown exception for test " + vector.name);
+                }, function(err) {
+                    assert_equals(err.name, "OperationError", "Should throw an OperationError instead of " + err.message)
+                });
+            }, vector.name);
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importKey failed for " + vector.name);
+            }, "importKey step: " + vector.name);
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for OperationError due to data lengths for decryption, too.
+    failingVectors.forEach(function(vector) {
+        var promise = importVectorKey(vector, ["encrypt", "decrypt"])
+        .then(function(vector) {
+            promise_test(function(test) {
+                return subtle.decrypt(vector.algorithm, vector.key, vector.result)
+                .then(function(result) {
+                    assert_unreached("should have thrown exception for test " + vector.name);
+                }, function(err) {
+                    assert_equals(err.name, "OperationError", "Should throw an OperationError instead of " + err.message)
+                });
+            }, vector.name + " decryption");
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importKey failed for " + vector.name);
+            }, "importKey step: decryption " + vector.name);
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for decryption failing for algorithm-specific reasons (such as bad
+    // padding for AES-CBC).
+    decryptionFailingVectors.forEach(function(vector) {
+        var promise = importVectorKey(vector, ["encrypt", "decrypt"])
+        .then(function(vector) {
+            promise_test(function(test) {
+                return subtle.decrypt(vector.algorithm, vector.key, vector.result)
+                .then(function(result) {
+                    assert_unreached("should have thrown exception for test " + vector.name);
+                }, function(err) {
+                    assert_equals(err.name, "OperationError", "Should throw an OperationError instead of " + err.message)
+                });
+            }, vector.name);
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importKey failed for " + vector.name);
+            }, "importKey step: decryption " + vector.name);
+        });
+
+        all_promises.push(promise);
+    });
+
+    Promise.all(all_promises)
+    .then(function() {done();})
+    .catch(function() {done();})
+
+    // A test vector has all needed fields for encryption, EXCEPT that the
+    // key field may be null. This function replaces that null with the Correct
+    // CryptoKey object.
+    //
+    // Returns a Promise that yields an updated vector on success.
+    function importVectorKey(vector, usages) {
+        if (vector.key !== null) {
+            return new Promise(function(resolve, reject) {
+                resolve(vector);
+            });
+        } else {
+            return subtle.importKey("raw", vector.keyBuffer, {name: vector.algorithm.name}, false, usages)
+            .then(function(key) {
+                vector.key = key;
+                return vector;
+            });
+        }
+    }
+
+    // Returns a copy of the sourceBuffer it is sent.
+    function copyBuffer(sourceBuffer) {
+        var source = new Uint8Array(sourceBuffer);
+        var copy = new Uint8Array(sourceBuffer.byteLength)
+
+        for (var i=0; i<source.byteLength; i++) {
+            copy[i] = source[i];
+        }
+
+        return copy;
+    }
+
+    function equalBuffers(a, b) {
+        if (a.byteLength !== b.byteLength) {
+            return false;
+        }
+
+        var aBytes = new Uint8Array(a);
+        var bBytes = new Uint8Array(b);
+
+        for (var i=0; i<a.byteLength; i++) {
+            if (aBytes[i] !== bBytes[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    return;
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_cbc.https.worker.js b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_cbc.https.worker.js
new file mode 100644
index 0000000..c0c0395
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_cbc.https.worker.js
@@ -0,0 +1,5 @@
+importScripts("/resources/testharness.js");
+importScripts("aes_cbc_vectors.js");
+importScripts("aes.js");
+
+run_test();
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_cbc_vectors.js b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_cbc_vectors.js
new file mode 100644
index 0000000..96445a9
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_cbc_vectors.js
@@ -0,0 +1,261 @@
+
+// aes_cbc_vectors.js
+
+// The following function returns an array of test vectors
+// for the subtleCrypto encrypt method.
+//
+// Each test vector has the following fields:
+//     name - a unique name for this vector
+//     keyBuffer - an arrayBuffer with the key data in raw form
+//     key - a CryptoKey object for the keyBuffer. INITIALLY null! You must fill this in first to use it!
+//     algorithm - the value of the AlgorithmIdentifier parameter to provide to encrypt
+//     plaintext - the text to encrypt
+//     result - the expected result (usually just ciphertext, sometimes with added authentication)
+function getTestVectors() {
+    // Before we can really start, we need to fill a bunch of buffers with data
+    var plaintext = new Uint8Array([84, 104, 105, 115, 32, 115,
+        112, 101, 99, 105, 102, 105, 99, 97, 116, 105, 111, 110,
+        32, 100, 101, 115, 99, 114, 105, 98, 101, 115, 32, 97, 32,
+        74, 97, 118, 97, 83, 99, 114, 105, 112, 116, 32, 65, 80,
+        73, 32, 102, 111, 114, 32, 112, 101, 114, 102, 111, 114,
+        109, 105, 110, 103, 32, 98, 97, 115, 105, 99, 32, 99, 114,
+        121, 112, 116, 111, 103, 114, 97, 112, 104, 105, 99, 32,
+        111, 112, 101, 114, 97, 116, 105, 111, 110, 115, 32, 105,
+        110, 32, 119, 101, 98, 32, 97, 112, 112, 108, 105, 99, 97,
+        116, 105, 111, 110, 115, 44, 32, 115, 117, 99, 104, 32, 97,
+        115, 32, 104, 97, 115, 104, 105, 110, 103, 44, 32, 115,
+        105, 103, 110, 97, 116, 117, 114, 101, 32, 103, 101, 110,
+        101, 114, 97, 116, 105, 111, 110, 32, 97, 110, 100, 32,
+        118, 101, 114, 105, 102, 105, 99, 97, 116, 105, 111, 110,
+        44, 32, 97, 110, 100, 32, 101, 110, 99, 114, 121, 112,
+        116, 105, 111, 110, 32, 97, 110, 100, 32, 100, 101, 99,
+        114, 121, 112, 116, 105, 111, 110, 46, 32, 65, 100, 100,
+        105, 116, 105, 111, 110, 97, 108, 108, 121, 44, 32, 105,
+        116, 32, 100, 101, 115, 99, 114, 105, 98, 101, 115, 32, 97,
+        110, 32, 65, 80, 73, 32, 102, 111, 114, 32, 97, 112, 112,
+        108, 105, 99, 97, 116, 105, 111, 110, 115, 32, 116, 111,
+        32, 103, 101, 110, 101, 114, 97, 116, 101, 32, 97, 110,
+        100, 47, 111, 114, 32, 109, 97, 110, 97, 103, 101, 32, 116,
+        104, 101, 32, 107, 101, 121, 105, 110, 103, 32, 109, 97,
+        116, 101, 114, 105, 97, 108, 32, 110, 101, 99, 101, 115,
+        115, 97, 114, 121, 32, 116, 111, 32, 112, 101, 114, 102,
+        111, 114, 109, 32, 116, 104, 101, 115, 101, 32, 111, 112,
+        101, 114, 97, 116, 105, 111, 110, 115, 46, 32, 85, 115,
+        101, 115, 32, 102, 111, 114, 32, 116, 104, 105, 115, 32,
+        65, 80, 73, 32, 114, 97, 110, 103, 101, 32, 102, 114, 111,
+        109, 32, 117, 115, 101, 114, 32, 111, 114, 32, 115, 101,
+        114, 118, 105, 99, 101, 32, 97, 117, 116, 104, 101, 110,
+        116, 105, 99, 97, 116, 105, 111, 110, 44, 32, 100, 111,
+        99, 117, 109, 101, 110, 116, 32, 111, 114, 32, 99, 111,
+        100, 101, 32, 115, 105, 103, 110, 105, 110, 103, 44, 32,
+        97, 110, 100, 32, 116, 104, 101, 32, 99, 111, 110, 102,
+        105, 100, 101, 110, 116, 105, 97, 108, 105, 116, 121, 32,
+        97, 110, 100, 32, 105, 110, 116, 101, 103, 114, 105, 116,
+        121, 32, 111, 102, 32, 99, 111, 109, 109, 117, 110, 105,
+        99, 97, 116, 105, 111, 110, 115, 46]);
+
+    // We want some random key bytes of various sizes.
+    // These were randomly generated from a script.
+    var keyBytes = {
+        128: new Uint8Array([222, 192, 212, 252, 191, 60, 71,
+            65, 200, 146, 218, 189, 28, 212, 192, 78]),
+        192: new Uint8Array([208, 238, 131, 65, 63, 68, 196, 63, 186, 208,
+            61, 207, 166, 18, 99, 152, 29, 109, 221, 95, 240, 30, 28, 246]),
+        256: new Uint8Array([103, 105, 56, 35, 251, 29, 88, 7, 63, 145, 236,
+            233, 204, 58, 249, 16, 229, 83, 38, 22, 164, 210, 123, 19, 235, 123, 116,
+            216, 0, 11, 191, 48])
+    }
+
+    // AES-CBC needs a 16 byte (128 bit) IV.
+    var iv = new Uint8Array([85, 170, 248, 155, 168, 148, 19, 213, 78, 167, 39,
+        167, 108, 39, 162, 132]);
+
+
+    // Results. These were created using the Python cryptography module.
+
+    // AES-CBC produces ciphertext
+    var ciphertext = {
+        128: new Uint8Array([35, 127, 3, 254, 231, 8, 114, 231, 143, 174, 193,
+            72, 221, 189, 1, 189, 119, 203, 150, 227, 56, 30, 244, 236, 226, 175,
+            234, 23, 167, 175, 211, 124, 203, 228, 97, 223, 156, 77, 88, 174,
+            166, 187, 186, 225, 176, 92, 250, 177, 225, 41, 135, 124, 215, 86,
+            198, 134, 124, 49, 154, 60, 224, 93, 165, 12, 190, 245, 241, 164,
+            247, 220, 227, 69, 242, 105, 208, 108, 222, 193, 223, 0, 226, 217,
+            39, 160, 78, 147, 191, 38, 153, 232, 206, 221, 254, 25, 185, 249, 7,
+            181, 215, 104, 98, 163, 194, 161, 103, 161, 237, 167, 10, 242, 37,
+            80, 2, 255, 173, 96, 20, 106, 170, 110, 80, 38, 136, 127, 16, 85,
+            244, 78, 172, 56, 106, 3, 115, 130, 58, 186, 129, 236, 255, 251,
+            178, 112, 24, 159, 82, 252, 1, 178, 132, 92, 40, 125, 18, 135, 116,
+            64, 178, 31, 174, 87, 114, 114, 218, 78, 111, 0, 239, 252, 79, 63,
+            119, 58, 118, 78, 55, 249, 36, 130, 225, 205, 13, 76, 97, 214, 250,
+            174, 232, 67, 103, 211, 178, 206, 32, 129, 188, 243, 100, 71, 63,
+            154, 159, 200, 125, 34, 138, 39, 73, 130, 75, 97, 203, 204, 111,
+            244, 75, 186, 181, 43, 207, 175, 146, 98, 207, 27, 23, 90, 144, 161,
+            19, 235, 199, 93, 98, 238, 72, 134, 157, 220, 207, 66, 167, 236, 94,
+            57, 0, 3, 202, 250, 55, 26, 163, 20, 133, 191, 67, 20, 63, 150, 203,
+            87, 216, 44, 57, 188, 236, 64, 80, 111, 68, 26, 12, 10, 163, 82, 3,
+            191, 19, 71, 186, 196, 177, 84, 244, 7, 78, 41, 172, 203, 27, 225,
+            231, 108, 206, 141, 221, 253, 204, 220, 134, 20, 130, 54, 113, 81,
+            127, 197, 27, 101, 121, 159, 223, 193, 115, 190, 12, 153, 174, 231,
+            196, 92, 142, 156, 61, 189, 3, 18, 153, 206, 190, 58, 255, 154, 115,
+            66, 23, 107, 94, 220, 156, 220, 228, 241, 66, 6, 184, 44, 238, 249,
+            51, 240, 109, 142, 208, 189, 11, 117, 70, 170, 217, 170, 216, 66,
+            231, 18, 175, 121, 221, 16, 29, 139, 55, 103, 91, 239, 111, 29, 108,
+            94, 179, 138, 134, 73, 130, 29, 69, 182, 192, 249, 150, 165, 79, 47,
+            91, 203, 226, 63, 87, 52, 60, 172, 191, 190, 179, 171, 155, 205, 88,
+            172, 111, 59, 40, 198, 250, 209, 148, 177, 115, 200, 40, 43, 165,
+            167, 67, 116, 64, 159, 240, 81, 253, 235, 137, 132, 49, 223, 214,
+            172, 53, 7, 47, 184, 223, 120, 59, 51, 33, 124, 147, 221, 27, 60,
+            16, 254, 24, 115, 115, 214, 75, 73, 97, 136, 214, 209, 177, 106, 71,
+            254, 211, 94, 57, 104, 170, 168, 35, 37, 93, 203, 199, 38, 28, 84]),
+
+        192: new Uint8Array([131, 160, 2, 14, 214, 229, 41, 230, 47, 99, 83,
+            193, 62, 133, 172, 195, 127, 61, 247, 80, 71, 167, 37, 184, 230,
+            207, 168, 163, 139, 145, 18, 225, 205, 134, 87, 138, 80, 247, 166,
+            176, 177, 18, 71, 88, 193, 56, 45, 96, 36, 78, 134, 212, 9, 250, 217,
+            24, 207, 215, 111, 72, 114, 203, 27, 188, 122, 34, 212, 191, 88, 72,
+            22, 194, 224, 217, 236, 201, 191, 236, 214, 231, 90, 244, 100, 153,
+            211, 35, 182, 205, 128, 84, 79, 161, 53, 166, 236, 196, 181, 163,
+            140, 255, 80, 59, 49, 71, 170, 118, 14, 100, 40, 105, 184, 187, 41,
+            198, 180, 135, 69, 211, 69, 74, 132, 243, 76, 144, 102, 90, 155,
+            243, 125, 140, 190, 20, 9, 232, 188, 198, 221, 148, 13, 53, 155, 91,
+            34, 235, 24, 121, 109, 48, 242, 142, 8, 160, 223, 242, 163, 98, 198,
+            131, 164, 160, 79, 27, 210, 216, 192, 228, 27, 4, 254, 222, 195, 14,
+            77, 72, 225, 151, 114, 38, 130, 143, 6, 17, 138, 229, 193, 114, 169,
+            2, 108, 225, 35, 37, 232, 200, 167, 147, 251, 210, 138, 243, 44, 48,
+            12, 84, 192, 169, 108, 0, 113, 77, 160, 218, 96, 4, 138, 171, 207,
+            20, 189, 146, 255, 206, 68, 160, 87, 127, 3, 83, 182, 203, 116, 59,
+            24, 186, 79, 68, 220, 161, 85, 227, 29, 118, 134, 128, 187, 29, 128,
+            121, 120, 64, 211, 30, 255, 52, 187, 185, 216, 151, 30, 10, 165,
+            203, 148, 39, 224, 14, 173, 199, 57, 0, 194, 79, 115, 206, 159, 43,
+            13, 36, 169, 97, 144, 32, 0, 207, 230, 16, 162, 156, 166, 34, 150,
+            12, 93, 141, 164, 181, 194, 10, 47, 139, 82, 75, 42, 23, 224, 3, 92,
+            151, 154, 249, 170, 57, 141, 113, 32, 52, 158, 218, 49, 242, 134,
+            65, 69, 203, 71, 19, 133, 125, 117, 1, 207, 210, 224, 130, 45, 37,
+            42, 181, 139, 34, 85, 8, 67, 165, 249, 180, 89, 3, 60, 152, 1, 231,
+            49, 1, 124, 243, 81, 44, 72, 232, 239, 129, 75, 108, 4, 169, 132,
+            73, 183, 21, 29, 46, 94, 138, 83, 190, 131, 146, 65, 104, 107, 251,
+            218, 95, 227, 94, 145, 70, 0, 2, 252, 59, 188, 58, 150, 203, 148,
+            100, 219, 36, 182, 81, 237, 138, 160, 83, 151, 119, 11, 216, 122,
+            134, 189, 246, 251, 192, 41, 158, 125, 247, 190, 32, 173, 104, 9,
+            58, 223, 97, 212, 48, 62, 3, 112, 21, 74, 206, 87, 182, 110, 197,
+            67, 68, 155, 189, 223, 136, 2, 239, 137, 151, 138, 252, 162, 141,
+            255, 209, 25, 4, 146, 24, 221, 43, 148, 120, 26, 228, 208, 200, 198,
+            192, 4, 96, 70, 227, 237, 104, 17, 67, 9, 211]),
+
+        256: new Uint8Array([41, 213, 121, 140, 181, 227, 200, 97, 100, 133, 58,
+            227, 106, 115, 25, 63, 77, 51, 26, 57, 238, 140, 99, 63, 71, 211,
+            128, 84, 115, 26, 236, 52, 103, 81, 145, 14, 101, 161, 181, 58, 135,
+            193, 56, 167, 214, 220, 5, 52, 85, 222, 183, 27, 101, 134, 86, 155,
+            64, 148, 124, 212, 219, 251, 65, 42, 32, 44, 128, 2, 50, 128, 221,
+            22, 238, 56, 189, 83, 28, 122, 121, 157, 215, 135, 151, 128, 233,
+            193, 65, 190, 86, 148, 191, 140, 196, 120, 8, 172, 100, 166, 254,
+            41, 245, 75, 56, 6, 166, 244, 178, 111, 234, 23, 4, 107, 6, 22, 132,
+            187, 230, 17, 71, 172, 113, 238, 73, 4, 180, 90, 103, 77, 37, 51,
+            118, 112, 129, 238, 199, 7, 222, 122, 173, 30, 232, 178, 233, 234,
+            144, 98, 14, 234, 112, 77, 68, 62, 62, 159, 230, 101, 98, 43, 2,
+            204, 69, 156, 86, 104, 128, 34, 128, 7, 173, 90, 120, 33, 104, 59,
+            45, 251, 93, 51, 240, 232, 60, 94, 189, 134, 90, 20, 184, 122, 29,
+            225, 85, 213, 38, 116, 159, 80, 69, 106, 168, 236, 201, 69, 140, 98,
+            240, 45, 160, 133, 225, 106, 45, 245, 212, 160, 176, 128, 27, 114,
+            153, 182, 144, 145, 214, 72, 196, 138, 183, 87, 61, 245, 150, 56,
+            82, 158, 224, 50, 114, 125, 122, 172, 161, 129, 234, 70, 63, 245,
+            136, 30, 136, 9, 128, 220, 229, 157, 222, 195, 149, 189, 70, 8, 71,
+            40, 195, 93, 27, 7, 234, 164, 175, 102, 201, 149, 115, 248, 179,
+            125, 66, 122, 194, 26, 61, 218, 198, 181, 152, 140, 199, 48, 148,
+            31, 14, 241, 197, 3, 70, 128, 239, 32, 86, 15, 215, 86, 245, 190,
+            95, 141, 41, 111, 0, 232, 28, 152, 67, 87, 197, 255, 118, 13, 251,
+            71, 84, 22, 231, 134, 188, 175, 115, 138, 37, 199, 5, 238, 199, 2,
+            99, 203, 75, 62, 231, 21, 150, 239, 94, 201, 185, 219, 58, 210, 228,
+            151, 131, 76, 148, 104, 60, 74, 82, 6, 168, 49, 251, 182, 3, 232,
+            173, 210, 201, 19, 101, 166, 7, 94, 11, 194, 211, 146, 229, 75, 241,
+            15, 50, 187, 36, 175, 78, 227, 98, 224, 3, 95, 209, 93, 126, 112,
+            178, 29, 18, 108, 241, 232, 79, 210, 41, 2, 238, 208, 190, 171, 134,
+            147, 188, 191, 229, 122, 32, 209, 166, 118, 129, 223, 130, 214, 195,
+            89, 67, 94, 218, 155, 185, 0, 144, 255, 132, 213, 25, 59, 83, 242,
+            57, 69, 148, 109, 133, 61, 163, 30, 214, 254, 54, 169, 3, 217, 77,
+            66, 123, 193, 204, 199, 109, 123, 49, 186, 223, 229, 8, 230, 164,
+            171, 196, 145, 225, 10, 111, 248, 111, 164, 216, 54, 225, 253])
+    };
+
+    // Replace the last block of each ciphertext with bad padding below for decryption errors
+    var badPadding = {
+        128: {
+            "zeroPadChar": new Uint8Array([238, 27, 248, 169, 218, 138, 164, 86, 207, 102, 36, 223, 6, 166, 77, 14]),
+            "bigPadChar": new Uint8Array([91, 67, 119, 104, 252, 238, 175, 144, 17, 75, 12, 163, 212, 52, 46, 51]),
+            "inconsistentPadChars": new Uint8Array([135, 101, 112, 208, 3, 106, 226, 20, 25, 219, 79, 94, 58, 212, 242, 192])
+        },
+        192: {
+            "zeroPadChar": new Uint8Array([22, 158, 50, 15, 168, 47, 19, 194, 182, 133, 184, 65, 36, 43, 177, 254]),
+            "bigPadChar": new Uint8Array([207, 110, 28, 160, 165, 213, 48, 213, 163, 242, 15, 78, 96, 117, 106, 87]),
+            "inconsistentPadChars": new Uint8Array([143, 227, 12, 112, 216, 207, 136, 167, 78, 137, 93, 30, 50, 75, 102, 101])
+        },
+        256: {
+            "zeroPadChar": new Uint8Array([1, 253, 141, 214, 30, 193, 254, 68, 140, 200, 157, 110, 200, 89, 177, 129]),
+            "bigPadChar": new Uint8Array([88, 7, 110, 221, 74, 34, 97, 109, 99, 25, 189, 222, 94, 90, 27, 60]),
+            "inconsistentPadChars": new Uint8Array([152, 54, 60, 148, 59, 136, 193, 21, 77, 140, 170, 67, 120, 74, 106, 62])
+        }
+    };
+
+    var keyLengths = [128, 192, 256];
+
+    // All the scenarios that should succeed, if the key has "encrypt" usage
+    var passing = [];
+    keyLengths.forEach(function(keyLength) {
+        passing.push({
+            name: "AES-CBC " + keyLength.toString() + "-bit key",
+            keyBuffer: keyBytes[keyLength],
+            key: null,
+            algorithm: {name: "AES-CBC", iv: iv},
+            plaintext: plaintext,
+            result: ciphertext[keyLength]
+        });
+    });
+
+    // Scenarios that should fail because of a bad iv length, causing an OperationError
+    var failing = [];
+    keyLengths.forEach(function(keyLength) {
+        var shortIv = iv.slice(0, 8);
+        failing.push({
+            name: "AES-CBC " + keyLength.toString() + "-bit key, 64-bit IV",
+            keyBuffer: keyBytes[keyLength],
+            key: null,
+            algorithm: {name: "AES-CBC", iv: shortIv},
+            plaintext: plaintext,
+            result: ciphertext[keyLength]
+        });
+
+        var longIv = new Uint8Array(24);
+        longIv.set(iv, 0);
+        longIv.set(iv.slice(0, 8), 16);
+        failing.push({
+            name: "AES-CBC " + keyLength.toString() + "-bit key, 192-bit IV",
+            keyBuffer: keyBytes[keyLength],
+            key: null,
+            algorithm: {name: "AES-CBC", iv: longIv},
+            plaintext: plaintext,
+            result: ciphertext[keyLength]
+        });
+    });
+
+    // Scenarios that should fail decryption because of bad padding
+    var decryptionFailing = [];
+    keyLengths.forEach(function(keyLength) {
+        ["zeroPadChar", "bigPadChar", "inconsistentPadChars"].forEach(function(paddingProblem) {
+            var badCiphertext = new Uint8Array(ciphertext[keyLength].byteLength);
+            badCiphertext.set(ciphertext[keyLength].slice(0, ciphertext[keyLength].byteLength - 16));
+            badCiphertext.set(badPadding[keyLength][paddingProblem]);
+
+            decryptionFailing.push({
+                name: "AES-CBC " + keyLength.toString() + "-bit key, " + paddingProblem,
+                keyBuffer: keyBytes[keyLength],
+                key: null,
+                algorithm: {name: "AES-CBC", iv: iv},
+                plaintext: plaintext,
+                result: badCiphertext
+            });
+        });
+    });
+
+    return {passing: passing, failing: failing, decryptionFailing: decryptionFailing};
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_ctr.https.worker.js b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_ctr.https.worker.js
new file mode 100644
index 0000000..6577525
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_ctr.https.worker.js
@@ -0,0 +1,5 @@
+importScripts("/resources/testharness.js");
+importScripts("aes_ctr_vectors.js");
+importScripts("aes.js");
+
+run_test();
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_ctr_vectors.js b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_ctr_vectors.js
new file mode 100644
index 0000000..201dff8
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_ctr_vectors.js
@@ -0,0 +1,123 @@
+
+// aes_ctr_vectors.js
+
+// The following function returns an array of test vectors
+// for the subtleCrypto encrypt method.
+//
+// Each test vector has the following fields:
+//     name - a unique name for this vector
+//     keyBuffer - an arrayBuffer with the key data in raw form
+//     key - a CryptoKey object for the keyBuffer. INITIALLY null! You must fill this in first to use it!
+//     algorithm - the value of the AlgorithmIdentifier parameter to provide to encrypt
+//     plaintext - the text to encrypt
+//     result - the expected result (usually just ciphertext, sometimes with added authentication)
+function getTestVectors() {
+    // Before we can really start, we need to fill a bunch of buffers with data
+    var plaintext = new Uint8Array([84, 104, 105, 115, 32, 115,
+        112, 101, 99, 105, 102, 105, 99, 97, 116, 105, 111, 110,
+        32, 100, 101, 115, 99, 114, 105, 98, 101, 115, 32, 97, 32,
+        74, 97, 118, 97, 83, 99, 114, 105, 112, 116, 32, 65, 80,
+        73, 32, 102, 111, 114, 32, 112, 101, 114, 102, 111, 114,
+        109, 105, 110, 103, 32, 98, 97, 115, 105, 99, 32, 99, 114,
+        121, 112, 116, 111, 103, 114, 97, 112, 104, 105, 99, 32,
+        111, 112, 101, 114, 97, 116, 105, 111, 110, 115, 32, 105,
+        110, 32, 119, 101, 98, 32, 97, 112, 112, 108, 105, 99, 97,
+        116, 105, 111, 110, 115, 44, 32, 115, 117, 99, 104, 32, 97,
+        115, 32, 104, 97, 115, 104, 105, 110, 103, 44, 32, 115,
+        105, 103, 110, 97, 116, 117, 114, 101, 32, 103, 101, 110,
+        101, 114, 97, 116, 105, 111, 110, 32, 97, 110, 100, 32,
+        118, 101, 114, 105, 102, 105, 99, 97, 116, 105, 111, 110,
+        44, 32, 97, 110, 100, 32, 101, 110, 99, 114, 121, 112,
+        116, 105, 111, 110, 32, 97, 110, 100, 32, 100, 101, 99,
+        114, 121, 112, 116, 105, 111, 110, 46, 32, 65, 100, 100,
+        105, 116, 105, 111, 110, 97, 108, 108, 121, 44, 32, 105,
+        116, 32, 100, 101, 115, 99, 114, 105, 98, 101, 115, 32, 97,
+        110, 32, 65, 80, 73, 32, 102, 111, 114, 32, 97, 112, 112,
+        108, 105, 99, 97, 116, 105, 111, 110, 115, 32, 116, 111,
+        32, 103, 101, 110, 101, 114, 97, 116, 101, 32, 97, 110,
+        100, 47, 111, 114, 32, 109, 97, 110, 97, 103, 101, 32, 116,
+        104, 101, 32, 107, 101, 121, 105, 110, 103, 32, 109, 97,
+        116, 101, 114, 105, 97, 108, 32, 110, 101, 99, 101, 115,
+        115, 97, 114, 121, 32, 116, 111, 32, 112, 101, 114, 102,
+        111, 114, 109, 32, 116, 104, 101, 115, 101, 32, 111, 112,
+        101, 114, 97, 116, 105, 111, 110, 115, 46, 32, 85, 115,
+        101, 115, 32, 102, 111, 114, 32, 116, 104, 105, 115, 32,
+        65, 80, 73, 32, 114, 97, 110, 103, 101, 32, 102, 114, 111,
+        109, 32, 117, 115, 101, 114, 32, 111, 114, 32, 115, 101,
+        114, 118, 105, 99, 101, 32, 97, 117, 116, 104, 101, 110,
+        116, 105, 99, 97, 116, 105, 111, 110, 44, 32, 100, 111,
+        99, 117, 109, 101, 110, 116, 32, 111, 114, 32, 99, 111,
+        100, 101, 32, 115, 105, 103, 110, 105, 110, 103, 44, 32,
+        97, 110, 100, 32, 116, 104, 101, 32, 99, 111, 110, 102,
+        105, 100, 101, 110, 116, 105, 97, 108, 105, 116, 121, 32,
+        97, 110, 100, 32, 105, 110, 116, 101, 103, 114, 105, 116,
+        121, 32, 111, 102, 32, 99, 111, 109, 109, 117, 110, 105,
+        99, 97, 116, 105, 111, 110, 115, 46]);
+
+    // We want some random key bytes of various sizes.
+    // These were randomly generated from a script.
+    var keyBytes = {
+        128: new Uint8Array([222, 192, 212, 252, 191, 60, 71,
+            65, 200, 146, 218, 189, 28, 212, 192, 78]),
+        192: new Uint8Array([208, 238, 131, 65, 63, 68, 196, 63, 186, 208,
+            61, 207, 166, 18, 99, 152, 29, 109, 221, 95, 240, 30, 28, 246]),
+        256: new Uint8Array([103, 105, 56, 35, 251, 29, 88, 7, 63, 145, 236,
+            233, 204, 58, 249, 16, 229, 83, 38, 22, 164, 210, 123, 19, 235, 123, 116,
+            216, 0, 11, 191, 48])
+    }
+
+    // AES-CTR needs a 16 byte (128 bit) counter.
+    var counter = new Uint8Array([85, 170, 248, 155, 168, 148, 19, 213, 78, 167, 39,
+        167, 108, 39, 162, 132]);
+
+
+    // Results. These were created using the Python cryptography module.
+
+    // AES-CTR produces ciphertext
+    var ciphertext = {
+        128: new Uint8Array([233, 17, 117, 253, 164, 245, 234, 87, 197, 43, 13, 0, 11, 190, 152, 175, 104, 192, 165, 144, 88, 174, 237, 138, 181, 183, 6, 53, 3, 161, 206, 71, 13, 121, 218, 209, 116, 249, 10, 170, 250, 165, 68, 157, 132, 141, 200, 178, 197, 87, 209, 231, 250, 75, 154, 65, 162, 251, 30, 159, 234, 20, 20, 181, 147, 218, 180, 12, 4, 241, 75, 79, 129, 64, 15, 228, 60, 147, 153, 1, 129, 176, 150, 161, 85, 97, 22, 154, 234, 23, 127, 16, 4, 22, 226, 11, 104, 16, 176, 14, 225, 176, 79, 239, 103, 243, 190, 222, 40, 186, 244, 212, 29, 57, 125, 175, 21, 17, 233, 2, 13, 119, 102, 233, 230, 4, 16, 222, 56, 225, 67, 45, 191, 250, 15, 153, 45, 193, 240, 212, 117, 101, 68, 232, 199, 101, 175, 125, 247, 6, 249, 14, 0, 157, 185, 56, 76, 51, 228, 77, 234, 84, 60, 42, 119, 187, 213, 32, 34, 222, 65, 231, 215, 26, 73, 141, 231, 254, 185, 118, 14, 180, 126, 80, 51, 102, 200, 141, 204, 45, 26, 56, 119, 136, 222, 45, 143, 120, 231, 44, 43, 221, 136, 21, 188, 138, 84, 232, 208, 238, 226, 117, 104, 60, 165, 4, 18, 144, 240, 49, 173, 90, 68, 84, 239, 161, 124, 196, 144, 119, 24, 243, 239, 75, 117, 254, 219, 209, 53, 131, 37, 79, 68, 26, 21, 168, 163, 50, 59, 18, 244, 11, 143, 190, 188, 129, 108, 249, 180, 104, 216, 215, 165, 160, 251, 84, 132, 152, 195, 154, 110, 216, 70, 21, 248, 148, 146, 152, 56, 174, 248, 227, 1, 102, 15, 118, 182, 50, 73, 63, 35, 112, 159, 237, 253, 94, 16, 127, 120, 38, 127, 51, 27, 96, 163, 140, 20, 111, 151, 16, 72, 74, 74, 205, 239, 241, 16, 179, 183, 116, 95, 248, 58, 168, 203, 93, 233, 225, 91, 17, 226, 10, 120, 85, 114, 4, 31, 40, 82, 161, 152, 17, 86, 237, 207, 7, 228, 110, 182, 65, 68, 68, 156, 206, 116, 185, 204, 148, 22, 58, 111, 218, 138, 225, 146, 25, 114, 29, 96, 183, 87, 181, 181, 236, 113, 141, 171, 213, 9, 84, 182, 230, 163, 147, 246, 86, 246, 52, 111, 64, 34, 157, 12, 80, 224, 28, 21, 112, 31, 42, 79, 229, 210, 90, 23, 78, 223, 155, 144, 238, 12, 14, 191, 158, 6, 181, 254, 0, 85, 134, 56, 161, 234, 55, 129, 64, 59, 12, 146, 6, 217, 232, 20, 214, 167, 159, 183, 165, 96, 96, 225, 199, 23, 106, 243, 108, 106, 26, 214, 53, 152, 26, 155, 253, 128, 7, 216, 207, 109, 159, 147, 240, 232, 226, 43, 147, 169, 162, 204, 215, 9, 10, 177, 223, 99, 206, 163, 240, 64]),
+
+        192: new Uint8Array([98, 123, 235, 65, 14, 86, 80, 133, 88, 104, 244, 125, 165, 185, 163, 4, 3, 230, 62, 58, 113, 222, 46, 210, 17, 155, 95, 19, 125, 125, 70, 234, 105, 54, 23, 246, 114, 9, 237, 191, 9, 194, 34, 254, 156, 11, 50, 216, 80, 178, 185, 221, 132, 154, 27, 85, 82, 49, 241, 123, 23, 106, 119, 134, 203, 0, 151, 66, 149, 218, 124, 247, 227, 233, 236, 184, 88, 234, 174, 250, 83, 168, 33, 15, 122, 26, 96, 213, 210, 4, 52, 92, 20, 12, 64, 12, 209, 197, 69, 100, 15, 56, 60, 63, 241, 52, 18, 189, 93, 146, 47, 60, 33, 200, 218, 243, 43, 169, 17, 108, 19, 199, 174, 33, 107, 186, 57, 95, 167, 138, 180, 187, 53, 113, 208, 148, 190, 48, 167, 53, 209, 52, 153, 184, 231, 63, 168, 54, 179, 238, 93, 130, 125, 3, 149, 119, 60, 25, 142, 150, 183, 193, 29, 18, 3, 219, 235, 219, 26, 116, 217, 196, 108, 6, 96, 103, 212, 48, 227, 91, 124, 77, 181, 169, 18, 111, 123, 83, 26, 169, 230, 88, 103, 185, 153, 93, 143, 152, 142, 231, 41, 226, 226, 156, 179, 206, 212, 67, 18, 193, 187, 53, 252, 214, 15, 228, 246, 131, 170, 101, 134, 212, 100, 170, 146, 47, 57, 125, 50, 230, 51, 246, 74, 175, 129, 196, 178, 206, 176, 52, 153, 39, 77, 24, 186, 99, 137, 83, 105, 111, 168, 35, 176, 24, 29, 170, 223, 74, 160, 138, 247, 12, 102, 233, 136, 59, 172, 228, 242, 84, 13, 34, 155, 80, 80, 87, 180, 143, 129, 61, 213, 54, 41, 8, 183, 102, 126, 179, 127, 77, 55, 176, 152, 41, 131, 85, 86, 225, 87, 216, 139, 226, 196, 195, 210, 34, 33, 161, 249, 153, 205, 197, 128, 41, 28, 121, 6, 159, 25, 211, 168, 137, 26, 217, 249, 113, 81, 141, 18, 1, 250, 228, 68, 238, 74, 54, 99, 167, 236, 176, 199, 148, 161, 143, 156, 51, 189, 204, 59, 240, 151, 170, 85, 63, 23, 38, 152, 199, 12, 81, 217, 244, 178, 231, 249, 159, 224, 107, 214, 58, 127, 116, 143, 219, 155, 80, 55, 213, 171, 80, 127, 235, 20, 247, 12, 104, 228, 147, 202, 124, 143, 110, 223, 76, 221, 154, 175, 143, 185, 237, 222, 189, 104, 218, 72, 244, 55, 253, 138, 183, 92, 231, 68, 176, 239, 171, 100, 10, 63, 61, 194, 228, 15, 133, 216, 45, 60, 135, 203, 142, 127, 153, 172, 223, 213, 230, 220, 189, 223, 234, 156, 134, 238, 220, 251, 104, 209, 117, 175, 47, 46, 148, 6, 61, 216, 215, 39, 30, 116, 212, 45, 112, 202, 227, 198, 98, 253, 97, 177, 120, 74, 238, 68, 99, 240, 96, 43, 88, 166]),
+
+        256: new Uint8Array([55, 82, 154, 67, 47, 80, 186, 78, 83, 56, 95, 130, 102, 236, 61, 236, 204, 236, 234, 222, 122, 226, 147, 149, 233, 41, 16, 118, 201, 91, 185, 162, 79, 71, 146, 252, 221, 110, 165, 137, 75, 129, 94, 219, 93, 94, 64, 34, 250, 190, 5, 90, 6, 177, 167, 224, 25, 121, 85, 91, 87, 152, 56, 100, 191, 35, 1, 156, 177, 179, 127, 253, 173, 176, 87, 247, 40, 207, 178, 175, 10, 51, 209, 70, 52, 76, 251, 160, 172, 203, 77, 191, 97, 58, 123, 238, 82, 60, 166, 214, 134, 14, 71, 74, 156, 15, 77, 6, 141, 76, 10, 205, 148, 204, 85, 203, 242, 30, 66, 133, 202, 21, 17, 108, 151, 2, 15, 44, 51, 180, 88, 80, 8, 248, 254, 151, 201, 226, 156, 6, 39, 197, 212, 124, 72, 217, 75, 232, 139, 155, 22, 199, 242, 223, 116, 10, 141, 42, 7, 85, 99, 5, 184, 43, 145, 159, 122, 135, 202, 46, 209, 157, 178, 114, 98, 194, 119, 194, 19, 242, 167, 236, 162, 94, 90, 106, 219, 234, 67, 11, 162, 225, 6, 17, 152, 23, 16, 84, 40, 90, 255, 158, 8, 105, 198, 56, 220, 213, 36, 203, 241, 242, 85, 218, 103, 90, 202, 214, 215, 134, 121, 169, 149, 139, 122, 143, 155, 178, 29, 217, 197, 128, 173, 25, 111, 154, 14, 76, 106, 101, 0, 215, 187, 33, 223, 116, 205, 89, 52, 206, 60, 77, 141, 31, 57, 211, 74, 42, 219, 88, 210, 36, 196, 128, 151, 136, 124, 222, 157, 59, 225, 70, 163, 234, 59, 173, 228, 198, 134, 76, 249, 228, 69, 181, 196, 194, 179, 239, 78, 43, 143, 94, 234, 10, 177, 192, 185, 171, 231, 164, 254, 91, 44, 11, 29, 148, 223, 107, 18, 149, 61, 50, 115, 38, 14, 128, 189, 9, 77, 236, 151, 163, 23, 122, 156, 236, 11, 80, 66, 190, 24, 4, 4, 12, 148, 57, 64, 59, 143, 114, 247, 66, 111, 167, 86, 173, 98, 102, 207, 44, 134, 89, 231, 64, 50, 157, 208, 210, 79, 159, 133, 73, 118, 98, 202, 215, 57, 247, 29, 97, 116, 1, 28, 119, 248, 243, 31, 180, 66, 38, 40, 141, 251, 134, 129, 126, 241, 113, 22, 50, 28, 113, 187, 158, 217, 125, 182, 233, 144, 246, 32, 88, 88, 15, 0, 102, 131, 67, 31, 34, 150, 98, 241, 213, 227, 205, 175, 254, 3, 53, 70, 124, 167, 38, 53, 104, 140, 147, 158, 200, 179, 45, 100, 101, 246, 81, 166, 53, 247, 60, 10, 78, 127, 10, 173, 176, 232, 31, 91, 203, 250, 236, 38, 113, 172, 151, 253, 194, 253, 50, 242, 76, 148, 23, 117, 195, 122, 104, 16, 212, 177, 113, 188, 138, 186, 144, 168, 102, 3])
+    };
+
+    var keyLengths = [128, 192, 256];
+
+    // All the scenarios that should succeed, if the key has "encrypt" usage
+    var passing = [];
+    keyLengths.forEach(function(keyLength) {
+        passing.push({
+            name: "AES-CTR " + keyLength.toString() + "-bit key",
+            keyBuffer: keyBytes[keyLength],
+            key: null,
+            algorithm: {name: "AES-CTR", counter: counter, length: 64},
+            plaintext: plaintext,
+            result: ciphertext[keyLength]
+        });
+    });
+
+    // Scenarios that should fail because of a bad length parameter, causing an OperationError
+    var failing = [];
+    keyLengths.forEach(function(keyLength) {
+        failing.push({
+            name: "AES-CTR " + keyLength.toString() + "-bit key, 0-bit counter",
+            keyBuffer: keyBytes[keyLength],
+            key: null,
+            algorithm: {name: "AES-CTR", counter: counter, length: 0},
+            plaintext: plaintext,
+            result: ciphertext[keyLength]
+        });
+
+        failing.push({
+            name: "AES-CTR " + keyLength.toString() + "-bit key, 129-bit counter",
+            keyBuffer: keyBytes[keyLength],
+            key: null,
+            algorithm: {name: "AES-CTR", counter: counter, length: 129},
+            plaintext: plaintext,
+            result: ciphertext[keyLength]
+        });
+    });
+
+    return {passing: passing, failing: failing, decryptionFailing: []};
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_gcm.https.worker.js b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_gcm.https.worker.js
new file mode 100644
index 0000000..7a14b69
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_gcm.https.worker.js
@@ -0,0 +1,5 @@
+importScripts("/resources/testharness.js");
+importScripts("aes_gcm_vectors.js");
+importScripts("aes.js");
+
+run_test();
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_gcm_vectors.js b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_gcm_vectors.js
new file mode 100644
index 0000000..72566de
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/aes_gcm_vectors.js
@@ -0,0 +1,262 @@
+
+// aes_gcm_vectors.js
+
+// The following function returns an array of test vectors
+// for the subtleCrypto encrypt method.
+//
+// Each test vector has the following fields:
+//     name - a unique name for this vector
+//     keyBuffer - an arrayBuffer with the key data in raw form
+//     key - a CryptoKey object for the keyBuffer. INITIALLY null! You must fill this in first to use it!
+//     algorithm - the value of the AlgorithmIdentifier parameter to provide to encrypt
+//     plaintext - the text to encrypt
+//     result - the expected result (usually just ciphertext, sometimes with added authentication)
+function getTestVectors() {
+    // Before we can really start, we need to fill a bunch of buffers with data
+    var plaintext = new Uint8Array([84, 104, 105, 115, 32, 115,
+        112, 101, 99, 105, 102, 105, 99, 97, 116, 105, 111, 110,
+        32, 100, 101, 115, 99, 114, 105, 98, 101, 115, 32, 97, 32,
+        74, 97, 118, 97, 83, 99, 114, 105, 112, 116, 32, 65, 80,
+        73, 32, 102, 111, 114, 32, 112, 101, 114, 102, 111, 114,
+        109, 105, 110, 103, 32, 98, 97, 115, 105, 99, 32, 99, 114,
+        121, 112, 116, 111, 103, 114, 97, 112, 104, 105, 99, 32,
+        111, 112, 101, 114, 97, 116, 105, 111, 110, 115, 32, 105,
+        110, 32, 119, 101, 98, 32, 97, 112, 112, 108, 105, 99, 97,
+        116, 105, 111, 110, 115, 44, 32, 115, 117, 99, 104, 32, 97,
+        115, 32, 104, 97, 115, 104, 105, 110, 103, 44, 32, 115,
+        105, 103, 110, 97, 116, 117, 114, 101, 32, 103, 101, 110,
+        101, 114, 97, 116, 105, 111, 110, 32, 97, 110, 100, 32,
+        118, 101, 114, 105, 102, 105, 99, 97, 116, 105, 111, 110,
+        44, 32, 97, 110, 100, 32, 101, 110, 99, 114, 121, 112,
+        116, 105, 111, 110, 32, 97, 110, 100, 32, 100, 101, 99,
+        114, 121, 112, 116, 105, 111, 110, 46, 32, 65, 100, 100,
+        105, 116, 105, 111, 110, 97, 108, 108, 121, 44, 32, 105,
+        116, 32, 100, 101, 115, 99, 114, 105, 98, 101, 115, 32, 97,
+        110, 32, 65, 80, 73, 32, 102, 111, 114, 32, 97, 112, 112,
+        108, 105, 99, 97, 116, 105, 111, 110, 115, 32, 116, 111,
+        32, 103, 101, 110, 101, 114, 97, 116, 101, 32, 97, 110,
+        100, 47, 111, 114, 32, 109, 97, 110, 97, 103, 101, 32, 116,
+        104, 101, 32, 107, 101, 121, 105, 110, 103, 32, 109, 97,
+        116, 101, 114, 105, 97, 108, 32, 110, 101, 99, 101, 115,
+        115, 97, 114, 121, 32, 116, 111, 32, 112, 101, 114, 102,
+        111, 114, 109, 32, 116, 104, 101, 115, 101, 32, 111, 112,
+        101, 114, 97, 116, 105, 111, 110, 115, 46, 32, 85, 115,
+        101, 115, 32, 102, 111, 114, 32, 116, 104, 105, 115, 32,
+        65, 80, 73, 32, 114, 97, 110, 103, 101, 32, 102, 114, 111,
+        109, 32, 117, 115, 101, 114, 32, 111, 114, 32, 115, 101,
+        114, 118, 105, 99, 101, 32, 97, 117, 116, 104, 101, 110,
+        116, 105, 99, 97, 116, 105, 111, 110, 44, 32, 100, 111,
+        99, 117, 109, 101, 110, 116, 32, 111, 114, 32, 99, 111,
+        100, 101, 32, 115, 105, 103, 110, 105, 110, 103, 44, 32,
+        97, 110, 100, 32, 116, 104, 101, 32, 99, 111, 110, 102,
+        105, 100, 101, 110, 116, 105, 97, 108, 105, 116, 121, 32,
+        97, 110, 100, 32, 105, 110, 116, 101, 103, 114, 105, 116,
+        121, 32, 111, 102, 32, 99, 111, 109, 109, 117, 110, 105,
+        99, 97, 116, 105, 111, 110, 115, 46]);
+
+    // We want some random key bytes of various sizes.
+    // These were randomly generated from a script.
+    var keyBytes = {
+        128: new Uint8Array([222, 192, 212, 252, 191, 60, 71,
+            65, 200, 146, 218, 189, 28, 212, 192, 78]),
+        192: new Uint8Array([208, 238, 131, 65, 63, 68, 196, 63, 186, 208,
+            61, 207, 166, 18, 99, 152, 29, 109, 221, 95, 240, 30, 28, 246]),
+        256: new Uint8Array([103, 105, 56, 35, 251, 29, 88, 7, 63, 145, 236,
+            233, 204, 58, 249, 16, 229, 83, 38, 22, 164, 210, 123, 19, 235, 123, 116,
+            216, 0, 11, 191, 48])
+    }
+
+    // AES-GCM needs an IV of no more than 2^64 - 1 bytes. Well, 32 bytes is okay then.
+    var iv = new Uint8Array([58, 146, 115, 42, 166, 234, 57,
+        191, 57, 134, 224, 199, 63, 169, 32, 0, 32, 33, 117, 56,
+        94, 248, 173, 234, 194, 200, 115, 53, 235, 146, 141, 212]);
+
+    // Authenticated encryption via AES-GCM requires additional data that
+    // will be checked. We use the ASCII encoded Editorial Note
+    // following the Abstract of the Web Cryptography API recommendation.
+    var additionalData = new Uint8Array([84, 104, 101, 114, 101,
+        32, 97, 114, 101, 32, 55, 32, 102, 117, 114, 116, 104, 101,
+        114, 32, 101, 100, 105, 116, 111, 114, 105, 97, 108, 32,
+        110, 111, 116, 101, 115, 32, 105, 110, 32, 116, 104, 101,
+        32, 100, 111, 99, 117, 109, 101, 110, 116, 46]);
+
+    //  The length of the tag defaults to 16 bytes (128 bit).
+    var tag = {
+        128: new Uint8Array([194, 226, 198, 253, 239, 28,
+            197, 240, 123, 216, 176, 151, 239, 200, 184, 183]),
+        192: new Uint8Array([183, 57, 32, 144, 164, 76, 121, 77, 58,
+            86, 62, 132, 53, 130, 96, 225]),
+        256: new Uint8Array([188, 239, 241, 48, 159, 21, 213, 0, 241,
+            42, 85, 76, 194, 28, 49, 60])
+    };
+
+    var tag_with_empty_ad = {
+        128: new Uint8Array([222, 51, 11, 23, 36, 222, 250, 248, 27, 98, 30, 81, 150, 35, 220, 198]),
+        192: new Uint8Array([243, 11, 130, 112, 169, 239, 114, 238, 185, 219, 93, 1, 95, 108, 184, 183]),
+        256: new Uint8Array([244, 186, 86, 203, 154, 37, 191, 248, 246, 57, 139, 130, 224, 47, 217, 238])
+    };
+
+
+    // Results. These were created using the Python cryptography module.
+
+    // AES-GCM produces ciphertext and a tag.
+    var ciphertext = {
+        128: new Uint8Array([180, 241, 40, 183, 105,
+            52, 147, 238, 224, 175, 175, 236, 168, 244, 241, 121, 9,
+            202, 225, 237, 56, 216, 253, 254, 186, 102, 111, 207, 228,
+            190, 130, 177, 159, 246, 6, 53, 249, 113, 228, 254, 81,
+            126, 253, 191, 100, 43, 251, 147, 107, 91, 166, 231, 201,
+            241, 180, 214, 112, 47, 123, 164, 186, 134, 54, 65, 22,
+            181, 201, 82, 236, 59, 52, 139, 172, 39, 41, 89, 123, 62,
+            102, 167, 82, 150, 250, 93, 96, 169, 135, 89, 245, 255,
+            164, 192, 169, 159, 25, 16, 139, 145, 76, 4, 144, 131,
+            148, 197, 204, 46, 23, 110, 193, 228, 127, 120, 242, 24,
+            54, 240, 181, 162, 98, 244, 249, 68, 134, 122, 126, 151,
+            38, 108, 116, 68, 150, 109, 38, 194, 21, 159, 140, 205,
+            183, 35, 97, 151, 186, 120, 145, 22, 235, 22, 210, 223,
+            187, 143, 162, 183, 93, 196, 104, 51, 96, 53, 234, 250,
+            184, 76, 237, 157, 37, 203, 226, 87, 222, 75, 240, 95, 218,
+            222, 64, 81, 165, 75, 201, 216, 190, 13, 116, 217, 69, 66,
+            47, 161, 68, 247, 74, 253, 157, 181, 162, 121, 53, 32, 91,
+            124, 230, 105, 224, 17, 187, 50, 61, 77, 103, 79, 71, 57,
+            163, 116, 234, 149, 27, 105, 24, 31, 159, 3, 128, 130, 42,
+            94, 125, 200, 142, 251, 148, 201, 17, 149, 232, 84, 50, 17,
+            18, 203, 186, 226, 164, 227, 202, 76, 65, 16, 163, 224,
+            132, 52, 31, 101, 129, 72, 171, 159, 42, 177, 253, 98, 86,
+            201, 95, 117, 62, 12, 205, 78, 36, 126, 196, 121, 89, 185,
+            37, 161, 66, 181, 117, 186, 71, 124, 132, 110, 120, 27,
+            246, 163, 18, 13, 90, 200, 127, 82, 209, 241, 170, 73, 247,
+            137, 96, 244, 254, 251, 119, 71, 156, 27, 107, 53, 33, 45,
+            22, 0, 144, 48, 32, 11, 116, 21, 125, 246, 217, 171, 158,
+            224, 142, 234, 141, 242, 168, 89, 154, 66, 227, 161, 182,
+            96, 1, 88, 78, 12, 7, 239, 30, 206, 31, 89, 111, 107, 42,
+            37, 241, 148, 232, 1, 8, 251, 117, 146, 183, 9, 48, 39, 94,
+            59, 70, 230, 26, 165, 97, 156, 140, 141, 31, 62, 10, 206,
+            55, 48, 207, 0, 197, 202, 197, 108, 133, 175, 80, 4, 16,
+            154, 223, 255, 4, 196, 188, 178, 240, 29, 13, 120, 5, 225,
+            202, 3, 35, 225, 158, 92, 152, 73, 205, 107, 157, 224, 245,
+            99, 194, 171, 156, 245, 247, 183, 165, 40, 62, 200, 110,
+            29, 151, 206, 100, 175, 88, 36, 242, 90, 4, 82, 73, 250,
+            140, 245, 217, 9, 153, 35, 242, 206, 78, 197, 121, 115, 15,
+            80, 128, 101, 191, 240, 91, 151, 249, 62, 62, 244, 18, 3,
+            17, 135, 222, 210, 93, 149, 123]),
+
+        192: new Uint8Array([126, 160, 166, 112, 227, 212, 106,
+            186, 175, 70, 24, 28, 86, 149, 31, 154, 156, 190, 244, 132, 44, 61, 149,
+            242, 105, 67, 17, 136, 7, 146, 153, 170, 200, 214, 142, 205, 170, 225,
+            85, 44, 241, 159, 255, 234, 10, 13, 37, 48, 255, 21, 141, 176, 60, 117,
+            73, 130, 247, 204, 144, 102, 167, 89, 203, 235, 229, 129, 122, 253, 124,
+            179, 115, 118, 163, 157, 67, 141, 122, 146, 209, 11, 112, 5, 230, 117,
+            123, 184, 243, 99, 83, 10, 31, 166, 96, 1, 121, 44, 10, 241, 24, 43,
+            184, 187, 25, 239, 246, 176, 108, 230, 127, 25, 42, 67, 202, 140, 179,
+            104, 159, 75, 103, 43, 248, 98, 166, 179, 67, 0, 163, 227, 84, 40, 129,
+            227, 198, 205, 7, 156, 16, 185, 24, 166, 59, 218, 197, 114, 74, 34, 126,
+            22, 226, 226, 85, 212, 69, 83, 163, 185, 68, 109, 182, 54, 209, 237, 96,
+            184, 32, 53, 127, 175, 13, 146, 141, 115, 164, 184, 98, 245, 174, 223,
+            46, 32, 167, 39, 103, 19, 210, 80, 131, 254, 103, 249, 247, 29, 120, 31,
+            105, 241, 103, 169, 249, 93, 153, 74, 56, 53, 239, 157, 132, 236, 169,
+            246, 242, 24, 113, 97, 128, 238, 152, 148, 31, 84, 8, 52, 105, 198, 116,
+            103, 132, 48, 199, 23, 90, 24, 29, 63, 41, 117, 191, 57, 31, 209, 128,
+            60, 119, 175, 84, 141, 177, 165, 169, 195, 35, 163, 105, 146, 157, 209,
+            93, 149, 105, 160, 93, 231, 78, 201, 92, 235, 200, 89, 37, 50, 181, 30,
+            213, 242, 59, 156, 219, 19, 158, 17, 224, 81, 108, 52, 87, 248, 101, 23,
+            39, 107, 67, 151, 103, 230, 126, 202, 184, 118, 226, 18, 29, 93, 37, 208,
+            40, 82, 113, 35, 157, 145, 152, 50, 253, 140, 47, 141, 192, 1, 148, 114,
+            40, 10, 112, 79, 227, 16, 105, 247, 31, 49, 102, 195, 75, 183, 172, 254,
+            188, 42, 89, 77, 38, 104, 1, 180, 106, 61, 71, 70, 35, 160, 103, 101,
+            244, 26, 226, 37, 159, 155, 4, 107, 222, 219, 136, 37, 24, 246, 44, 23,
+            44, 248, 132, 108, 59, 179, 99, 145, 132, 82, 53, 203, 111, 150, 55,
+            123, 51, 214, 165, 108, 124, 179, 131, 174, 139, 224, 114, 96, 218, 181,
+            243, 128, 198, 98, 115, 92, 95, 165, 23, 229, 108, 146, 14, 244, 162,
+            37, 85, 201, 33, 44, 92, 106, 112, 185, 16, 189, 42, 114, 109, 59, 124,
+            131, 16, 211, 31, 97, 29, 135, 61, 150, 75, 250, 207, 129, 38, 205, 187,
+            186, 55, 207, 232, 24, 48, 232, 49, 226, 16, 12, 27, 70, 31, 124, 128,
+            218, 100, 91, 200, 184, 78, 252, 100, 235, 62, 43, 69, 214, 163, 65, 14,
+            44, 180]),
+
+        256: new Uint8Array([8, 97, 235, 113, 70, 32, 135, 131,
+            210, 209, 124, 160, 255, 182, 9, 29, 125, 193, 27, 240, 129, 46, 2, 137,
+            169, 142, 61, 7, 145, 54, 170, 207, 159, 111, 39, 95, 87, 63, 162, 27,
+            6, 18, 219, 215, 116, 34, 90, 57, 114, 244, 102, 145, 67, 6, 51, 152,
+            247, 165, 242, 116, 100, 219, 177, 72, 177, 17, 110, 67, 93, 219, 100,
+            217, 20, 207, 89, 154, 45, 37, 105, 83, 67, 162, 140, 235, 129, 40, 177,
+            202, 174, 54, 148, 55, 156, 193, 232, 249, 134, 163, 195, 51, 114, 116,
+            65, 38, 73, 99, 96, 249, 224, 69, 17, 119, 186, 188, 181, 43, 78, 156,
+            76, 138, 226, 63, 5, 248, 9, 94, 26, 1, 2, 235, 39, 174, 74, 47, 183,
+            22, 40, 47, 47, 13, 100, 119, 12, 67, 178, 184, 56, 167, 238, 143, 13,
+            44, 208, 185, 151, 108, 6, 17, 52, 122, 182, 210, 207, 42, 219, 37, 74,
+            94, 126, 36, 249, 37, 32, 4, 218, 44, 238, 69, 56, 219, 31, 77, 173, 46,
+            187, 103, 36, 112, 213, 252, 40, 87, 164, 240, 163, 159, 32, 129, 125,
+            178, 108, 47, 28, 31, 36, 42, 115, 36, 14, 145, 195, 156, 191, 46, 163,
+            249, 181, 31, 90, 73, 30, 72, 57, 223, 63, 60, 79, 140, 14, 117, 31,
+            145, 222, 156, 121, 237, 32, 145, 143, 96, 12, 254, 35, 21, 21, 59, 168,
+            171, 154, 217, 0, 59, 202, 175, 103, 214, 192, 175, 26, 18, 43, 54, 176,
+            222, 75, 22, 7, 122, 253, 224, 145, 61, 42, 208, 73, 237, 84, 141, 209,
+            213, 228, 46, 244, 59, 9, 68, 6, 35, 88, 189, 10, 62, 9, 85, 28, 44, 82,
+            19, 153, 160, 178, 240, 56, 160, 244, 201, 173, 77, 61, 20, 227, 30,
+            180, 167, 16, 105, 185, 193, 95, 207, 41, 23, 134, 78, 198, 182, 93, 24,
+            89, 247, 231, 75, 233, 194, 137, 242, 114, 194, 190, 130, 138, 238, 94,
+            137, 193, 194, 115, 137, 190, 207, 169, 83, 155, 14, 210, 160, 129, 195,
+            161, 234, 221, 255, 114, 67, 98, 12, 93, 41, 65, 183, 244, 103, 247,
+            101, 82, 246, 125, 87, 125, 78, 21, 186, 102, 205, 20, 40, 32, 201, 174,
+            15, 52, 240, 217, 180, 162, 108, 6, 211, 41, 18, 135, 232, 184, 18, 188,
+            169, 157, 190, 76, 166, 75, 176, 127, 39, 251, 22, 203, 153, 80, 49,
+            241, 124, 137, 151, 123, 204, 43, 159, 190, 177, 196, 18, 117, 169, 46,
+            152, 251, 45, 25, 164, 27, 145, 214, 228, 55, 15, 2, 131, 216, 80, 255,
+            204, 175, 100, 59, 145, 15, 103, 40, 33, 45, 255, 200, 254, 172, 138,
+            20, 58, 87, 182, 192, 148, 219, 41, 88, 230, 229, 70, 249])
+    };
+
+    var keyLengths = [128, 192, 256];
+    var tagLengths = [32, 64, 96, 104, 112, 120, 128];
+
+    // All the scenarios that should succeed, if the key has "encrypt" usage
+    var passing = [];
+    keyLengths.forEach(function(keyLength) {
+        tagLengths.forEach(function(tagLength) {
+            var byteCount = tagLength / 8;
+
+            var result = new Uint8Array(ciphertext[keyLength].byteLength + byteCount);
+            result.set(ciphertext[keyLength], 0);
+            result.set(tag[keyLength].slice(0, byteCount), ciphertext[keyLength].byteLength);
+            passing.push({
+                    name: "AES-GCM " + keyLength.toString() + "-bit key, " + tagLength.toString() + "-bit tag",
+                    keyBuffer: keyBytes[keyLength],
+                    key: null,
+                    algorithm: {name: "AES-GCM", iv: iv, additionalData: additionalData, tagLength: tagLength},
+                    plaintext: plaintext,
+                    result: result
+            });
+
+            var noadresult = new Uint8Array(ciphertext[keyLength].byteLength + byteCount);
+            noadresult.set(ciphertext[keyLength], 0);
+            noadresult.set(tag_with_empty_ad[keyLength].slice(0, byteCount), ciphertext[keyLength].byteLength);
+            passing.push({
+                    name: "AES-GCM " + keyLength.toString() + "-bit key, no additional data, " + tagLength.toString() + "-bit tag",
+                    keyBuffer: keyBytes[keyLength],
+                    key: null,
+                    algorithm: {name: "AES-GCM", iv: iv, tagLength: tagLength},
+                    plaintext: plaintext,
+                    result: noadresult
+            });
+        });
+    });
+
+    // Scenarios that should fail because of a bad tag length, causing an OperationError
+    var failing = [];
+    keyLengths.forEach(function(keyLength) {
+        // First, make some tests for bad tag lengths
+        [24, 48, 72, 95, 129, 256].forEach(function(badTagLength) {
+            failing.push({
+                name: "AES-GCM " + keyLength.toString() + "-bit key, illegal tag length " + badTagLength.toString() + "-bits",
+                keyBuffer: keyBytes[keyLength],
+                key: null,
+                algorithm: {name: "AES-GCM", iv: iv, additionalData: additionalData, tagLength: badTagLength},
+                plaintext: plaintext,
+                result: ciphertext[keyLength]
+            });
+        });
+    });
+
+    return {passing: passing, failing: failing, decryptionFailing: []};
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/rsa.https.worker.js b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/rsa.https.worker.js
new file mode 100644
index 0000000..b016326
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/rsa.https.worker.js
@@ -0,0 +1,5 @@
+importScripts("/resources/testharness.js");
+importScripts("rsa_vectors.js");
+importScripts("rsa.js");
+
+run_test();
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/rsa.js b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/rsa.js
new file mode 100644
index 0000000..e9c2265
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/rsa.js
@@ -0,0 +1,376 @@
+
+function run_test() {
+    var subtle = self.crypto.subtle; // Change to test prefixed implementations
+
+    // When are all these tests really done? When all the promises they use have resolved.
+    var all_promises = [];
+
+    // Source file rsa_vectors.js provides the getTestVectors method
+    // for the RSA-OAEP algorithm that drives these tests.
+    var vectors = getTestVectors();
+    var passingVectors = vectors.passing;
+    var failingVectors = vectors.failing;
+
+    // Test decryption, first, because encryption tests rely on that working
+    passingVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["encrypt"], ["decrypt"])
+        .then(function(vectors) {
+            // Get a one byte longer plaintext to encrypt
+            if (!("ciphertext" in vector)) {
+                return;
+            }
+
+            promise_test(function(test) {
+                return subtle.decrypt(vector.algorithm, vector.privateKey, vector.ciphertext)
+                .then(function(plaintext) {
+                    assert_true(equalBuffers(plaintext, vector.plaintext, "Decryption works"));
+                }, function(err) {
+                    assert_unreached("Decryption should not throw error " + vector.name + ": " + err.message + "'");
+                });
+            }, vector.name + " decryption");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " decryption");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Test decryption with an altered buffer
+    passingVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["encrypt"], ["decrypt"])
+        .then(function(vectors) {
+            // Get a one byte longer plaintext to encrypt
+            if (!("ciphertext" in vector)) {
+                return;
+            }
+
+            promise_test(function(test) {
+                var ciphertext = copyBuffer(vector.ciphertext);
+                var operation = subtle.decrypt(vector.algorithm, vector.privateKey, ciphertext)
+                .then(function(plaintext) {
+                    assert_true(equalBuffers(plaintext, vector.plaintext, "Decryption works"));
+                }, function(err) {
+                    assert_unreached("Decryption should not throw error " + vector.name + ": " + err.message + "'");
+                });
+                ciphertext[0] = 255 - ciphertext[0];
+                return operation;
+            }, vector.name + " decryption with altered ciphertext");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " decryption with altered ciphertext");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for failures due to using publicKey to decrypt.
+    passingVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["encrypt"], ["decrypt"])
+        .then(function(vectors) {
+            promise_test(function(test) {
+                return subtle.decrypt(vector.algorithm, vector.publicKey, vector.ciphertext)
+                .then(function(plaintext) {
+                    assert_unreached("Should have thrown error for using publicKey to decrypt in " + vector.name + ": " + err.message + "'");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw InvalidAccessError instead of " + err.message);
+                });
+            }, vector.name + " using publicKey to decrypt");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " using publicKey to decrypt");
+        });
+
+        all_promises.push(promise);
+    });
+
+
+    // Check for failures due to no "decrypt" usage.
+    passingVectors.forEach(function(originalVector) {
+        var vector = Object.assign({}, originalVector);
+
+        var promise = importVectorKeys(vector, ["encrypt"], ["unwrapKey"])
+        .then(function(vectors) {
+            // Get a one byte longer plaintext to encrypt
+            promise_test(function(test) {
+                return subtle.decrypt(vector.algorithm, vector.publicKey, vector.ciphertext)
+                .then(function(plaintext) {
+                    assert_unreached("Should have thrown error for no decrypt usage in " + vector.name + ": " + err.message + "'");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw InvalidAccessError instead of " + err.message);
+                });
+            }, vector.name + " no decrypt usage");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " no decrypt usage");
+        });
+
+        all_promises.push(promise);
+    });
+
+
+    // Check for successful encryption even if plaintext is altered after call.
+    passingVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["encrypt"], ["decrypt"])
+        .then(function(vectors) {
+            promise_test(function(test) {
+                var plaintext = copyBuffer(vector.plaintext);
+                var operation = subtle.encrypt(vector.algorithm, vector.publicKey, plaintext)
+                .then(function(ciphertext) {
+                    assert_equals(ciphertext.byteLength * 8, vector.privateKey.algorithm.modulusLength, "Ciphertext length matches modulus length");
+                    // Can we get the original plaintext back via decrypt?
+                    return subtle.decrypt(vector.algorithm, vector.privateKey, ciphertext)
+                    .then(function(result) {
+                        assert_true(equalBuffers(result, vector.plaintext), "Round trip returns original plaintext");
+                        return ciphertext;
+                    }, function(err) {
+                        assert_unreached("decrypt error for test " + vector.name + ": " + err.message + "'");
+                    });
+                })
+                .then(function(priorCiphertext) {
+                    // Will a second encrypt give us different ciphertext, as it should?
+                    return subtle.encrypt(vector.algorithm, vector.publicKey, vector.plaintext)
+                    .then(function(ciphertext) {
+                        assert_false(equalBuffers(priorCiphertext, ciphertext), "Two encrypts give different results")
+                    }, function(err) {
+                        assert_unreached("second time encrypt error for test " + vector.name + ": '" + err.message + "'");
+                    });
+                }, function(err) {
+                    assert_unreached("decrypt error for test " + vector.name + ": '" + err.message + "'");
+                });
+
+                plaintext[0] = 255 - plaintext[0];
+                return operation;
+            }, vector.name + " with altered plaintext");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " with altered plaintext");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for successful encryption.
+    passingVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["encrypt"], ["decrypt"])
+        .then(function(vectors) {
+            promise_test(function(test) {
+                return subtle.encrypt(vector.algorithm, vector.publicKey, vector.plaintext)
+                .then(function(ciphertext) {
+                    assert_equals(ciphertext.byteLength * 8, vector.privateKey.algorithm.modulusLength, "Ciphertext length matches modulus length");
+
+                    // Can we get the original plaintext back via decrypt?
+                    return subtle.decrypt(vector.algorithm, vector.privateKey, ciphertext)
+                    .then(function(result) {
+                        assert_true(equalBuffers(result, vector.plaintext), "Round trip returns original plaintext");
+                        return ciphertext;
+                    }, function(err) {
+                        assert_unreached("decrypt error for test " + vector.name + ": " + err.message + "'");
+                    });
+                })
+                .then(function(priorCiphertext) {
+                    // Will a second encrypt give us different ciphertext, as it should?
+                    return subtle.encrypt(vector.algorithm, vector.publicKey, vector.plaintext)
+                    .then(function(ciphertext) {
+                        assert_false(equalBuffers(priorCiphertext, ciphertext), "Two encrypts give different results")
+                    }, function(err) {
+                        assert_unreached("second time encrypt error for test " + vector.name + ": '" + err.message + "'");
+                    });
+                }, function(err) {
+                    assert_unreached("decrypt error for test " + vector.name + ": '" + err.message + "'");
+                });
+            }, vector.name);
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name);
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for failures due to too long plaintext.
+    passingVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["encrypt"], ["decrypt"])
+        .then(function(vectors) {
+            // Get a one byte longer plaintext to encrypt
+            var plaintext = new Uint8Array(vector.plaintext.byteLength + 1);
+            plaintext.set(plaintext, 0);
+            plaintext.set(new Uint8Array([32]), vector.plaintext.byteLength);
+            promise_test(function(test) {
+                return subtle.encrypt(vector.algorithm, vector.publicKey, plaintext)
+                .then(function(ciphertext) {
+                    assert_unreached("Should have thrown error for too long plaintext in " + vector.name + ": " + err.message + "'");
+                }, function(err) {
+                    assert_equals(err.name, "OperationError", "Should throw OperationError instead of " + err.message);
+                });
+            }, vector.name + " too long plaintext");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " too long plaintext");
+        });
+
+        all_promises.push(promise);
+    });
+
+
+    // Check for failures due to using privateKey to encrypt.
+    passingVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["encrypt"], ["decrypt"])
+        .then(function(vectors) {
+            promise_test(function(test) {
+                return subtle.encrypt(vector.algorithm, vector.privateKey, vector.plaintext)
+                .then(function(ciphertext) {
+                    assert_unreached("Should have thrown error for using privateKey to encrypt in " + vector.name + ": " + err.message + "'");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw InvalidAccessError instead of " + err.message);
+                });
+            }, vector.name + " using privateKey to encrypt");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " using privateKey to encrypt");
+        });
+
+        all_promises.push(promise);
+    });
+
+
+    // Check for failures due to no "encrypt usage".
+    passingVectors.forEach(function(originalVector) {
+        var vector = Object.assign({}, originalVector);
+
+        var promise = importVectorKeys(vector, [], ["decrypt"])
+        .then(function(vectors) {
+            // Get a one byte longer plaintext to encrypt
+            promise_test(function(test) {
+                return subtle.encrypt(vector.algorithm, vector.publicKey, vector.plaintext)
+                .then(function(ciphertext) {
+                    assert_unreached("Should have thrown error for no encrypt usage in " + vector.name + ": " + err.message + "'");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw InvalidAccessError instead of " + err.message);
+                });
+            }, vector.name + " no encrypt usage");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested encryption
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " no encrypt usage");
+        });
+
+        all_promises.push(promise);
+    });
+
+    Promise.all(all_promises)
+    .then(function() {done();})
+    .catch(function() {done();})
+
+    // A test vector has all needed fields for encryption, EXCEPT that the
+    // key field may be null. This function replaces that null with the Correct
+    // CryptoKey object.
+    //
+    // Returns a Promise that yields an updated vector on success.
+    function importVectorKeys(vector, publicKeyUsages, privateKeyUsages) {
+        var publicPromise, privatePromise;
+
+        if (vector.publicKey !== null) {
+            publicPromise = new Promise(function(resolve, reject) {
+                resolve(vector);
+            });
+        } else {
+            publicPromise = subtle.importKey(vector.publicKeyFormat, vector.publicKeyBuffer, {name: vector.algorithm.name, hash: vector.hash}, false, publicKeyUsages)
+            .then(function(key) {
+                vector.publicKey = key;
+                return vector;
+            });        // Returns a copy of the sourceBuffer it is sent.
+        function copyBuffer(sourceBuffer) {
+            var source = new Uint8Array(sourceBuffer);
+            var copy = new Uint8Array(sourceBuffer.byteLength)
+
+            for (var i=0; i<source.byteLength; i++) {
+                copy[i] = source[i];
+            }
+
+            return copy;
+        }
+
+        }
+
+        if (vector.privateKey !== null) {
+            privatePromise = new Promise(function(resolve, reject) {
+                resolve(vector);
+            });
+        } else {
+            privatePromise = subtle.importKey(vector.privateKeyFormat, vector.privateKeyBuffer, {name: vector.algorithm.name, hash: vector.hash}, false, privateKeyUsages)
+            .then(function(key) {
+                vector.privateKey = key;
+                return vector;
+            });
+        }
+
+        return Promise.all([publicPromise, privatePromise]);
+    }
+
+    // Returns a copy of the sourceBuffer it is sent.
+    function copyBuffer(sourceBuffer) {
+        var source = new Uint8Array(sourceBuffer);
+        var copy = new Uint8Array(sourceBuffer.byteLength)
+
+        for (var i=0; i<source.byteLength; i++) {
+            copy[i] = source[i];
+        }
+
+        return copy;
+    }
+
+    function equalBuffers(a, b) {
+        if (a.byteLength !== b.byteLength) {
+            return false;
+        }
+
+        var aBytes = new Uint8Array(a);
+        var bBytes = new Uint8Array(b);
+
+        for (var i=0; i<a.byteLength; i++) {
+            if (aBytes[i] !== bBytes[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    return;
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/rsa_vectors.js b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/rsa_vectors.js
new file mode 100644
index 0000000..fcc732e
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/rsa_vectors.js
@@ -0,0 +1,204 @@
+
+// rsa_vectors.js
+
+// Data for testing RSA-OAEP with a 2048-bit modulus and 65537 public exponent.
+
+// The following function returns an array of test vectors
+// for the subtleCrypto encrypt method.
+//
+// Each test vector has the following fields:
+//     name - a unique name for this vector
+//     publicKeyBuffer - an arrayBuffer with the key data
+//     publicKeyFormat - "spki" "jwk"
+//     publicKey - a CryptoKey object for the keyBuffer. INITIALLY null! You must fill this in first to use it!
+//     privateKeyBuffer - an arrayBuffer with the key data
+//     privateKeyFormat - "pkcs8" or "jwk"
+//     privateKey - a CryptoKey object for the keyBuffer. INITIALLY null! You must fill this in first to use it!
+//     algorithm - the value of the AlgorithmIdentifier parameter to provide to encrypt
+//     plaintext - the text to encrypt
+//     result - the expected result (usually just ciphertext, sometimes with added authentication)
+function getTestVectors() {
+    var pkcs8 = new Uint8Array([48, 130, 4, 191, 2, 1, 0, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 4, 130, 4, 169, 48, 130, 4, 165, 2, 1, 0, 2, 130, 1, 1, 0, 211, 87, 96, 146, 230, 41, 87, 54, 69, 68, 231, 228, 35, 59, 123, 219, 41, 61, 178, 8, 81, 34, 196, 121, 50, 133, 70, 249, 240, 247, 18, 246, 87, 196, 177, 120, 104, 201, 48, 144, 140, 197, 148, 247, 237, 0, 192, 20, 66, 193, 175, 4, 194, 246, 120, 164, 139, 162, 200, 15, 209, 113, 62, 48, 181, 172, 80, 120, 122, 195, 81, 101, 137, 241, 113, 150, 127, 99, 134, 173, 163, 73, 0, 166, 187, 4, 238, 206, 164, 43, 240, 67, 206, 217, 160, 249, 77, 12, 192, 158, 145, 155, 157, 113, 102, 192, 138, 182, 206, 32, 70, 64, 174, 164, 196, 146, 13, 182, 216, 110, 185, 22, 208, 220, 192, 244, 52, 26, 16, 56, 4, 41, 231, 225, 3, 33, 68, 234, 148, 157, 232, 246, 192, 204, 191, 149, 250, 142, 146, 141, 112, 216, 163, 140, 225, 104, 219, 69, 246, 241, 52, 102, 61, 111, 101, 111, 92, 234, 188, 114, 93, 168, 192, 42, 171, 234, 170, 19, 172, 54, 167, 92, 192, 186, 225, 53, 223, 49, 20, 182, 101, 137, 199, 237, 60, 182, 21, 89, 174, 90, 56, 79, 22, 43, 250, 128, 219, 228, 97, 127, 134, 195, 241, 208, 16, 201, 79, 226, 201, 191, 1, 154, 110, 99, 179, 239, 192, 40, 212, 60, 238, 97, 28, 133, 236, 38, 60, 144, 108, 70, 55, 114, 198, 145, 27, 25, 238, 192, 150, 202, 118, 236, 94, 49, 225, 227, 2, 3, 1, 0, 1, 2, 130, 1, 1, 0, 139, 55, 92, 203, 135, 200, 37, 197, 255, 61, 83, 208, 9, 145, 110, 150, 65, 5, 126, 24, 82, 114, 39, 160, 122, 178, 38, 190, 16, 136, 129, 58, 59, 56, 187, 123, 72, 243, 119, 5, 81, 101, 250, 42, 147, 57, 210, 77, 198, 103, 213, 197, 186, 52, 39, 230, 164, 129, 23, 110, 172, 21, 255, 212, 144, 104, 49, 30, 28, 40, 59, 159, 58, 142, 12, 184, 9, 180, 99, 12, 80, 170, 143, 62, 69, 166, 11, 53, 158, 25, 191, 140, 187, 94, 202, 214, 78, 118, 31, 16, 149, 116, 63, 243, 106, 175, 92, 240, 236, 185, 127, 237, 173, 221, 166, 11, 91, 243, 93, 129, 26, 117, 184, 34, 35, 12, 250, 160, 25, 47, 173, 64, 84, 126, 39, 84, 72, 170, 51, 22, 191, 142, 43, 76, 224, 133, 79, 199, 112, 139, 83, 123, 162, 45, 19, 33, 11, 9, 174, 195, 122, 39, 89, 239, 192, 130, 161, 83, 27, 35, 169, 23, 48, 3, 125, 222, 78, 242, 107, 95, 150, 239, 220, 195, 159, 211, 76, 52, 90, 213, 28, 187, 228, 79, 229, 139, 138, 59, 78, 201, 151, 134, 108, 8, 109, 255, 27, 136, 49, 239, 10, 31, 234, 38, 60, 247, 218, 205, 3, 192, 76, 188, 194, 178, 121, 229, 127, 165, 185, 83, 153, 107, 251, 29, 214, 136, 23, 175, 127, 180, 44, 222, 247, 165, 41, 74, 87, 250, 194, 184, 173, 115, 159, 27, 2, 153, 2, 129, 129, 0, 251, 248, 51, 194, 198, 49, 201, 112, 36, 12, 142, 116, 133, 240, 106, 62, 162, 168, 72, 34, 81, 26, 134, 39, 221, 70, 78, 248, 175, 175, 113, 72, 209, 164, 37, 182, 184, 101, 125, 221, 82, 70, 131, 43, 142, 83, 48, 32, 197, 187, 181, 104, 133, 90, 106, 236, 62, 66, 33, 215, 147, 241, 220, 91, 47, 37, 132, 226, 65, 94, 72, 233, 162, 189, 41, 43, 19, 64, 49, 249, 156, 142, 180, 47, 192, 188, 208, 68, 155, 242, 44, 230, 222, 201, 112, 20, 239, 229, 172, 147, 235, 232, 53, 135, 118, 86, 37, 44, 187, 177, 108, 65, 91, 103, 177, 132, 210, 40, 69, 104, 162, 119, 213, 147, 53, 88, 92, 253, 2, 129, 129, 0, 214, 184, 206, 39, 199, 41, 93, 93, 22, 252, 53, 112, 237, 100, 200, 218, 147, 3, 250, 210, 148, 136, 193, 166, 94, 154, 215, 17, 249, 3, 112, 24, 125, 187, 253, 129, 49, 109, 105, 100, 139, 200, 140, 197, 200, 53, 81, 175, 255, 69, 222, 186, 207, 182, 17, 5, 247, 9, 228, 195, 8, 9, 185, 0, 49, 235, 214, 134, 36, 68, 150, 198, 246, 158, 105, 46, 189, 200, 20, 246, 66, 57, 244, 173, 21, 117, 110, 203, 120, 197, 165, 176, 153, 49, 219, 24, 48, 119, 197, 70, 163, 140, 76, 116, 56, 137, 173, 61, 62, 208, 121, 181, 98, 46, 208, 18, 15, 160, 225, 249, 59, 89, 61, 183, 216, 82, 224, 95, 2, 129, 128, 56, 135, 75, 157, 131, 247, 129, 120, 206, 45, 158, 252, 23, 92, 131, 137, 127, 214, 127, 48, 107, 191, 166, 159, 100, 238, 52, 35, 104, 206, 212, 124, 128, 195, 241, 206, 23, 122, 117, 141, 100, 186, 251, 12, 151, 134, 164, 66, 133, 250, 1, 205, 236, 53, 7, 205, 238, 125, 201, 183, 226, 178, 29, 60, 187, 204, 16, 14, 238, 153, 103, 132, 59, 5, 115, 41, 253, 204, 166, 41, 152, 237, 15, 17, 179, 140, 232, 176, 171, 199, 222, 57, 1, 124, 113, 207, 208, 174, 87, 84, 108, 85, 145, 68, 205, 208, 175, 208, 100, 95, 126, 168, 255, 7, 185, 116, 209, 237, 68, 253, 31, 142, 0, 245, 96, 191, 109, 69, 2, 129, 129, 0, 133, 41, 239, 144, 115, 207, 143, 123, 95, 249, 226, 26, 186, 223, 58, 65, 115, 211, 144, 6, 112, 223, 175, 89, 66, 106, 188, 223, 4, 147, 193, 61, 47, 29, 27, 70, 184, 36, 166, 172, 24, 148, 179, 217, 37, 37, 12, 24, 30, 52, 114, 193, 96, 120, 5, 110, 177, 154, 141, 40, 247, 31, 48, 128, 146, 117, 52, 129, 212, 148, 68, 253, 247, 140, 158, 166, 194, 68, 7, 220, 1, 142, 119, 211, 175, 239, 56, 91, 47, 247, 67, 158, 150, 35, 121, 65, 51, 45, 212, 70, 206, 190, 255, 219, 68, 4, 254, 79, 113, 89, 81, 97, 208, 22, 64, 44, 51, 77, 15, 87, 198, 26, 190, 79, 249, 244, 203, 249, 2, 129, 129, 0, 135, 216, 119, 8, 212, 103, 99, 228, 204, 190, 178, 209, 233, 113, 46, 91, 240, 33, 109, 112, 222, 148, 32, 165, 178, 6, 155, 116, 89, 185, 159, 93, 159, 127, 47, 173, 124, 215, 154, 174, 230, 122, 127, 154, 52, 67, 126, 60, 121, 168, 74, 240, 205, 141, 233, 223, 242, 104, 235, 12, 71, 147, 245, 1, 249, 136, 213, 64, 246, 211, 71, 92, 32, 121, 184, 34, 122, 35, 217, 104, 222, 196, 227, 198, 101, 3, 24, 113, 147, 69, 150, 48, 71, 43, 253, 182, 186, 29, 231, 134, 199, 151, 250, 111, 78, 166, 90, 42, 132, 25, 38, 47, 41, 103, 136, 86, 203, 115, 201, 189, 75, 200, 155, 94, 4, 27, 34, 119]);
+    var spki = new Uint8Array([48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0, 48, 130, 1, 10, 2, 130, 1, 1, 0, 211, 87, 96, 146, 230, 41, 87, 54, 69, 68, 231, 228, 35, 59, 123, 219, 41, 61, 178, 8, 81, 34, 196, 121, 50, 133, 70, 249, 240, 247, 18, 246, 87, 196, 177, 120, 104, 201, 48, 144, 140, 197, 148, 247, 237, 0, 192, 20, 66, 193, 175, 4, 194, 246, 120, 164, 139, 162, 200, 15, 209, 113, 62, 48, 181, 172, 80, 120, 122, 195, 81, 101, 137, 241, 113, 150, 127, 99, 134, 173, 163, 73, 0, 166, 187, 4, 238, 206, 164, 43, 240, 67, 206, 217, 160, 249, 77, 12, 192, 158, 145, 155, 157, 113, 102, 192, 138, 182, 206, 32, 70, 64, 174, 164, 196, 146, 13, 182, 216, 110, 185, 22, 208, 220, 192, 244, 52, 26, 16, 56, 4, 41, 231, 225, 3, 33, 68, 234, 148, 157, 232, 246, 192, 204, 191, 149, 250, 142, 146, 141, 112, 216, 163, 140, 225, 104, 219, 69, 246, 241, 52, 102, 61, 111, 101, 111, 92, 234, 188, 114, 93, 168, 192, 42, 171, 234, 170, 19, 172, 54, 167, 92, 192, 186, 225, 53, 223, 49, 20, 182, 101, 137, 199, 237, 60, 182, 21, 89, 174, 90, 56, 79, 22, 43, 250, 128, 219, 228, 97, 127, 134, 195, 241, 208, 16, 201, 79, 226, 201, 191, 1, 154, 110, 99, 179, 239, 192, 40, 212, 60, 238, 97, 28, 133, 236, 38, 60, 144, 108, 70, 55, 114, 198, 145, 27, 25, 238, 192, 150, 202, 118, 236, 94, 49, 225, 227, 2, 3, 1, 0, 1]);
+
+    // Can optionally provide a label for encryption. We use the ASCII-encoded
+    // abstract from the candidate recommendation.
+    var label = new Uint8Array([84, 104, 101, 114, 101, 32, 97, 114, 101, 32, 55, 32, 102, 117, 114, 116, 104, 101, 114, 32, 101, 100, 105, 116, 111, 114, 105, 97, 108, 32, 110, 111, 116, 101, 115, 32, 105, 110, 32, 116, 104, 101, 32, 100, 111, 99, 117, 109, 101, 110, 116, 46]);
+
+    // overlong plaintext for RSA-OAEP
+    var plaintext = new Uint8Array([95, 77, 186, 79, 50, 12, 12, 232, 118, 114, 90, 252, 229, 251, 210, 91, 248, 62, 90, 113, 37, 160, 140, 175, 231, 60, 62, 186, 196, 33, 119, 157, 249, 213, 93, 24, 12, 58, 233, 148, 38, 69, 225, 216, 47, 238, 140, 157, 41, 75, 60, 177, 160, 138, 153, 49, 32, 27, 60, 14, 129, 252, 71, 202, 207, 131, 21, 162, 175, 102, 50, 65, 19, 195, 182, 98, 48, 195, 70, 8, 196, 244, 89, 54, 52, 206, 2, 178, 103, 54, 34, 119, 240, 168, 64, 202, 116, 188, 61, 26, 98, 54, 149, 44, 94, 215, 170, 248, 168, 254, 203, 221, 250, 117, 132, 230, 151, 140, 234, 93, 42, 91, 159, 183, 241, 180, 140, 139, 11, 229, 138, 48, 82, 2, 117, 77, 131, 118, 16, 115, 116, 121, 60, 240, 38, 170, 238, 83, 0, 114, 125, 131, 108, 215, 30, 113, 179, 69, 221, 178, 228, 68, 70, 255, 197, 185, 1, 99, 84, 19, 137, 13, 145, 14, 163, 128, 152, 74, 144, 25, 16, 49, 50, 63, 22, 219, 204, 157, 107, 225, 104, 184, 72, 133, 56, 76, 160, 62, 18, 96, 10, 193, 194, 72, 2, 138, 243, 114, 108, 201, 52, 99, 136, 46, 168, 192, 42, 171]);
+
+    var ciphertext = {
+        "sha-1, no label": new Uint8Array([144, 30, 240, 156, 187, 254, 154, 73, 57, 185, 169, 196, 60, 207, 34, 51, 158, 21, 117, 247, 199, 67, 36, 73, 64, 117, 193, 146, 196, 11, 104, 153, 110, 176, 224, 77, 123, 21, 23, 116, 255, 247, 192, 11, 102, 23, 74, 36, 159, 195, 209, 166, 18, 60, 112, 166, 111, 214, 27, 103, 203, 84, 246, 131, 254, 1, 4, 158, 74, 68, 165, 147, 126, 53, 202, 225, 183, 61, 206, 18, 174, 160, 156, 208, 28, 76, 72, 144, 15, 175, 221, 117, 58, 196, 1, 86, 96, 118, 185, 184, 99, 128, 124, 241, 65, 166, 48, 68, 134, 94, 12, 188, 66, 185, 149, 251, 99, 159, 233, 153, 78, 148, 192, 227, 129, 64, 74, 225, 181, 84, 205, 178, 117, 21, 240, 151, 152, 220, 90, 7, 166, 11, 22, 43, 241, 47, 140, 239, 124, 225, 102, 49, 77, 148, 45, 128, 254, 67, 208, 223, 31, 176, 215, 233, 81, 78, 247, 41, 220, 108, 132, 85, 131, 247, 74, 178, 195, 109, 150, 132, 212, 59, 113, 150, 42, 24, 255, 14, 43, 19, 206, 116, 245, 55, 251, 58, 11, 0, 237, 227, 41, 231, 124, 17, 144, 10, 7, 14, 32, 248, 109, 192, 124, 172, 181, 111, 120, 33, 208, 36, 146, 52, 16, 108, 110, 11, 77, 218, 130, 224, 254, 189, 178, 2, 239, 12, 123, 16, 213, 96, 240, 186, 253, 199, 143, 6, 36, 24, 87, 131, 181, 34, 131, 202, 20, 161]),
+        "sha-256, no label": new Uint8Array([5, 49, 234, 235, 75, 140, 239, 142, 183, 125, 215, 54, 208, 150, 182, 220, 232, 64, 209, 100, 233, 117, 31, 134, 168, 213, 206, 208, 153, 153, 80, 111, 250, 240, 0, 235, 97, 83, 247, 69, 111, 49, 26, 233, 158, 71, 241, 32, 113, 80, 235, 151, 242, 152, 45, 183, 115, 190, 158, 153, 14, 138, 18, 152, 90, 95, 17, 90, 249, 6, 239, 12, 135, 14, 239, 60, 159, 202, 28, 91, 77, 94, 153, 4, 153, 182, 125, 0, 148, 173, 175, 141, 235, 223, 154, 93, 114, 51, 91, 84, 179, 209, 202, 38, 234, 9, 87, 214, 62, 6, 76, 233, 105, 213, 46, 83, 189, 96, 92, 143, 169, 50, 13, 158, 171, 226, 57, 238, 244, 112, 153, 85, 92, 25, 77, 27, 184, 223, 239, 254, 173, 107, 79, 215, 248, 242, 137, 144, 53, 92, 200, 238, 34, 163, 108, 72, 103, 240, 172, 234, 215, 244, 165, 2, 95, 21, 23, 247, 82, 167, 232, 192, 147, 83, 61, 12, 214, 89, 173, 96, 167, 220, 5, 4, 66, 32, 1, 152, 135, 1, 100, 55, 220, 201, 76, 111, 158, 130, 2, 176, 59, 201, 85, 235, 44, 121, 13, 63, 183, 199, 231, 126, 38, 18, 255, 165, 33, 218, 244, 103, 246, 64, 167, 73, 233, 225, 25, 21, 116, 190, 118, 226, 213, 92, 60, 254, 122, 147, 85, 26, 124, 40, 221, 178, 186, 107, 38, 195, 58, 48, 194, 55, 28, 216, 151, 77]),
+        "sha-384, no label": new Uint8Array([12, 35, 146, 227, 15, 146, 241, 244, 228, 172, 209, 180, 166, 253, 153, 249, 131, 198, 29, 202, 243, 155, 221, 222, 71, 178, 158, 173, 58, 221, 16, 74, 122, 134, 223, 31, 112, 153, 243, 104, 60, 101, 175, 254, 50, 157, 43, 202, 185, 80, 53, 236, 150, 193, 61, 171, 154, 12, 180, 185, 89, 96, 254, 224, 138, 165, 131, 85, 102, 161, 181, 125, 223, 84, 90, 201, 80, 80, 145, 52, 206, 208, 2, 115, 158, 166, 255, 62, 61, 228, 132, 31, 159, 169, 6, 22, 96, 236, 241, 5, 51, 233, 245, 12, 165, 22, 79, 50, 73, 68, 204, 123, 142, 58, 236, 105, 152, 163, 102, 249, 12, 250, 238, 121, 119, 101, 26, 13, 30, 141, 25, 75, 205, 28, 42, 0, 135, 41, 170, 1, 26, 157, 13, 140, 162, 113, 249, 142, 1, 91, 244, 102, 188, 217, 156, 217, 118, 134, 181, 146, 246, 111, 177, 54, 159, 84, 163, 88, 147, 122, 188, 249, 23, 223, 111, 57, 186, 220, 111, 95, 246, 48, 199, 172, 115, 185, 47, 173, 186, 221, 12, 41, 252, 224, 76, 167, 214, 42, 171, 82, 178, 100, 229, 162, 130, 188, 191, 2, 114, 28, 17, 158, 40, 233, 66, 108, 217, 150, 179, 121, 25, 115, 216, 162, 172, 244, 58, 44, 47, 103, 255, 136, 76, 26, 119, 184, 63, 195, 38, 140, 100, 12, 171, 65, 67, 54, 195, 31, 122, 105, 119, 228, 149, 16, 49, 212]),
+        "sha-512, no label": new Uint8Array([6, 38, 211, 70, 213, 37, 49, 19, 221, 192, 248, 206, 209, 145, 141, 136, 235, 240, 8, 105, 248, 128, 175, 137, 197, 230, 123, 179, 121, 75, 181, 138, 155, 246, 25, 229, 165, 89, 9, 65, 143, 108, 126, 20, 168, 88, 160, 197, 48, 66, 117, 2, 219, 122, 254, 96, 36, 147, 170, 111, 123, 168, 57, 151, 25, 139, 10, 233, 125, 219, 141, 58, 125, 174, 89, 38, 220, 25, 15, 5, 135, 69, 33, 5, 10, 49, 28, 253, 148, 251, 213, 53, 223, 8, 184, 64, 185, 94, 249, 211, 64, 53, 131, 136, 32, 2, 163, 61, 76, 9, 162, 189, 80, 71, 109, 237, 147, 9, 11, 147, 61, 254, 1, 185, 208, 228, 44, 219, 17, 163, 239, 184, 212, 197, 229, 210, 35, 236, 4, 117, 37, 187, 169, 26, 248, 95, 160, 165, 252, 21, 102, 253, 73, 115, 145, 124, 88, 143, 121, 128, 236, 70, 144, 101, 181, 54, 179, 64, 57, 232, 153, 72, 158, 96, 241, 79, 255, 90, 67, 16, 110, 43, 234, 153, 20, 57, 75, 83, 23, 184, 216, 25, 215, 52, 9, 241, 114, 21, 223, 184, 177, 247, 66, 98, 13, 15, 175, 204, 82, 81, 205, 22, 15, 92, 28, 99, 186, 234, 241, 33, 37, 210, 15, 8, 197, 30, 208, 97, 7, 42, 51, 173, 213, 171, 59, 154, 71, 53, 78, 88, 244, 50, 157, 33, 111, 143, 185, 61, 91, 118, 237, 245, 93, 91, 156, 36]),
+        "sha-1, with label": new Uint8Array([69, 12, 147, 43, 219, 95, 34, 61, 29, 64, 218, 190, 133, 69, 125, 35, 8, 73, 73, 154, 87, 194, 91, 201, 130, 111, 243, 86, 90, 124, 254, 130, 187, 133, 158, 32, 159, 234, 150, 98, 91, 246, 120, 166, 57, 233, 98, 7, 160, 58, 125, 113, 53, 79, 2, 202, 208, 104, 123, 195, 27, 84, 250, 98, 8, 169, 83, 207, 94, 31, 97, 31, 81, 0, 247, 153, 208, 99, 64, 185, 244, 213, 194, 62, 200, 171, 78, 245, 10, 62, 144, 176, 206, 94, 104, 172, 45, 57, 114, 196, 243, 166, 34, 67, 137, 41, 77, 6, 32, 225, 9, 234, 183, 32, 38, 40, 26, 93, 230, 191, 27, 192, 183, 67, 224, 156, 64, 189, 36, 27, 216, 57, 58, 164, 48, 164, 74, 218, 167, 196, 208, 221, 79, 102, 118, 23, 123, 26, 227, 53, 185, 196, 14, 233, 154, 6, 140, 233, 204, 153, 109, 163, 164, 226, 170, 207, 79, 123, 26, 188, 129, 124, 98, 82, 255, 95, 142, 71, 26, 5, 215, 198, 129, 179, 110, 130, 251, 222, 140, 210, 226, 37, 200, 117, 100, 172, 26, 138, 97, 11, 87, 161, 104, 210, 68, 117, 46, 87, 74, 250, 152, 86, 194, 42, 117, 122, 254, 235, 202, 150, 249, 63, 110, 109, 23, 197, 32, 81, 89, 39, 217, 156, 163, 78, 237, 253, 25, 188, 227, 31, 35, 174, 190, 21, 157, 160, 37, 60, 39, 193, 11, 92, 15, 251, 125, 151]),
+        "sha-256, with label": new Uint8Array([180, 212, 109, 8, 118, 130, 24, 5, 96, 121, 113, 102, 114, 157, 149, 31, 234, 5, 95, 138, 184, 10, 16, 249, 49, 78, 85, 222, 225, 47, 172, 108, 33, 36, 206, 47, 211, 159, 202, 241, 52, 172, 221, 28, 99, 1, 176, 51, 82, 146, 190, 137, 163, 202, 212, 74, 150, 204, 58, 26, 43, 54, 135, 91, 246, 201, 59, 106, 181, 9, 10, 118, 191, 106, 122, 102, 175, 114, 2, 182, 146, 169, 55, 124, 84, 187, 254, 237, 225, 252, 32, 197, 79, 97, 219, 250, 54, 81, 52, 121, 146, 194, 13, 196, 88, 212, 7, 146, 237, 232, 31, 113, 215, 200, 42, 158, 239, 180, 51, 152, 214, 145, 106, 11, 115, 160, 21, 71, 171, 190, 45, 25, 229, 19, 130, 210, 52, 58, 14, 55, 82, 250, 215, 193, 110, 178, 143, 101, 163, 63, 149, 216, 176, 163, 145, 66, 239, 62, 204, 195, 102, 14, 157, 2, 155, 114, 230, 225, 55, 121, 167, 172, 182, 187, 75, 149, 49, 213, 104, 144, 202, 230, 111, 125, 119, 236, 213, 155, 131, 122, 43, 55, 245, 183, 60, 140, 103, 88, 45, 84, 157, 247, 67, 243, 169, 102, 176, 225, 192, 181, 154, 252, 90, 95, 17, 161, 112, 96, 196, 105, 104, 55, 6, 92, 212, 18, 127, 85, 190, 12, 105, 123, 219, 110, 130, 111, 179, 32, 183, 81, 246, 240, 135, 59, 5, 210, 173, 15, 102, 215, 87, 95, 136, 136, 238, 12]),
+        "sha-384, with label": new Uint8Array([204, 60, 188, 131, 15, 114, 86, 246, 190, 155, 206, 62, 68, 249, 98, 63, 178, 144, 1, 244, 42, 248, 47, 9, 253, 8, 139, 186, 215, 180, 191, 92, 247, 19, 146, 242, 65, 195, 105, 211, 136, 84, 225, 236, 75, 202, 243, 148, 197, 68, 115, 51, 135, 65, 180, 62, 123, 91, 27, 67, 133, 14, 183, 65, 49, 1, 6, 94, 114, 249, 66, 36, 250, 189, 73, 218, 155, 213, 204, 240, 103, 185, 206, 189, 80, 60, 249, 1, 127, 190, 28, 196, 167, 67, 123, 222, 203, 124, 205, 153, 251, 242, 77, 151, 213, 215, 162, 189, 31, 28, 244, 1, 167, 58, 3, 169, 93, 143, 27, 40, 167, 86, 225, 85, 59, 93, 176, 82, 209, 224, 144, 21, 35, 252, 182, 97, 115, 200, 70, 117, 109, 240, 175, 102, 208, 100, 124, 230, 180, 232, 159, 77, 176, 75, 139, 58, 57, 254, 13, 183, 25, 27, 246, 182, 51, 171, 197, 226, 26, 7, 105, 225, 238, 147, 61, 68, 102, 97, 247, 149, 39, 8, 68, 70, 215, 220, 205, 74, 195, 183, 112, 152, 91, 70, 122, 163, 30, 66, 51, 52, 190, 204, 209, 223, 111, 67, 44, 18, 14, 108, 156, 62, 165, 221, 6, 246, 148, 233, 148, 50, 217, 248, 44, 99, 233, 118, 235, 248, 78, 45, 202, 61, 211, 220, 193, 74, 6, 229, 203, 212, 114, 116, 242, 214, 85, 165, 199, 114, 125, 53, 5, 87, 238, 208, 145, 184]),
+        "sha-512, with label": new Uint8Array([134, 151, 181, 94, 239, 91, 13, 83, 17, 57, 138, 21, 242, 206, 24, 86, 184, 239, 238, 57, 231, 116, 113, 139, 12, 128, 104, 100, 147, 57, 228, 183, 167, 225, 41, 180, 247, 133, 141, 0, 121, 193, 235, 168, 184, 248, 107, 34, 34, 233, 97, 215, 247, 240, 20, 245, 14, 0, 167, 17, 235, 204, 81, 97, 52, 81, 9, 136, 91, 59, 122, 200, 249, 74, 63, 68, 10, 18, 162, 243, 10, 190, 118, 60, 24, 74, 231, 92, 98, 179, 221, 150, 5, 66, 78, 93, 200, 212, 29, 76, 50, 246, 190, 84, 7, 245, 176, 148, 97, 5, 16, 22, 222, 173, 165, 200, 169, 93, 58, 8, 123, 229, 124, 220, 66, 123, 34, 69, 49, 33, 25, 107, 32, 250, 98, 61, 41, 222, 108, 112, 37, 42, 178, 163, 81, 157, 28, 160, 3, 121, 88, 10, 249, 25, 25, 22, 66, 0, 36, 187, 176, 199, 154, 122, 138, 43, 72, 217, 90, 43, 119, 50, 210, 166, 202, 2, 121, 172, 24, 172, 51, 74, 161, 45, 107, 150, 187, 89, 9, 89, 183, 233, 169, 149, 78, 145, 228, 156, 138, 215, 232, 219, 33, 33, 224, 178, 174, 16, 6, 72, 185, 214, 34, 204, 159, 161, 154, 11, 151, 142, 39, 244, 74, 75, 243, 191, 123, 231, 32, 54, 118, 235, 12, 19, 200, 165, 252, 161, 87, 46, 99, 51, 248, 146, 180, 122, 44, 210, 103, 237, 169, 50, 28, 210, 121, 136])
+    };
+
+    var passing = [
+        {
+            name: "RSA-OAEP with SHA-1 and no label",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-OAEP"},
+            hash: "SHA-1",
+            plaintext: plaintext.slice(0, 214),
+            ciphertext: ciphertext["sha-1, no label"]
+        },
+        {
+            name: "RSA-OAEP with SHA-256 and no label",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-OAEP"},
+            hash: "SHA-256",
+            plaintext: plaintext.slice(0, 190),
+            ciphertext: ciphertext["sha-256, no label"]
+        },
+        {
+            name: "RSA-OAEP with SHA-384 and no label",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-OAEP"},
+            hash: "SHA-384",
+            plaintext: plaintext.slice(0, 158),
+            ciphertext: ciphertext["sha-384, no label"]
+        },
+        {
+            name: "RSA-OAEP with SHA-512 and no label",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-OAEP"},
+            hash: "SHA-512",
+            plaintext: plaintext.slice(0, 126),
+            ciphertext: ciphertext["sha-512, no label"]
+        },
+        {
+            name: "RSA-OAEP with SHA-1 and empty label",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-OAEP", label: new Uint8Array([])},
+            hash: "SHA-1",
+            plaintext: plaintext.slice(0, 214),
+            ciphertext: ciphertext["sha-1, no label"]
+        },
+        {
+            name: "RSA-OAEP with SHA-256 and empty label",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-OAEP", label: new Uint8Array([])},
+            hash: "SHA-256",
+            plaintext: plaintext.slice(0, 190),
+            ciphertext: ciphertext["sha-256, no label"]
+        },
+        {
+            name: "RSA-OAEP with SHA-384 and empty label",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-OAEP", label: new Uint8Array([])},
+            hash: "SHA-384",
+            plaintext: plaintext.slice(0, 158),
+            ciphertext: ciphertext["sha-384, no label"]
+        },
+        {
+            name: "RSA-OAEP with SHA-512 and empty label",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-OAEP", label: new Uint8Array([])},
+            hash: "SHA-512",
+            plaintext: plaintext.slice(0, 126),
+            ciphertext: ciphertext["sha-512, no label"]
+        },
+        {
+            name: "RSA-OAEP with SHA-1 and a label",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-OAEP", label: label},
+            hash: "SHA-1",
+            plaintext: plaintext.slice(0, 214),
+            ciphertext: ciphertext["sha-1, with label"]
+        },
+        {
+            name: "RSA-OAEP with SHA-256 and a label",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-OAEP", label: label},
+            hash: "SHA-256",
+            plaintext: plaintext.slice(0, 190),
+            ciphertext: ciphertext["sha-256, with label"]
+        },
+        {
+            name: "RSA-OAEP with SHA-384 and a label",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-OAEP", label: label},
+            hash: "SHA-384",
+            plaintext: plaintext.slice(0, 158),
+            ciphertext: ciphertext["sha-384, with label"]
+        },
+        {
+            name: "RSA-OAEP with SHA-512 and a label",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-OAEP", label: label},
+            hash: "SHA-512",
+            plaintext: plaintext.slice(0, 126),
+            ciphertext: ciphertext["sha-512, with label"]
+        }
+    ];
+
+    var failing = [];
+
+    return {passing: passing, failing: failing};
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/test_aes_cbc.https.html b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/test_aes_cbc.https.html
new file mode 100644
index 0000000..7a2805d
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/test_aes_cbc.https.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>WebCryptoAPI: encrypt() Using AES-CBC</title>
+<link rel="author" title="Charles Engelke" href="mailto:w3c@engelke.com">
+<link rel="help" href="https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#SubtleCrypto-method-encrypt">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script src="aes_cbc_vectors.js"></script>
+<script src="aes.js"></script>
+
+<h1>encrypt Tests for AES-CBC</h1>
+
+<div id="log"></div>
+<script>
+run_test();
+</script>
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/test_aes_ctr.https.html b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/test_aes_ctr.https.html
new file mode 100644
index 0000000..a48dfd3
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/test_aes_ctr.https.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>WebCryptoAPI: encrypt() Using AES-CTR</title>
+<link rel="author" title="Charles Engelke" href="mailto:w3c@engelke.com">
+<link rel="help" href="https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#SubtleCrypto-method-encrypt">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script src="aes_ctr_vectors.js"></script>
+<script src="aes.js"></script>
+
+<h1>encrypt Tests for AES-CTR</h1>
+
+<div id="log"></div>
+<script>
+run_test();
+</script>
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/test_aes_gcm.https.html b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/test_aes_gcm.https.html
new file mode 100644
index 0000000..9f8b8bd
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/test_aes_gcm.https.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>WebCryptoAPI: encrypt() Using AES-GCM</title>
+<link rel="author" title="Charles Engelke" href="mailto:w3c@engelke.com">
+<link rel="help" href="https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#SubtleCrypto-method-encrypt">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script src="aes_gcm_vectors.js"></script>
+<script src="aes.js"></script>
+
+<h1>encrypt Tests for AES-GCM</h1>
+
+<div id="log"></div>
+<script>
+run_test();
+</script>
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/test_rsa_oaep.https.html b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/test_rsa_oaep.https.html
new file mode 100644
index 0000000..df8d8f4
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/encrypt_decrypt/test_rsa_oaep.https.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>WebCryptoAPI: encrypt() Using RSA-OAEP</title>
+<link rel="author" title="Charles Engelke" href="mailto:w3c@engelke.com">
+<link rel="help" href="https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#SubtleCrypto-method-encrypt">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script src="rsa_vectors.js"></script>
+<script src="rsa.js"></script>
+
+<h1>encrypt Tests for RSA-OAEP</h1>
+
+<div id="log"></div>
+<script>
+run_test();
+</script>
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures.js
new file mode 100644
index 0000000..23fb280
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures.js
@@ -0,0 +1,216 @@
+function run_test(algorithmNames) {
+    var subtle = crypto.subtle; // Change to test prefixed implementations
+
+    setup({explicit_timeout: true});
+
+// These tests check that generateKey throws an error, and that
+// the error is of the right type, for a wide set of incorrect parameters.
+//
+// Error testing occurs by setting the parameter that should trigger the
+// error to an invalid value, then combining that with all valid
+// parameters that should be checked earlier by generateKey, and all
+// valid and invalid parameters that should be checked later by
+// generateKey.
+//
+// There are a lot of combinations of possible parameters for both
+// success and failure modes, resulting in a very large number of tests
+// performed.
+
+
+// Setup: define the correct behaviors that should be sought, and create
+// helper functions that generate all possible test parameters for
+// different situations.
+
+    var allTestVectors = [ // Parameters that should work for generateKey
+        {name: "AES-CTR",  resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
+        {name: "AES-CBC",  resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
+        {name: "AES-GCM",  resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
+        {name: "AES-KW",   resultType: CryptoKey, usages: ["wrapKey", "unwrapKey"], mandatoryUsages: []},
+        {name: "HMAC",     resultType: CryptoKey, usages: ["sign", "verify"], mandatoryUsages: []},
+        {name: "RSASSA-PKCS1-v1_5", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
+        {name: "RSA-PSS",  resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
+        {name: "RSA-OAEP", resultType: "CryptoKeyPair", usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: ["decrypt", "unwrapKey"]},
+        {name: "ECDSA",    resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
+        {name: "ECDH",     resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]}
+    ];
+
+    var testVectors = [];
+    if (algorithmNames && !Array.isArray(algorithmNames)) {
+        algorithmNames = [algorithmNames];
+    };
+    allTestVectors.forEach(function(vector) {
+        if (!algorithmNames || algorithmNames.includes(vector.name)) {
+            testVectors.push(vector);
+        }
+    });
+
+
+    function parameterString(algorithm, extractable, usages) {
+        if (typeof algorithm !== "object" && typeof algorithm !== "string") {
+            alert(algorithm);
+        }
+
+        var result = "(" +
+                        objectToString(algorithm) + ", " +
+                        objectToString(extractable) + ", " +
+                        objectToString(usages) +
+                     ")";
+
+        return result;
+    }
+
+    // Test that a given combination of parameters results in an error,
+    // AND that it is the correct kind of error.
+    //
+    // Expected error is either a number, tested against the error code,
+    // or a string, tested against the error name.
+    function testError(algorithm, extractable, usages, expectedError, testTag) {
+        promise_test(function(test) {
+            return crypto.subtle.generateKey(algorithm, extractable, usages)
+            .then(function(result) {
+                assert_unreached("Operation succeeded, but should not have");
+            }, function(err) {
+                if (typeof expectedError === "number") {
+                    assert_equals(err.code, expectedError, testTag + " not supported");
+                } else {
+                    assert_equals(err.name, expectedError, testTag + " not supported");
+                }
+            });
+        }, testTag + ": generateKey" + parameterString(algorithm, extractable, usages));
+    }
+
+
+    // Given an algorithm name, create several invalid parameters.
+    function badAlgorithmPropertySpecifiersFor(algorithmName) {
+        var results = [];
+
+        if (algorithmName.toUpperCase().substring(0, 3) === "AES") {
+            // Specifier properties are name and length
+            [64, 127, 129, 255, 257, 512].forEach(function(length) {
+                results.push({name: algorithmName, length: length});
+            });
+        } else if (algorithmName.toUpperCase().substring(0, 3) === "RSA") {
+            [new Uint8Array([1]), new Uint8Array([1,0,0])].forEach(function(publicExponent) {
+                results.push({name: algorithmName, hash: "SHA-256", modulusLength: 1024, publicExponent: publicExponent});
+            });
+        } else if (algorithmName.toUpperCase().substring(0, 2) === "EC") {
+            ["P-512", "Curve25519"].forEach(function(curveName) {
+                results.push({name: algorithmName, namedCurve: curveName});
+            });
+        }
+
+        return results;
+    }
+
+
+    // Don't create an exhaustive list of all invalid usages,
+    // because there would usually be nearly 2**8 of them,
+    // way too many to test. Instead, create every singleton
+    // of an illegal usage, and "poison" every valid usage
+    // with an illegal one.
+    function invalidUsages(validUsages, mandatoryUsages) {
+        var results = [];
+
+        var illegalUsages = [];
+        ["encrypt", "decrypt", "sign", "verify", "wrapKey", "unwrapKey", "deriveKey", "deriveBits"].forEach(function(usage) {
+            if (!validUsages.includes(usage)) {
+                illegalUsages.push(usage);
+            }
+        });
+
+        var goodUsageCombinations = allValidUsages(validUsages, false, mandatoryUsages);
+
+        illegalUsages.forEach(function(illegalUsage) {
+            results.push([illegalUsage]);
+            goodUsageCombinations.forEach(function(usageCombination) {
+                results.push(usageCombination.concat([illegalUsage]));
+            });
+        });
+
+        return results;
+    }
+
+
+// Now test for properly handling errors
+// - Unsupported algorithm
+// - Bad usages for algorithm
+// - Bad key lengths
+
+    // Algorithm normalization should fail with "Not supported"
+    var badAlgorithmNames = [
+        "AES",
+        {name: "AES"},
+        {name: "AES", length: 128},
+        {name: "AES-CMAC", length: 128},    // Removed after CR
+        {name: "AES-CFB", length: 128},      // Removed after CR
+        {name: "HMAC", hash: "MD5"},
+        {name: "RSA", hash: "SHA-256", modulusLength: 2048, publicExponent: new Uint8Array([1,0,1])},
+        {name: "RSA-PSS", hash: "SHA", modulusLength: 2048, publicExponent: new Uint8Array([1,0,1])},
+        {name: "EC", namedCurve: "P521"}
+    ];
+
+
+    // Algorithm normalization failures should be found first
+    // - all other parameters can be good or bad, should fail
+    //   due to NotSupportedError.
+    badAlgorithmNames.forEach(function(algorithm) {
+        allValidUsages(["decrypt", "sign", "deriveBits"], true, []) // Small search space, shouldn't matter because should fail before used
+        .forEach(function(usages) {
+            [false, true, "RED", 7].forEach(function(extractable){
+                testError(algorithm, extractable, usages, "NotSupportedError", "Bad algorithm");
+            });
+        });
+    });
+
+
+    // Algorithms normalize okay, but usages bad (though not empty).
+    // It shouldn't matter what other extractable is. Should fail
+    // due to SyntaxError
+    testVectors.forEach(function(vector) {
+        var name = vector.name;
+
+        allAlgorithmSpecifiersFor(name).forEach(function(algorithm) {
+            invalidUsages(vector.usages, vector.mandatoryUsages).forEach(function(usages) {
+                [true].forEach(function(extractable) {
+                    testError(algorithm, extractable, usages, "SyntaxError", "Bad usages");
+                });
+            });
+        });
+    });
+
+
+    // Other algorithm properties should be checked next, so try good
+    // algorithm names and usages, but bad algorithm properties next.
+    // - Special case: normally bad usage [] isn't checked until after properties,
+    //   so it's included in this test case. It should NOT cause an error.
+    testVectors.forEach(function(vector) {
+        var name = vector.name;
+        badAlgorithmPropertySpecifiersFor(name).forEach(function(algorithm) {
+            allValidUsages(vector.usages, true, vector.mandatoryUsages)
+            .forEach(function(usages) {
+                [false, true].forEach(function(extractable) {
+                    if (name.substring(0,2) === "EC") {
+                        testError(algorithm, extractable, usages, "NotSupportedError", "Bad algorithm property");
+                    } else {
+                        testError(algorithm, extractable, usages, "OperationError", "Bad algorithm property");
+                    }
+                });
+            });
+        });
+    });
+
+
+    // The last thing that should be checked is an empty usages (for secret keys).
+    testVectors.forEach(function(vector) {
+        var name = vector.name;
+
+        allAlgorithmSpecifiersFor(name).forEach(function(algorithm) {
+            var usages = [];
+            [false, true].forEach(function(extractable) {
+                testError(algorithm, extractable, usages, "SyntaxError", "Empty usages");
+            });
+        });
+    });
+
+
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_AES-CBC.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_AES-CBC.https.any.js
new file mode 100644
index 0000000..38bed1c
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_AES-CBC.https.any.js
@@ -0,0 +1,5 @@
+// META: title=WebCryptoAPI: generateKey() for Failures
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=failures.js
+run_test(["AES-CBC"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_AES-CTR.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_AES-CTR.https.any.js
new file mode 100644
index 0000000..0e79407
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_AES-CTR.https.any.js
@@ -0,0 +1,5 @@
+// META: title=WebCryptoAPI: generateKey() for Failures
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=failures.js
+run_test(["AES-CTR"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_AES-GCM.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_AES-GCM.https.any.js
new file mode 100644
index 0000000..a394c8b
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_AES-GCM.https.any.js
@@ -0,0 +1,5 @@
+// META: title=WebCryptoAPI: generateKey() for Failures
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=failures.js
+run_test(["AES-GCM"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_AES-KW.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_AES-KW.https.any.js
new file mode 100644
index 0000000..40c199b
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_AES-KW.https.any.js
@@ -0,0 +1,5 @@
+// META: title=WebCryptoAPI: generateKey() for Failures
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=failures.js
+run_test(["AES-KW"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_ECDH.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_ECDH.https.any.js
new file mode 100644
index 0000000..e522254
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_ECDH.https.any.js
@@ -0,0 +1,5 @@
+// META: title=WebCryptoAPI: generateKey() for Failures
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=failures.js
+run_test(["ECDH"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_ECDSA.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_ECDSA.https.any.js
new file mode 100644
index 0000000..e19974f
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_ECDSA.https.any.js
@@ -0,0 +1,5 @@
+// META: title=WebCryptoAPI: generateKey() for Failures
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=failures.js
+run_test(["ECDSA"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_HMAC.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_HMAC.https.any.js
new file mode 100644
index 0000000..43ce1c0
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_HMAC.https.any.js
@@ -0,0 +1,5 @@
+// META: title=WebCryptoAPI: generateKey() for Failures
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=failures.js
+run_test(["HMAC"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_RSA-OAEP.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_RSA-OAEP.https.any.js
new file mode 100644
index 0000000..1d2bca9
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_RSA-OAEP.https.any.js
@@ -0,0 +1,5 @@
+// META: title=WebCryptoAPI: generateKey() for Failures
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=failures.js
+run_test(["RSA-OAEP"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_RSA-PSS.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_RSA-PSS.https.any.js
new file mode 100644
index 0000000..562f666
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_RSA-PSS.https.any.js
@@ -0,0 +1,5 @@
+// META: title=WebCryptoAPI: generateKey() for Failures
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=failures.js
+run_test(["RSA-PSS"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_RSASSA-PKCS1-v1_5.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_RSASSA-PKCS1-v1_5.https.any.js
new file mode 100644
index 0000000..fb19308
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/failures_RSASSA-PKCS1-v1_5.https.any.js
@@ -0,0 +1,5 @@
+// META: title=WebCryptoAPI: generateKey() for Failures
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=failures.js
+run_test(["RSASSA-PKCS1-v1_5"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes.js
new file mode 100644
index 0000000..b99e44d
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes.js
@@ -0,0 +1,87 @@
+
+function run_test(algorithmNames, slowTest) {
+    var subtle = crypto.subtle; // Change to test prefixed implementations
+
+    setup({explicit_timeout: true});
+
+// These tests check that generateKey successfully creates keys
+// when provided any of a wide set of correct parameters.
+//
+// There are a lot of combinations of possible parameters,
+// resulting in a very large number of tests
+// performed.
+
+
+// Setup: define the correct behaviors that should be sought, and create
+// helper functions that generate all possible test parameters for
+// different situations.
+
+    var allTestVectors = [ // Parameters that should work for generateKey
+        {name: "AES-CTR",  resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
+        {name: "AES-CBC",  resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
+        {name: "AES-GCM",  resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
+        {name: "AES-KW",   resultType: CryptoKey, usages: ["wrapKey", "unwrapKey"], mandatoryUsages: []},
+        {name: "HMAC",     resultType: CryptoKey, usages: ["sign", "verify"], mandatoryUsages: []},
+        {name: "RSASSA-PKCS1-v1_5", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
+        {name: "RSA-PSS",  resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
+        {name: "RSA-OAEP", resultType: "CryptoKeyPair", usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: ["decrypt", "unwrapKey"]},
+        {name: "ECDSA",    resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
+        {name: "ECDH",     resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]}
+    ];
+
+    var testVectors = [];
+    if (algorithmNames && !Array.isArray(algorithmNames)) {
+        algorithmNames = [algorithmNames];
+    };
+    allTestVectors.forEach(function(vector) {
+        if (!algorithmNames || algorithmNames.includes(vector.name)) {
+            testVectors.push(vector);
+        }
+    });
+
+    function parameterString(algorithm, extractable, usages) {
+        var result = "(" +
+                        objectToString(algorithm) + ", " +
+                        objectToString(extractable) + ", " +
+                        objectToString(usages) +
+                     ")";
+
+        return result;
+    }
+
+    // Test that a given combination of parameters is successful
+    function testSuccess(algorithm, extractable, usages, resultType, testTag) {
+        // algorithm, extractable, and usages are the generateKey parameters
+        // resultType is the expected result, either the CryptoKey object or "CryptoKeyPair"
+        // testTag is a string to prepend to the test name.
+
+        promise_test(function(test) {
+            return subtle.generateKey(algorithm, extractable, usages)
+            .then(function(result) {
+                if (resultType === "CryptoKeyPair") {
+                    assert_goodCryptoKey(result.privateKey, algorithm, extractable, usages, "private");
+                    assert_goodCryptoKey(result.publicKey, algorithm, extractable, usages, "public");
+                } else {
+                    assert_goodCryptoKey(result, algorithm, extractable, usages, "secret");
+                }
+            }, function(err) {
+                assert_unreached("Threw an unexpected error: " + err.toString());
+            });
+        }, testTag + ": generateKey" + parameterString(algorithm, extractable, usages));
+    }
+
+    // Test all valid sets of parameters for successful
+    // key generation.
+    testVectors.forEach(function(vector) {
+        allNameVariants(vector.name, slowTest).forEach(function(name) {
+            allAlgorithmSpecifiersFor(name).forEach(function(algorithm) {
+                allValidUsages(vector.usages, false, vector.mandatoryUsages).forEach(function(usages) {
+                    [false, true].forEach(function(extractable) {
+                        subsetTest(testSuccess, algorithm, extractable, usages, vector.resultType, "Success");
+                    });
+                });
+            });
+        });
+    });
+
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_AES-CBC.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_AES-CBC.https.any.js
new file mode 100644
index 0000000..80f92c2
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_AES-CBC.https.any.js
@@ -0,0 +1,6 @@
+// META: title=WebCryptoAPI: generateKey() Successful Calls
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=/common/subset-tests.js
+// META: script=successes.js
+run_test(["AES-CBC"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_AES-CTR.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_AES-CTR.https.any.js
new file mode 100644
index 0000000..243a104
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_AES-CTR.https.any.js
@@ -0,0 +1,6 @@
+// META: title=WebCryptoAPI: generateKey() Successful Calls
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=/common/subset-tests.js
+// META: script=successes.js
+run_test(["AES-CTR"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_AES-GCM.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_AES-GCM.https.any.js
new file mode 100644
index 0000000..f0f947c
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_AES-GCM.https.any.js
@@ -0,0 +1,6 @@
+// META: title=WebCryptoAPI: generateKey() Successful Calls
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=/common/subset-tests.js
+// META: script=successes.js
+run_test(["AES-GCM"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_AES-KW.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_AES-KW.https.any.js
new file mode 100644
index 0000000..dbc040f
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_AES-KW.https.any.js
@@ -0,0 +1,6 @@
+// META: title=WebCryptoAPI: generateKey() Successful Calls
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=/common/subset-tests.js
+// META: script=successes.js
+run_test(["AES-KW"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_ECDH.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_ECDH.https.any.js
new file mode 100644
index 0000000..e9dee52
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_ECDH.https.any.js
@@ -0,0 +1,6 @@
+// META: title=WebCryptoAPI: generateKey() Successful Calls
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=/common/subset-tests.js
+// META: script=successes.js
+run_test(["ECDH"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_ECDSA.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_ECDSA.https.any.js
new file mode 100644
index 0000000..a022f31
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_ECDSA.https.any.js
@@ -0,0 +1,6 @@
+// META: title=WebCryptoAPI: generateKey() Successful Calls
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=/common/subset-tests.js
+// META: script=successes.js
+run_test(["ECDSA"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_HMAC.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_HMAC.https.any.js
new file mode 100644
index 0000000..18e0b27
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_HMAC.https.any.js
@@ -0,0 +1,6 @@
+// META: title=WebCryptoAPI: generateKey() Successful Calls
+// META: timeout=long
+// META: script=../util/helpers.js
+// META: script=/common/subset-tests.js
+// META: script=successes.js
+run_test(["HMAC"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_RSA-OAEP.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_RSA-OAEP.https.any.js
new file mode 100644
index 0000000..d933fd9
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_RSA-OAEP.https.any.js
@@ -0,0 +1,22 @@
+// META: title=WebCryptoAPI: generateKey() Successful Calls
+// META: timeout=long
+// META: variant=?1-10
+// META: variant=?11-20
+// META: variant=?21-30
+// META: variant=?31-40
+// META: variant=?41-50
+// META: variant=?51-60
+// META: variant=?61-70
+// META: variant=?71-80
+// META: variant=?81-90
+// META: variant=?91-100
+// META: variant=?101-110
+// META: variant=?111-120
+// META: variant=?121-130
+// META: variant=?131-140
+// META: variant=?141-150
+// META: variant=?151-last
+// META: script=../util/helpers.js
+// META: script=/common/subset-tests.js
+// META: script=successes.js
+run_test(["RSA-OAEP"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_RSA-PSS.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_RSA-PSS.https.any.js
new file mode 100644
index 0000000..cb43e3d
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_RSA-PSS.https.any.js
@@ -0,0 +1,10 @@
+// META: title=WebCryptoAPI: generateKey() Successful Calls
+// META: timeout=long
+// META: variant=?1-10
+// META: variant=?11-20
+// META: variant=?21-30
+// META: variant=?31-last
+// META: script=../util/helpers.js
+// META: script=/common/subset-tests.js
+// META: script=successes.js
+run_test(["RSA-PSS"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_RSASSA-PKCS1-v1_5.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_RSASSA-PKCS1-v1_5.https.any.js
new file mode 100644
index 0000000..b8db597
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/generateKey/successes_RSASSA-PKCS1-v1_5.https.any.js
@@ -0,0 +1,10 @@
+// META: title=WebCryptoAPI: generateKey() Successful Calls
+// META: timeout=long
+// META: variant=?1-10
+// META: variant=?11-20
+// META: variant=?21-30
+// META: variant=?31-last
+// META: script=../util/helpers.js
+// META: script=/common/subset-tests.js
+// META: script=successes.js
+run_test(["RSASSA-PKCS1-v1_5"]);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/getRandomValues.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/getRandomValues.any.js
new file mode 100644
index 0000000..473d57f
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/getRandomValues.any.js
@@ -0,0 +1,48 @@
+// Step 1.
+test(function() {
+    assert_throws_dom("TypeMismatchError", function() {
+        self.crypto.getRandomValues(new Float32Array(6))
+    }, "Float32Array")
+    assert_throws_dom("TypeMismatchError", function() {
+        self.crypto.getRandomValues(new Float64Array(6))
+    }, "Float64Array")
+
+    assert_throws_dom("TypeMismatchError", function() {
+        self.crypto.getRandomValues(new Float32Array(65537))
+    }, "Float32Array (too long)")
+    assert_throws_dom("TypeMismatchError", function() {
+        self.crypto.getRandomValues(new Float64Array(65537))
+    }, "Float64Array (too long)")
+}, "Float arrays")
+
+var arrays = {
+    'Int8Array': Int8Array,
+    'Int16Array': Int16Array,
+    'Int32Array': Int32Array,
+    'Uint8Array': Uint8Array,
+    'Uint8ClampedArray': Uint8ClampedArray,
+    'Uint16Array': Uint16Array,
+    'Uint32Array': Uint32Array,
+};
+
+test(function() {
+    for (var array in arrays) {
+        assert_equals(self.crypto.getRandomValues(new arrays[array](8)).constructor,
+                      arrays[array], "crypto.getRandomValues(new " + array + "(8))")
+    }
+}, "Integer array")
+
+test(function() {
+    for (var array in arrays) {
+        var maxlength = 65536 / (arrays[array].BYTES_PER_ELEMENT);
+        assert_throws_dom("QuotaExceededError", function() {
+            self.crypto.getRandomValues(new arrays[array](maxlength + 1))
+        }, "crypto.getRandomValues length over 65536")
+    }
+}, "Large length")
+
+test(function() {
+    for (var array in arrays) {
+        assert_true(self.crypto.getRandomValues(new arrays[array](0)).length == 0)
+    }
+}, "Null arrays")
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/getRandomValues.js b/src/third_party/web_platform_tests/WebCryptoAPI/getRandomValues.js
deleted file mode 100644
index 0649d95..0000000
--- a/src/third_party/web_platform_tests/WebCryptoAPI/getRandomValues.js
+++ /dev/null
@@ -1,50 +0,0 @@
-function run_test() {
-    // Step 1.
-    test(function() {
-        assert_throws("TypeMismatchError", function() {
-            self.crypto.getRandomValues(new Float32Array(6))
-        }, "Float32Array")
-        assert_throws("TypeMismatchError", function() {
-            self.crypto.getRandomValues(new Float64Array(6))
-        }, "Float64Array")
-
-        assert_throws("TypeMismatchError", function() {
-            self.crypto.getRandomValues(new Float32Array(65537))
-        }, "Float32Array (too long)")
-        assert_throws("TypeMismatchError", function() {
-            self.crypto.getRandomValues(new Float64Array(65537))
-        }, "Float64Array (too long)")
-    }, "Float arrays")
-
-    var arrays = {
-        'Int8Array': Int8Array,
-        'Int16Array': Int16Array,
-        'Int32Array': Int32Array,
-        'Uint8Array': Uint8Array,
-        'Uint8ClampedArray': Uint8ClampedArray,
-        'Uint16Array': Uint16Array,
-        'Uint32Array': Uint32Array,
-    };
-
-    test(function() {
-        for (var array in arrays) {
-            assert_equals(self.crypto.getRandomValues(new arrays[array](8)).constructor,
-                          arrays[array], "crypto.getRandomValues(new " + array + "(8))")
-        }
-    }, "Integer array")
-
-    test(function() {
-        for (var array in arrays) {
-            var maxlength = 65536 / (arrays[array].BYTES_PER_ELEMENT);
-            assert_throws("QuotaExceededError", function() {
-                self.crypto.getRandomValues(new arrays[array](maxlength + 1))
-            }, "crypto.getRandomValues length over 65536")
-        }
-    }, "Large length")
-
-    test(function() {
-        for (var array in arrays) {
-            assert_true(self.crypto.getRandomValues(new arrays[array](0)).length == 0)
-        }
-    }, "Null arrays")
-}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/historical.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/historical.any.js
new file mode 100644
index 0000000..67ca9d9
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/historical.any.js
@@ -0,0 +1,18 @@
+// META: global=window,dedicatedworker,sharedworker
+//
+// Do not run this in a service worker as that's always in a secure context
+
+test(() => {
+  assert_equals(self.crypto.subtle, undefined);
+  assert_false("subtle" in self.crypto);
+}, "Non-secure context window does not have access to crypto.subtle");
+
+test(() => {
+  assert_equals(self.SubtleCrypto, undefined);
+  assert_false("SubtleCrypto" in self);
+}, "Non-secure context window does not have access to SubtleCrypto")
+
+test(() => {
+  assert_equals(self.CryptoKey, undefined);
+  assert_false("CryptoKey" in self);
+}, "Non-secure context window does not have access to CryptoKey")
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/idlharness.https.any.js b/src/third_party/web_platform_tests/WebCryptoAPI/idlharness.https.any.js
new file mode 100644
index 0000000..b0ccebd
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/idlharness.https.any.js
@@ -0,0 +1,15 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+// https://w3c.github.io/webcrypto/Overview.html
+
+idl_test(
+  ['WebCryptoAPI'],
+  ['html', 'dom'],
+  idl_array => {
+    idl_array.add_objects({
+      Crypto: ['crypto'],
+      SubtleCrypto: ['crypto.subtle']
+    });
+  }
+);
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/getRandomValues.worker.js b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/ec_importKey.https.worker.js
similarity index 63%
copy from src/third_party/web_platform_tests/WebCryptoAPI/getRandomValues.worker.js
copy to src/third_party/web_platform_tests/WebCryptoAPI/import_export/ec_importKey.https.worker.js
index cc26356..698df52 100644
--- a/src/third_party/web_platform_tests/WebCryptoAPI/getRandomValues.worker.js
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/ec_importKey.https.worker.js
@@ -1,4 +1,4 @@
 importScripts("/resources/testharness.js");
-importScripts("getRandomValues.js");
+importScripts("ec_importKey.js");
 run_test();
 done();
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/import_export/ec_importKey.js b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/ec_importKey.js
new file mode 100644
index 0000000..277851d
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/ec_importKey.js
@@ -0,0 +1,279 @@
+// Test importKey and exportKey for EC algorithms. Only "happy paths" are
+// currently tested - those where the operation should succeed.
+
+function run_test() {
+    var subtle = crypto.subtle;
+
+    var curves = ['P-256', 'P-384', 'P-521'];
+
+    var keyData = {
+        "P-521": {
+            spki: new Uint8Array([48, 129, 155, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 35, 3, 129, 134, 0, 4, 1, 86, 244, 121, 248, 223, 30, 32, 167, 255, 192, 76, 228, 32, 195, 225, 84, 174, 37, 25, 150, 190, 228, 47, 3, 75, 132, 212, 27, 116, 63, 52, 228, 95, 49, 27, 129, 58, 156, 222, 200, 205, 165, 155, 187, 189, 49, 212, 96, 179, 41, 37, 33, 231, 193, 183, 34, 229, 102, 124, 3, 219, 47, 174, 117, 63, 1, 80, 23, 54, 207, 226, 71, 57, 67, 32, 216, 228, 175, 194, 253, 57, 181, 169, 51, 16, 97, 184, 30, 34, 65, 40, 43, 158, 23, 137, 24, 34, 181, 183, 158, 5, 47, 69, 151, 181, 150, 67, 253, 57, 55, 156, 81, 189, 81, 37, 196, 244, 139, 195, 240, 37, 206, 60, 211, 105, 83, 40, 108, 203, 56, 251]),
+            pkcs8: new Uint8Array([48, 129, 238, 2, 1, 0, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 35, 4, 129, 214, 48, 129, 211, 2, 1, 1, 4, 66, 0, 244, 8, 117, 131, 104, 186, 147, 15, 48, 247, 106, 224, 84, 254, 92, 210, 206, 127, 218, 44, 159, 118, 166, 212, 54, 207, 117, 214, 108, 68, 11, 254, 99, 49, 199, 193, 114, 161, 36, 120, 25, 60, 130, 81, 72, 123, 201, 18, 99, 250, 80, 33, 127, 133, 255, 99, 111, 89, 205, 84, 110, 58, 180, 131, 180, 161, 129, 137, 3, 129, 134, 0, 4, 1, 86, 244, 121, 248, 223, 30, 32, 167, 255, 192, 76, 228, 32, 195, 225, 84, 174, 37, 25, 150, 190, 228, 47, 3, 75, 132, 212, 27, 116, 63, 52, 228, 95, 49, 27, 129, 58, 156, 222, 200, 205, 165, 155, 187, 189, 49, 212, 96, 179, 41, 37, 33, 231, 193, 183, 34, 229, 102, 124, 3, 219, 47, 174, 117, 63, 1, 80, 23, 54, 207, 226, 71, 57, 67, 32, 216, 228, 175, 194, 253, 57, 181, 169, 51, 16, 97, 184, 30, 34, 65, 40, 43, 158, 23, 137, 24, 34, 181, 183, 158, 5, 47, 69, 151, 181, 150, 67, 253, 57, 55, 156, 81, 189, 81, 37, 196, 244, 139, 195, 240, 37, 206, 60, 211, 105, 83, 40, 108, 203, 56, 251]),
+            jwk: {
+                kty: "EC",
+                crv: "P-521",
+                x: "AVb0efjfHiCn_8BM5CDD4VSuJRmWvuQvA0uE1Bt0PzTkXzEbgTqc3sjNpZu7vTHUYLMpJSHnwbci5WZ8A9svrnU_",
+                y: "AVAXNs_iRzlDINjkr8L9ObWpMxBhuB4iQSgrnheJGCK1t54FL0WXtZZD_Tk3nFG9USXE9IvD8CXOPNNpUyhsyzj7",
+                d: "APQIdYNoupMPMPdq4FT-XNLOf9osn3am1DbPddZsRAv-YzHHwXKhJHgZPIJRSHvJEmP6UCF_hf9jb1nNVG46tIO0"
+            }
+        },
+
+        "P-256": {
+            spki: new Uint8Array([48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 210, 16, 176, 166, 249, 217, 240, 18, 134, 128, 88, 180, 63, 164, 244, 113, 1, 133, 67, 187, 160, 12, 146, 80, 223, 146, 87, 194, 172, 174, 93, 209, 206, 3, 117, 82, 212, 129, 69, 12, 227, 155, 77, 16, 149, 112, 27, 23, 91, 250, 179, 75, 142, 108, 9, 158, 24, 241, 193, 152, 53, 131, 97, 232]),
+            pkcs8: new Uint8Array([48, 129, 135, 2, 1, 0, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 4, 109, 48, 107, 2, 1, 1, 4, 32, 19, 211, 58, 45, 90, 191, 156, 249, 235, 178, 31, 248, 96, 212, 174, 254, 110, 86, 231, 119, 144, 244, 222, 233, 180, 8, 132, 235, 211, 53, 68, 234, 161, 68, 3, 66, 0, 4, 210, 16, 176, 166, 249, 217, 240, 18, 134, 128, 88, 180, 63, 164, 244, 113, 1, 133, 67, 187, 160, 12, 146, 80, 223, 146, 87, 194, 172, 174, 93, 209, 206, 3, 117, 82, 212, 129, 69, 12, 227, 155, 77, 16, 149, 112, 27, 23, 91, 250, 179, 75, 142, 108, 9, 158, 24, 241, 193, 152, 53, 131, 97, 232]),
+            jwk: {
+                kty: "EC",
+                crv: "P-256",
+                x: "0hCwpvnZ8BKGgFi0P6T0cQGFQ7ugDJJQ35JXwqyuXdE",
+                y: "zgN1UtSBRQzjm00QlXAbF1v6s0uObAmeGPHBmDWDYeg",
+                d: "E9M6LVq_nPnrsh_4YNSu_m5W53eQ9N7ptAiE69M1ROo"
+            }
+        },
+
+        "P-384": {
+            spki: new Uint8Array([48, 118, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 34, 3, 98, 0, 4, 33, 156, 20, 214, 102, 23, 179, 110, 198, 216, 133, 107, 56, 91, 115, 167, 77, 52, 79, 216, 174, 117, 239, 4, 100, 53, 221, 165, 78, 59, 68, 189, 95, 189, 235, 209, 208, 141, 214, 158, 45, 125, 193, 220, 33, 140, 180, 53, 189, 40, 19, 140, 199, 120, 51, 122, 132, 47, 107, 214, 27, 36, 14, 116, 36, 159, 36, 102, 124, 42, 88, 16, 167, 107, 252, 40, 224, 51, 95, 136, 166, 80, 29, 236, 1, 151, 109, 168, 90, 251, 0, 134, 156, 182, 172, 232]),
+            pkcs8: new Uint8Array([48, 129, 182, 2, 1, 0, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 34, 4, 129, 158, 48, 129, 155, 2, 1, 1, 4, 48, 69, 55, 181, 153, 7, 132, 211, 194, 210, 46, 150, 168, 249, 47, 161, 170, 73, 46, 232, 115, 229, 118, 164, 21, 130, 225, 68, 24, 60, 152, 136, 209, 14, 107, 158, 180, 206, 212, 178, 204, 64, 18, 228, 172, 94, 168, 64, 115, 161, 100, 3, 98, 0, 4, 33, 156, 20, 214, 102, 23, 179, 110, 198, 216, 133, 107, 56, 91, 115, 167, 77, 52, 79, 216, 174, 117, 239, 4, 100, 53, 221, 165, 78, 59, 68, 189, 95, 189, 235, 209, 208, 141, 214, 158, 45, 125, 193, 220, 33, 140, 180, 53, 189, 40, 19, 140, 199, 120, 51, 122, 132, 47, 107, 214, 27, 36, 14, 116, 36, 159, 36, 102, 124, 42, 88, 16, 167, 107, 252, 40, 224, 51, 95, 136, 166, 80, 29, 236, 1, 151, 109, 168, 90, 251, 0, 134, 156, 182, 172, 232]),
+            jwk: {
+                kty: "EC",
+                crv: "P-384",
+                x: "IZwU1mYXs27G2IVrOFtzp000T9iude8EZDXdpU47RL1fvevR0I3Wni19wdwhjLQ1",
+                y: "vSgTjMd4M3qEL2vWGyQOdCSfJGZ8KlgQp2v8KOAzX4imUB3sAZdtqFr7AIactqzo",
+                d: "RTe1mQeE08LSLpao-S-hqkku6HPldqQVguFEGDyYiNEOa560ztSyzEAS5KxeqEBz"
+            }
+        },
+
+    };
+
+    // combinations to test
+    var testVectors = [
+        {name: "ECDSA", privateUsages: ["sign"], publicUsages: ["verify"]},
+        {name: "ECDH",  privateUsages: ["deriveKey", "deriveBits"], publicUsages: []}
+    ];
+
+    // TESTS ARE HERE:
+    // Test every test vector, along with all available key data
+    testVectors.forEach(function(vector) {
+        curves.forEach(function(curve) {
+
+            [true, false].forEach(function(extractable) {
+
+                // Test public keys first
+                [[]].forEach(function(usages) { // Only valid usages argument is empty array
+                    ['spki', 'jwk'].forEach(function(format) {
+                        var algorithm = {name: vector.name, namedCurve: curve};
+                        var data = keyData[curve];
+                        if (format === "jwk") { // Not all fields used for public keys
+                            data = {jwk: {kty: keyData[curve].jwk.kty, crv: keyData[curve].jwk.crv, x: keyData[curve].jwk.x, y: keyData[curve].jwk.y}};
+                        }
+
+                        testFormat(format, algorithm, data, curve, usages, extractable);
+                    });
+
+                });
+
+                // Next, test private keys
+                allValidUsages(vector.privateUsages, []).forEach(function(usages) {
+                    ['pkcs8', 'jwk'].forEach(function(format) {
+                        var algorithm = {name: vector.name, namedCurve: curve};
+                        var data = keyData[curve];
+
+                        testFormat(format, algorithm, data, curve, usages, extractable);
+                    });
+                });
+            });
+
+        });
+    });
+
+
+    // Test importKey with a given key format and other parameters. If
+    // extrable is true, export the key and verify that it matches the input.
+    function testFormat(format, algorithm, keyData, keySize, usages, extractable) {
+        promise_test(function(test) {
+            return subtle.importKey(format, keyData[format], algorithm, extractable, usages).
+            then(function(key) {
+                assert_equals(key.constructor, CryptoKey, "Imported a CryptoKey object");
+                if (!extractable) {
+                    return;
+                }
+
+                return subtle.exportKey(format, key).
+                then(function(result) {
+                    if (format !== "jwk") {
+                        assert_true(equalBuffers(keyData[format], result), "Round trip works");
+                    } else {
+                        assert_true(equalJwk(keyData[format], result), "Round trip works");
+                    }
+                }, function(err) {
+                    assert_unreached("Threw an unexpected error: " + err.toString());
+                });
+            }, function(err) {
+                assert_unreached("Threw an unexpected error: " + err.toString());
+            });
+        }, "Good parameters: " + keySize.toString() + " bits " + parameterString(format, keyData[format], algorithm, extractable, usages));
+    }
+
+
+
+    // Helper methods follow:
+
+    // Are two array buffers the same?
+    function equalBuffers(a, b) {
+        if (a.byteLength !== b.byteLength) {
+            return false;
+        }
+
+        var aBytes = new Uint8Array(a);
+        var bBytes = new Uint8Array(b);
+
+        for (var i=0; i<a.byteLength; i++) {
+            if (aBytes[i] !== bBytes[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    // Are two Jwk objects "the same"? That is, does the object returned include
+    // matching values for each property that was expected? It's okay if the
+    // returned object has extra methods; they aren't checked.
+    function equalJwk(expected, got) {
+        var fields = Object.keys(expected);
+        var fieldName;
+
+        for(var i=0; i<fields.length; i++) {
+            fieldName = fields[i];
+            if (!(fieldName in got)) {
+                return false;
+            }
+            if (expected[fieldName] !== got[fieldName]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    // Build minimal Jwk objects from raw key data and algorithm specifications
+    function jwkData(keyData, algorithm) {
+        var result = {
+            kty: "oct",
+            k: byteArrayToUnpaddedBase64(keyData)
+        };
+
+        if (algorithm.name.substring(0, 3) === "AES") {
+            result.alg = "A" + (8 * keyData.byteLength).toString() + algorithm.name.substring(4);
+        } else if (algorithm.name === "HMAC") {
+            result.alg = "HS" + algorithm.hash.substring(4);
+        }
+        return result;
+    }
+
+    // Jwk format wants Base 64 without the typical padding at the end.
+    function byteArrayToUnpaddedBase64(byteArray){
+        var binaryString = "";
+        for (var i=0; i<byteArray.byteLength; i++){
+            binaryString += String.fromCharCode(byteArray[i]);
+        }
+        var base64String = btoa(binaryString);
+
+        return base64String.replace(/=/g, "");
+    }
+
+    // Want to test every valid combination of usages. Start by creating a list
+    // of all non-empty subsets to possible usages.
+    function allNonemptySubsetsOf(arr) {
+        var results = [];
+        var firstElement;
+        var remainingElements;
+
+        for(var i=0; i<arr.length; i++) {
+            firstElement = arr[i];
+            remainingElements = arr.slice(i+1);
+            results.push([firstElement]);
+
+            if (remainingElements.length > 0) {
+                allNonemptySubsetsOf(remainingElements).forEach(function(combination) {
+                    combination.push(firstElement);
+                    results.push(combination);
+                });
+            }
+        }
+
+        return results;
+    }
+
+    // Return a list of all valid usage combinations, given the possible ones
+    // and the ones that are required for a particular operation.
+    function allValidUsages(possibleUsages, requiredUsages) {
+        var allUsages = [];
+
+        allNonemptySubsetsOf(possibleUsages).forEach(function(usage) {
+            for (var i=0; i<requiredUsages.length; i++) {
+                if (!usage.includes(requiredUsages[i])) {
+                    return;
+                }
+            }
+            allUsages.push(usage);
+        });
+
+        return allUsages;
+    }
+
+    // Convert method parameters to a string to uniquely name each test
+    function parameterString(format, data, algorithm, extractable, usages) {
+        if ("byteLength" in data) {
+            data = "buffer(" + data.byteLength.toString() + ")";
+        } else {
+            data = "object(" + Object.keys(data).join(", ") + ")";
+        }
+        var result = "(" +
+                        objectToString(format) + ", " +
+                        objectToString(data) + ", " +
+                        objectToString(algorithm) + ", " +
+                        objectToString(extractable) + ", " +
+                        objectToString(usages) +
+                     ")";
+
+        return result;
+    }
+
+    // Character representation of any object we may use as a parameter.
+    function objectToString(obj) {
+        var keyValuePairs = [];
+
+        if (Array.isArray(obj)) {
+            return "[" + obj.map(function(elem){return objectToString(elem);}).join(", ") + "]";
+        } else if (typeof obj === "object") {
+            Object.keys(obj).sort().forEach(function(keyName) {
+                keyValuePairs.push(keyName + ": " + objectToString(obj[keyName]));
+            });
+            return "{" + keyValuePairs.join(", ") + "}";
+        } else if (typeof obj === "undefined") {
+            return "undefined";
+        } else {
+            return obj.toString();
+        }
+
+        var keyValuePairs = [];
+
+        Object.keys(obj).sort().forEach(function(keyName) {
+            var value = obj[keyName];
+            if (typeof value === "object") {
+                value = objectToString(value);
+            } else if (typeof value === "array") {
+                value = "[" + value.map(function(elem){return objectToString(elem);}).join(", ") + "]";
+            } else {
+                value = value.toString();
+            }
+
+            keyValuePairs.push(keyName + ": " + value);
+        });
+
+        return "{" + keyValuePairs.join(", ") + "}";
+    }
+
+    return; // from run_test
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/getRandomValues.worker.js b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/rsa_importKey.https.worker.js
similarity index 63%
rename from src/third_party/web_platform_tests/WebCryptoAPI/getRandomValues.worker.js
rename to src/third_party/web_platform_tests/WebCryptoAPI/import_export/rsa_importKey.https.worker.js
index cc26356..3b2435b 100644
--- a/src/third_party/web_platform_tests/WebCryptoAPI/getRandomValues.worker.js
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/rsa_importKey.https.worker.js
@@ -1,4 +1,4 @@
 importScripts("/resources/testharness.js");
-importScripts("getRandomValues.js");
+importScripts("rsa_importKey.js");
 run_test();
 done();
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/import_export/rsa_importKey.js b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/rsa_importKey.js
new file mode 100644
index 0000000..1d80f1c
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/rsa_importKey.js
@@ -0,0 +1,296 @@
+// Test importKey and exportKey for RSA algorithms. Only "happy paths" are
+// currently tested - those where the operation should succeed.
+
+function run_test() {
+    var subtle = crypto.subtle;
+
+    var sizes = [1024, 2048, 4096];
+
+    var hashes = ["SHA-1", "SHA-256", "SHA-384", "SHA-512"];
+
+    var keyData = {
+        1024: {
+            spki: new Uint8Array([48, 129, 159, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 129, 141, 0, 48, 129, 137, 2, 129, 129, 0, 205, 153, 248, 177, 17, 159, 141, 10, 44, 231, 172, 139, 253, 12, 181, 71, 211, 72, 249, 49, 204, 156, 92, 167, 159, 222, 32, 229, 28, 64, 235, 1, 171, 38, 30, 1, 37, 61, 241, 232, 143, 113, 208, 134, 233, 75, 122, 190, 119, 131, 145, 3, 164, 118, 190, 224, 204, 135, 199, 67, 21, 26, 253, 68, 49, 250, 93, 143, 160, 81, 39, 28, 245, 78, 73, 207, 117, 0, 216, 169, 149, 126, 192, 155, 157, 67, 239, 112, 9, 140, 87, 241, 13, 3, 191, 211, 23, 72, 175, 86, 59, 136, 22, 135, 114, 13, 60, 123, 16, 161, 205, 85, 58, 199, 29, 41, 107, 110, 222, 236, 165, 185, 156, 138, 251, 54, 221, 151, 2, 3, 1, 0, 1]),
+            pkcs8: new Uint8Array([48, 130, 2, 120, 2, 1, 0, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 4, 130, 2, 98, 48, 130, 2, 94, 2, 1, 0, 2, 129, 129, 0, 205, 153, 248, 177, 17, 159, 141, 10, 44, 231, 172, 139, 253, 12, 181, 71, 211, 72, 249, 49, 204, 156, 92, 167, 159, 222, 32, 229, 28, 64, 235, 1, 171, 38, 30, 1, 37, 61, 241, 232, 143, 113, 208, 134, 233, 75, 122, 190, 119, 131, 145, 3, 164, 118, 190, 224, 204, 135, 199, 67, 21, 26, 253, 68, 49, 250, 93, 143, 160, 81, 39, 28, 245, 78, 73, 207, 117, 0, 216, 169, 149, 126, 192, 155, 157, 67, 239, 112, 9, 140, 87, 241, 13, 3, 191, 211, 23, 72, 175, 86, 59, 136, 22, 135, 114, 13, 60, 123, 16, 161, 205, 85, 58, 199, 29, 41, 107, 110, 222, 236, 165, 185, 156, 138, 251, 54, 221, 151, 2, 3, 1, 0, 1, 2, 129, 128, 98, 162, 10, 252, 103, 71, 243, 145, 126, 25, 102, 93, 129, 248, 38, 191, 94, 77, 19, 191, 32, 57, 162, 249, 135, 104, 56, 191, 176, 222, 51, 223, 137, 11, 176, 57, 60, 116, 139, 40, 214, 39, 243, 177, 197, 25, 192, 184, 190, 253, 15, 4, 128, 81, 183, 32, 128, 254, 98, 73, 124, 70, 134, 88, 228, 85, 8, 229, 210, 6, 149, 141, 122, 147, 24, 166, 42, 57, 218, 125, 240, 230, 232, 249, 81, 145, 44, 6, 118, 237, 101, 205, 4, 181, 104, 85, 23, 96, 46, 169, 174, 213, 110, 34, 171, 89, 196, 20, 18, 1, 8, 241, 93, 32, 19, 144, 248, 183, 32, 96, 240, 101, 239, 247, 222, 249, 117, 1, 2, 65, 0, 244, 26, 192, 131, 146, 245, 205, 250, 134, 62, 229, 137, 14, 224, 194, 5, 127, 147, 154, 214, 93, 172, 226, 55, 98, 206, 25, 104, 223, 178, 48, 249, 83, 143, 5, 146, 16, 243, 180, 170, 119, 227, 17, 151, 48, 217, 88, 23, 30, 2, 73, 153, 181, 92, 163, 164, 241, 114, 66, 66, 152, 70, 42, 121, 2, 65, 0, 215, 158, 227, 12, 157, 88, 107, 153, 230, 66, 244, 207, 110, 18, 128, 60, 7, 140, 90, 136, 49, 11, 38, 144, 78, 64, 107, 167, 125, 41, 16, 167, 122, 152, 100, 129, 223, 206, 97, 170, 190, 1, 34, 79, 44, 221, 254, 204, 117, 122, 76, 249, 68, 169, 105, 152, 20, 161, 62, 40, 255, 101, 68, 143, 2, 65, 0, 169, 215, 127, 65, 76, 220, 104, 31, 186, 142, 66, 168, 213, 72, 62, 215, 18, 136, 2, 0, 203, 22, 194, 35, 37, 69, 31, 90, 223, 226, 28, 191, 45, 139, 98, 165, 217, 211, 167, 77, 192, 178, 166, 7, 155, 62, 110, 83, 79, 86, 234, 28, 223, 154, 128, 102, 0, 116, 174, 115, 165, 125, 148, 137, 2, 65, 0, 132, 212, 95, 192, 228, 169, 148, 215, 225, 46, 252, 75, 80, 222, 218, 218, 160, 55, 201, 137, 190, 212, 196, 179, 255, 80, 214, 64, 254, 236, 174, 82, 206, 70, 85, 28, 96, 248, 109, 216, 86, 102, 178, 113, 30, 13, 192, 42, 202, 112, 70, 61, 5, 28, 108, 109, 128, 191, 248, 96, 31, 61, 142, 103, 2, 65, 0, 205, 186, 73, 64, 8, 98, 158, 188, 82, 109, 82, 177, 5, 13, 132, 100, 97, 84, 15, 103, 183, 88, 37, 219, 0, 148, 88, 166, 79, 7, 85, 14, 64, 3, 157, 142, 132, 164, 226, 112, 236, 158, 218, 17, 7, 158, 184, 41, 20, 172, 194, 242, 44, 231, 78, 192, 134, 220, 83, 36, 191, 7, 35, 225]),
+            jwk: {
+                kty: "RSA",
+                n: "zZn4sRGfjQos56yL_Qy1R9NI-THMnFynn94g5RxA6wGrJh4BJT3x6I9x0IbpS3q-d4ORA6R2vuDMh8dDFRr9RDH6XY-gUScc9U5Jz3UA2KmVfsCbnUPvcAmMV_ENA7_TF0ivVjuIFodyDTx7EKHNVTrHHSlrbt7spbmcivs23Zc",
+                e: "AQAB",
+                d: "YqIK_GdH85F-GWZdgfgmv15NE78gOaL5h2g4v7DeM9-JC7A5PHSLKNYn87HFGcC4vv0PBIBRtyCA_mJJfEaGWORVCOXSBpWNepMYpio52n3w5uj5UZEsBnbtZc0EtWhVF2Auqa7VbiKrWcQUEgEI8V0gE5D4tyBg8GXv9975dQE",
+                p: "9BrAg5L1zfqGPuWJDuDCBX-TmtZdrOI3Ys4ZaN-yMPlTjwWSEPO0qnfjEZcw2VgXHgJJmbVco6TxckJCmEYqeQ",
+                q: "157jDJ1Ya5nmQvTPbhKAPAeMWogxCyaQTkBrp30pEKd6mGSB385hqr4BIk8s3f7MdXpM-USpaZgUoT4o_2VEjw",
+                dp: "qdd_QUzcaB-6jkKo1Ug-1xKIAgDLFsIjJUUfWt_iHL8ti2Kl2dOnTcCypgebPm5TT1bqHN-agGYAdK5zpX2UiQ",
+                dq: "hNRfwOSplNfhLvxLUN7a2qA3yYm-1MSz_1DWQP7srlLORlUcYPht2FZmsnEeDcAqynBGPQUcbG2Av_hgHz2OZw",
+                qi: "zbpJQAhinrxSbVKxBQ2EZGFUD2e3WCXbAJRYpk8HVQ5AA52OhKTicOye2hEHnrgpFKzC8iznTsCG3FMkvwcj4Q"
+            }
+        },
+
+        2048: {
+            spki: new Uint8Array([48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0, 48, 130, 1, 10, 2, 130, 1, 1, 0, 217, 133, 128, 235, 45, 23, 114, 244, 164, 118, 188, 84, 4, 190, 230, 13, 154, 60, 42, 203, 188, 242, 74, 116, 117, 77, 159, 90, 104, 18, 56, 143, 158, 63, 38, 10, 216, 22, 135, 221, 179, 102, 248, 218, 85, 148, 98, 179, 151, 241, 192, 151, 137, 109, 13, 246, 230, 222, 49, 192, 79, 141, 71, 205, 21, 96, 13, 17, 190, 78, 196, 230, 48, 158, 32, 4, 22, 37, 127, 171, 186, 139, 190, 211, 58, 176, 193, 101, 218, 60, 155, 31, 206, 194, 196, 233, 229, 42, 202, 99, 89, 167, 207, 84, 213, 39, 91, 68, 134, 191, 1, 162, 180, 95, 4, 250, 226, 11, 113, 125, 1, 167, 148, 87, 7, 40, 129, 82, 151, 178, 183, 242, 43, 224, 14, 243, 2, 56, 19, 202, 135, 183, 224, 190, 131, 67, 51, 92, 250, 240, 118, 158, 54, 108, 249, 37, 108, 244, 66, 57, 69, 139, 180, 126, 189, 107, 50, 240, 22, 137, 128, 103, 0, 146, 115, 247, 157, 69, 184, 91, 159, 51, 245, 115, 24, 223, 197, 175, 152, 26, 162, 150, 72, 52, 231, 245, 179, 48, 18, 211, 105, 100, 106, 103, 56, 178, 43, 202, 85, 229, 144, 102, 241, 230, 159, 106, 105, 241, 238, 222, 204, 232, 129, 183, 66, 63, 212, 77, 252, 122, 124, 152, 156, 66, 103, 65, 216, 129, 60, 63, 205, 192, 36, 181, 61, 132, 41, 10, 59, 237, 163, 200, 56, 114, 202, 253, 2, 3, 1, 0, 1]),
+            pkcs8: new Uint8Array([48, 130, 4, 190, 2, 1, 0, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 4, 130, 4, 168, 48, 130, 4, 164, 2, 1, 0, 2, 130, 1, 1, 0, 217, 133, 128, 235, 45, 23, 114, 244, 164, 118, 188, 84, 4, 190, 230, 13, 154, 60, 42, 203, 188, 242, 74, 116, 117, 77, 159, 90, 104, 18, 56, 143, 158, 63, 38, 10, 216, 22, 135, 221, 179, 102, 248, 218, 85, 148, 98, 179, 151, 241, 192, 151, 137, 109, 13, 246, 230, 222, 49, 192, 79, 141, 71, 205, 21, 96, 13, 17, 190, 78, 196, 230, 48, 158, 32, 4, 22, 37, 127, 171, 186, 139, 190, 211, 58, 176, 193, 101, 218, 60, 155, 31, 206, 194, 196, 233, 229, 42, 202, 99, 89, 167, 207, 84, 213, 39, 91, 68, 134, 191, 1, 162, 180, 95, 4, 250, 226, 11, 113, 125, 1, 167, 148, 87, 7, 40, 129, 82, 151, 178, 183, 242, 43, 224, 14, 243, 2, 56, 19, 202, 135, 183, 224, 190, 131, 67, 51, 92, 250, 240, 118, 158, 54, 108, 249, 37, 108, 244, 66, 57, 69, 139, 180, 126, 189, 107, 50, 240, 22, 137, 128, 103, 0, 146, 115, 247, 157, 69, 184, 91, 159, 51, 245, 115, 24, 223, 197, 175, 152, 26, 162, 150, 72, 52, 231, 245, 179, 48, 18, 211, 105, 100, 106, 103, 56, 178, 43, 202, 85, 229, 144, 102, 241, 230, 159, 106, 105, 241, 238, 222, 204, 232, 129, 183, 66, 63, 212, 77, 252, 122, 124, 152, 156, 66, 103, 65, 216, 129, 60, 63, 205, 192, 36, 181, 61, 132, 41, 10, 59, 237, 163, 200, 56, 114, 202, 253, 2, 3, 1, 0, 1, 2, 130, 1, 0, 90, 210, 167, 117, 138, 170, 83, 209, 90, 42, 73, 144, 59, 59, 10, 11, 123, 238, 203, 95, 174, 80, 236, 77, 155, 253, 1, 32, 90, 123, 225, 41, 246, 69, 31, 185, 63, 104, 136, 234, 68, 210, 37, 237, 227, 245, 197, 16, 127, 204, 237, 65, 88, 156, 52, 76, 119, 49, 39, 76, 200, 234, 144, 164, 76, 220, 130, 24, 122, 129, 161, 45, 11, 247, 186, 30, 122, 176, 197, 146, 10, 157, 246, 219, 115, 146, 1, 238, 105, 37, 13, 16, 70, 224, 132, 31, 181, 20, 28, 213, 70, 198, 14, 135, 185, 72, 105, 143, 63, 67, 217, 134, 250, 17, 2, 159, 78, 106, 192, 196, 21, 64, 199, 107, 95, 13, 198, 144, 212, 69, 255, 226, 191, 121, 46, 30, 103, 153, 111, 171, 166, 137, 88, 229, 86, 142, 66, 238, 136, 24, 72, 248, 27, 43, 116, 101, 215, 99, 39, 246, 212, 111, 241, 132, 169, 7, 252, 19, 104, 172, 233, 8, 40, 227, 172, 42, 47, 36, 134, 34, 214, 97, 228, 179, 215, 193, 4, 222, 129, 165, 1, 59, 216, 171, 50, 17, 100, 68, 199, 226, 114, 175, 49, 6, 95, 129, 122, 189, 198, 152, 17, 113, 70, 121, 104, 51, 75, 18, 210, 27, 237, 93, 87, 104, 49, 64, 112, 122, 198, 34, 61, 209, 7, 6, 121, 22, 191, 95, 151, 248, 124, 7, 87, 143, 45, 123, 22, 128, 153, 197, 130, 196, 244, 164, 225, 241, 2, 129, 129, 0, 252, 223, 109, 18, 211, 223, 124, 146, 67, 138, 211, 142, 156, 153, 102, 192, 192, 236, 129, 21, 14, 158, 28, 228, 12, 184, 69, 239, 165, 195, 209, 9, 236, 240, 88, 59, 143, 104, 199, 197, 124, 83, 168, 201, 166, 249, 158, 156, 67, 158, 15, 116, 155, 224, 83, 172, 112, 187, 1, 225, 127, 254, 175, 175, 214, 214, 36, 111, 218, 85, 109, 33, 228, 157, 192, 61, 195, 207, 25, 136, 154, 244, 134, 69, 18, 103, 225, 172, 131, 16, 168, 70, 3, 30, 5, 98, 162, 47, 88, 191, 99, 241, 127, 93, 36, 4, 72, 97, 227, 7, 70, 60, 141, 25, 150, 77, 170, 201, 86, 129, 29, 96, 60, 41, 231, 190, 200, 107, 2, 129, 129, 0, 220, 54, 40, 140, 204, 79, 7, 149, 241, 40, 229, 237, 13, 3, 118, 172, 76, 61, 137, 8, 253, 72, 223, 119, 189, 19, 87, 199, 3, 61, 197, 45, 111, 18, 58, 224, 121, 190, 144, 46, 143, 225, 7, 129, 10, 154, 24, 140, 96, 246, 212, 224, 232, 144, 67, 98, 6, 188, 167, 17, 224, 215, 160, 182, 249, 132, 174, 249, 21, 78, 138, 59, 186, 184, 239, 10, 71, 146, 46, 189, 206, 165, 57, 50, 38, 241, 230, 57, 169, 77, 76, 229, 53, 45, 184, 87, 22, 194, 94, 48, 68, 246, 171, 255, 73, 197, 25, 64, 13, 132, 56, 120, 241, 100, 197, 243, 171, 84, 246, 32, 86, 55, 55, 216, 121, 64, 52, 55, 2, 129, 128, 109, 221, 189, 12, 35, 21, 196, 143, 223, 220, 159, 82, 36, 227, 217, 107, 1, 231, 63, 166, 32, 117, 189, 227, 175, 75, 24, 199, 168, 99, 205, 156, 220, 95, 8, 86, 200, 86, 36, 5, 191, 160, 177, 130, 251, 147, 20, 192, 155, 248, 62, 138, 209, 118, 195, 163, 246, 78, 169, 224, 137, 181, 228, 43, 39, 210, 94, 126, 98, 132, 31, 40, 76, 165, 229, 114, 112, 114, 184, 139, 75, 151, 214, 6, 136, 154, 173, 200, 64, 33, 170, 154, 208, 155, 232, 135, 20, 36, 50, 16, 229, 161, 117, 78, 200, 105, 59, 241, 155, 171, 251, 110, 47, 119, 224, 127, 218, 38, 35, 249, 113, 3, 240, 223, 220, 26, 94, 5, 2, 129, 129, 0, 149, 113, 187, 187, 49, 188, 64, 109, 165, 168, 23, 193, 244, 30, 241, 158, 164, 110, 238, 92, 199, 103, 121, 32, 141, 148, 94, 241, 148, 101, 139, 54, 246, 53, 236, 247, 2, 40, 45, 57, 44, 51, 143, 32, 39, 205, 195, 243, 32, 170, 226, 117, 111, 222, 215, 155, 226, 238, 140, 131, 57, 143, 156, 102, 16, 151, 215, 22, 251, 58, 189, 221, 35, 46, 246, 42, 135, 191, 209, 48, 198, 216, 162, 36, 67, 1, 207, 56, 58, 137, 87, 50, 6, 16, 237, 21, 77, 64, 195, 35, 6, 234, 80, 119, 131, 220, 218, 241, 249, 58, 78, 8, 229, 233, 121, 221, 143, 220, 172, 219, 237, 38, 180, 35, 152, 197, 213, 169, 2, 129, 129, 0, 157, 34, 27, 203, 101, 161, 91, 231, 149, 223, 255, 186, 178, 175, 168, 93, 194, 163, 171, 101, 186, 95, 110, 38, 250, 23, 38, 18, 213, 87, 33, 41, 187, 18, 0, 21, 202, 68, 70, 236, 63, 219, 158, 201, 128, 166, 97, 210, 170, 210, 56, 80, 81, 24, 152, 240, 124, 20, 135, 22, 9, 92, 209, 189, 96, 214, 49, 70, 74, 200, 155, 82, 70, 96, 189, 70, 89, 82, 210, 229, 125, 135, 64, 183, 195, 243, 219, 121, 73, 43, 22, 184, 122, 92, 209, 118, 126, 19, 82, 110, 246, 109, 121, 198, 145, 226, 199, 242, 82, 139, 105, 101, 44, 41, 186, 33, 10, 94, 103, 157, 35, 178, 26, 104, 12, 191, 13, 7]),
+            jwk: {
+                kty: "RSA",
+                n: "2YWA6y0XcvSkdrxUBL7mDZo8Ksu88kp0dU2fWmgSOI-ePyYK2BaH3bNm-NpVlGKzl_HAl4ltDfbm3jHAT41HzRVgDRG-TsTmMJ4gBBYlf6u6i77TOrDBZdo8mx_OwsTp5SrKY1mnz1TVJ1tEhr8BorRfBPriC3F9AaeUVwcogVKXsrfyK-AO8wI4E8qHt-C-g0MzXPrwdp42bPklbPRCOUWLtH69azLwFomAZwCSc_edRbhbnzP1cxjfxa-YGqKWSDTn9bMwEtNpZGpnOLIrylXlkGbx5p9qafHu3szogbdCP9RN_Hp8mJxCZ0HYgTw_zcAktT2EKQo77aPIOHLK_Q",
+                e: "AQAB",
+                d: "WtKndYqqU9FaKkmQOzsKC3vuy1-uUOxNm_0BIFp74Sn2RR-5P2iI6kTSJe3j9cUQf8ztQVicNEx3MSdMyOqQpEzcghh6gaEtC_e6HnqwxZIKnfbbc5IB7mklDRBG4IQftRQc1UbGDoe5SGmPP0PZhvoRAp9OasDEFUDHa18NxpDURf_iv3kuHmeZb6umiVjlVo5C7ogYSPgbK3Rl12Mn9tRv8YSpB_wTaKzpCCjjrCovJIYi1mHks9fBBN6BpQE72KsyEWREx-JyrzEGX4F6vcaYEXFGeWgzSxLSG-1dV2gxQHB6xiI90QcGeRa_X5f4fAdXjy17FoCZxYLE9KTh8Q",
+                p: "_N9tEtPffJJDitOOnJlmwMDsgRUOnhzkDLhF76XD0Qns8Fg7j2jHxXxTqMmm-Z6cQ54PdJvgU6xwuwHhf_6vr9bWJG_aVW0h5J3APcPPGYia9IZFEmfhrIMQqEYDHgVioi9Yv2Pxf10kBEhh4wdGPI0Zlk2qyVaBHWA8Kee-yGs",
+                q: "3DYojMxPB5XxKOXtDQN2rEw9iQj9SN93vRNXxwM9xS1vEjrgeb6QLo_hB4EKmhiMYPbU4OiQQ2IGvKcR4NegtvmErvkVToo7urjvCkeSLr3OpTkyJvHmOalNTOU1LbhXFsJeMET2q_9JxRlADYQ4ePFkxfOrVPYgVjc32HlANDc",
+                dp: "bd29DCMVxI_f3J9SJOPZawHnP6Ygdb3jr0sYx6hjzZzcXwhWyFYkBb-gsYL7kxTAm_g-itF2w6P2TqngibXkKyfSXn5ihB8oTKXlcnByuItLl9YGiJqtyEAhqprQm-iHFCQyEOWhdU7IaTvxm6v7bi934H_aJiP5cQPw39waXgU",
+                dq: "lXG7uzG8QG2lqBfB9B7xnqRu7lzHZ3kgjZRe8ZRlizb2Nez3AigtOSwzjyAnzcPzIKridW_e15vi7oyDOY-cZhCX1xb7Or3dIy72Koe_0TDG2KIkQwHPODqJVzIGEO0VTUDDIwbqUHeD3Nrx-TpOCOXped2P3Kzb7Sa0I5jF1ak",
+                qi: "nSIby2WhW-eV3_-6sq-oXcKjq2W6X24m-hcmEtVXISm7EgAVykRG7D_bnsmApmHSqtI4UFEYmPB8FIcWCVzRvWDWMUZKyJtSRmC9RllS0uV9h0C3w_PbeUkrFrh6XNF2fhNSbvZtecaR4sfyUotpZSwpuiEKXmedI7IaaAy_DQc"
+            }
+        },
+
+        4096: {
+            spki: new Uint8Array([48, 130, 2, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 2, 15, 0, 48, 130, 2, 10, 2, 130, 2, 1, 0, 218, 170, 246, 76, 189, 156, 216, 153, 155, 176, 221, 14, 44, 132, 103, 104, 0, 127, 100, 166, 245, 248, 104, 125, 31, 74, 155, 226, 90, 193, 184, 54, 170, 145, 111, 222, 20, 252, 19, 248, 146, 44, 190, 115, 73, 188, 52, 251, 4, 178, 121, 238, 212, 204, 34, 62, 122, 100, 203, 111, 233, 231, 210, 73, 53, 146, 147, 211, 14, 161, 109, 137, 212, 175, 226, 18, 183, 173, 103, 103, 30, 128, 31, 218, 69, 126, 234, 65, 88, 231, 160, 91, 51, 245, 77, 54, 4, 167, 192, 33, 68, 244, 163, 242, 187, 111, 209, 180, 241, 221, 107, 172, 5, 40, 134, 47, 210, 85, 8, 112, 57, 186, 29, 131, 176, 93, 116, 198, 202, 82, 108, 251, 209, 3, 72, 75, 143, 59, 44, 222, 56, 89, 69, 103, 159, 211, 160, 19, 214, 173, 77, 133, 0, 68, 219, 164, 79, 64, 238, 65, 189, 201, 248, 173, 180, 146, 196, 238, 86, 232, 215, 109, 39, 165, 162, 16, 230, 46, 134, 234, 148, 106, 34, 230, 198, 63, 231, 143, 16, 179, 208, 109, 22, 100, 54, 156, 107, 132, 28, 208, 118, 205, 217, 89, 228, 75, 196, 169, 181, 5, 85, 157, 144, 110, 129, 186, 141, 119, 104, 162, 206, 170, 115, 7, 96, 82, 240, 33, 143, 81, 243, 215, 67, 96, 137, 207, 209, 22, 162, 251, 108, 208, 232, 32, 236, 205, 167, 174, 161, 116, 13, 249, 187, 22, 240, 185, 172, 160, 103, 94, 162, 147, 26, 15, 143, 183, 147, 98, 231, 117, 134, 185, 50, 64, 40, 30, 27, 13, 152, 132, 40, 138, 32, 78, 158, 162, 207, 212, 229, 210, 251, 88, 116, 67, 229, 164, 164, 147, 59, 32, 94, 217, 197, 242, 149, 102, 74, 219, 46, 127, 68, 28, 116, 10, 2, 249, 231, 130, 123, 29, 45, 73, 56, 17, 195, 208, 45, 25, 60, 252, 98, 189, 109, 25, 0, 253, 151, 254, 124, 211, 48, 23, 156, 78, 163, 154, 188, 17, 69, 14, 188, 16, 64, 59, 190, 136, 70, 162, 253, 237, 156, 111, 41, 27, 40, 63, 205, 204, 94, 0, 50, 237, 62, 87, 211, 115, 91, 68, 194, 104, 119, 72, 106, 226, 160, 48, 165, 138, 134, 2, 138, 153, 181, 38, 249, 48, 120, 72, 15, 245, 227, 15, 164, 64, 188, 74, 4, 84, 213, 83, 67, 73, 87, 181, 72, 94, 46, 54, 193, 252, 188, 14, 207, 28, 82, 159, 131, 168, 238, 168, 145, 28, 230, 27, 126, 151, 93, 5, 96, 68, 126, 66, 174, 155, 101, 123, 20, 218, 131, 92, 124, 78, 82, 44, 55, 139, 77, 105, 177, 136, 121, 177, 43, 77, 12, 240, 0, 76, 20, 133, 121, 129, 73, 15, 160, 200, 150, 114, 95, 59, 59, 165, 240, 204, 13, 156, 134, 194, 4, 70, 158, 213, 111, 229, 103, 216, 239, 132, 16, 184, 151, 206, 254, 229, 62, 23, 58, 125, 49, 144, 208, 215, 2, 3, 1, 0, 1]),
+            pkcs8: new Uint8Array([48, 130, 9, 68, 2, 1, 0, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 4, 130, 9, 46, 48, 130, 9, 42, 2, 1, 0, 2, 130, 2, 1, 0, 218, 170, 246, 76, 189, 156, 216, 153, 155, 176, 221, 14, 44, 132, 103, 104, 0, 127, 100, 166, 245, 248, 104, 125, 31, 74, 155, 226, 90, 193, 184, 54, 170, 145, 111, 222, 20, 252, 19, 248, 146, 44, 190, 115, 73, 188, 52, 251, 4, 178, 121, 238, 212, 204, 34, 62, 122, 100, 203, 111, 233, 231, 210, 73, 53, 146, 147, 211, 14, 161, 109, 137, 212, 175, 226, 18, 183, 173, 103, 103, 30, 128, 31, 218, 69, 126, 234, 65, 88, 231, 160, 91, 51, 245, 77, 54, 4, 167, 192, 33, 68, 244, 163, 242, 187, 111, 209, 180, 241, 221, 107, 172, 5, 40, 134, 47, 210, 85, 8, 112, 57, 186, 29, 131, 176, 93, 116, 198, 202, 82, 108, 251, 209, 3, 72, 75, 143, 59, 44, 222, 56, 89, 69, 103, 159, 211, 160, 19, 214, 173, 77, 133, 0, 68, 219, 164, 79, 64, 238, 65, 189, 201, 248, 173, 180, 146, 196, 238, 86, 232, 215, 109, 39, 165, 162, 16, 230, 46, 134, 234, 148, 106, 34, 230, 198, 63, 231, 143, 16, 179, 208, 109, 22, 100, 54, 156, 107, 132, 28, 208, 118, 205, 217, 89, 228, 75, 196, 169, 181, 5, 85, 157, 144, 110, 129, 186, 141, 119, 104, 162, 206, 170, 115, 7, 96, 82, 240, 33, 143, 81, 243, 215, 67, 96, 137, 207, 209, 22, 162, 251, 108, 208, 232, 32, 236, 205, 167, 174, 161, 116, 13, 249, 187, 22, 240, 185, 172, 160, 103, 94, 162, 147, 26, 15, 143, 183, 147, 98, 231, 117, 134, 185, 50, 64, 40, 30, 27, 13, 152, 132, 40, 138, 32, 78, 158, 162, 207, 212, 229, 210, 251, 88, 116, 67, 229, 164, 164, 147, 59, 32, 94, 217, 197, 242, 149, 102, 74, 219, 46, 127, 68, 28, 116, 10, 2, 249, 231, 130, 123, 29, 45, 73, 56, 17, 195, 208, 45, 25, 60, 252, 98, 189, 109, 25, 0, 253, 151, 254, 124, 211, 48, 23, 156, 78, 163, 154, 188, 17, 69, 14, 188, 16, 64, 59, 190, 136, 70, 162, 253, 237, 156, 111, 41, 27, 40, 63, 205, 204, 94, 0, 50, 237, 62, 87, 211, 115, 91, 68, 194, 104, 119, 72, 106, 226, 160, 48, 165, 138, 134, 2, 138, 153, 181, 38, 249, 48, 120, 72, 15, 245, 227, 15, 164, 64, 188, 74, 4, 84, 213, 83, 67, 73, 87, 181, 72, 94, 46, 54, 193, 252, 188, 14, 207, 28, 82, 159, 131, 168, 238, 168, 145, 28, 230, 27, 126, 151, 93, 5, 96, 68, 126, 66, 174, 155, 101, 123, 20, 218, 131, 92, 124, 78, 82, 44, 55, 139, 77, 105, 177, 136, 121, 177, 43, 77, 12, 240, 0, 76, 20, 133, 121, 129, 73, 15, 160, 200, 150, 114, 95, 59, 59, 165, 240, 204, 13, 156, 134, 194, 4, 70, 158, 213, 111, 229, 103, 216, 239, 132, 16, 184, 151, 206, 254, 229, 62, 23, 58, 125, 49, 144, 208, 215, 2, 3, 1, 0, 1, 2, 130, 2, 1, 0, 185, 115, 209, 92, 24, 92, 19, 159, 131, 89, 166, 193, 68, 164, 46, 135, 24, 20, 243, 42, 94, 230, 4, 200, 73, 103, 159, 121, 131, 251, 83, 222, 153, 30, 171, 191, 176, 16, 114, 103, 152, 161, 118, 12, 148, 246, 152, 0, 100, 101, 113, 224, 74, 125, 174, 117, 74, 156, 125, 165, 54, 189, 179, 172, 255, 80, 135, 42, 178, 247, 217, 204, 209, 163, 49, 155, 42, 72, 88, 176, 46, 63, 255, 195, 192, 184, 248, 183, 223, 76, 226, 197, 54, 245, 206, 60, 8, 10, 181, 122, 1, 223, 113, 196, 133, 143, 58, 77, 185, 235, 78, 76, 32, 59, 212, 66, 110, 162, 75, 123, 210, 153, 180, 58, 97, 179, 129, 60, 175, 142, 228, 123, 85, 50, 241, 119, 147, 204, 94, 43, 65, 163, 4, 167, 243, 247, 41, 134, 105, 197, 165, 63, 45, 145, 56, 174, 203, 192, 135, 209, 29, 195, 83, 179, 14, 184, 131, 104, 152, 48, 245, 179, 207, 178, 60, 23, 21, 1, 84, 207, 82, 124, 9, 137, 171, 141, 187, 55, 172, 180, 180, 10, 48, 185, 97, 79, 156, 39, 249, 192, 27, 98, 77, 250, 93, 18, 157, 130, 72, 210, 115, 96, 36, 132, 116, 101, 225, 96, 234, 79, 89, 243, 89, 135, 97, 252, 53, 72, 97, 34, 226, 41, 41, 45, 144, 243, 189, 162, 243, 43, 69, 136, 143, 182, 140, 223, 134, 93, 38, 245, 36, 125, 46, 93, 48, 94, 215, 39, 156, 57, 86, 93, 207, 204, 72, 106, 112, 215, 203, 230, 80, 20, 137, 224, 242, 33, 146, 33, 108, 188, 185, 254, 117, 189, 240, 82, 64, 60, 186, 247, 190, 138, 170, 159, 147, 75, 49, 148, 101, 174, 130, 21, 177, 211, 121, 6, 153, 144, 230, 166, 181, 155, 94, 232, 2, 4, 119, 236, 35, 133, 253, 223, 14, 30, 199, 57, 215, 31, 251, 90, 167, 19, 231, 154, 54, 225, 85, 68, 17, 234, 158, 53, 50, 243, 182, 149, 193, 214, 60, 188, 6, 38, 2, 200, 161, 232, 193, 30, 153, 231, 221, 57, 140, 55, 69, 35, 21, 153, 34, 238, 175, 65, 253, 210, 119, 125, 120, 116, 153, 127, 67, 204, 9, 66, 210, 200, 165, 212, 216, 2, 62, 19, 15, 171, 77, 183, 247, 127, 224, 138, 41, 208, 170, 227, 36, 158, 176, 111, 128, 172, 70, 73, 241, 148, 172, 50, 174, 126, 80, 177, 235, 93, 89, 102, 84, 76, 221, 30, 216, 49, 125, 142, 35, 45, 96, 224, 60, 161, 63, 48, 85, 143, 20, 76, 182, 111, 15, 156, 139, 55, 155, 113, 226, 248, 239, 130, 252, 241, 197, 247, 124, 61, 39, 197, 170, 119, 76, 136, 195, 180, 169, 106, 240, 234, 101, 114, 207, 11, 160, 170, 139, 194, 187, 48, 22, 114, 84, 64, 151, 30, 212, 99, 213, 176, 106, 79, 232, 127, 197, 153, 133, 8, 56, 210, 83, 67, 106, 124, 231, 96, 2, 145, 2, 130, 1, 1, 0, 244, 218, 215, 194, 174, 36, 99, 217, 1, 4, 236, 11, 160, 86, 85, 65, 206, 36, 36, 143, 205, 108, 166, 191, 91, 209, 75, 117, 7, 81, 33, 179, 44, 101, 145, 215, 39, 117, 195, 81, 31, 111, 36, 7, 26, 105, 30, 249, 91, 2, 2, 237, 126, 141, 231, 153, 213, 181, 100, 234, 219, 192, 114, 179, 215, 229, 39, 212, 107, 9, 55, 220, 136, 233, 237, 28, 74, 97, 6, 22, 26, 47, 150, 83, 82, 95, 186, 146, 22, 38, 176, 231, 255, 166, 199, 223, 217, 86, 142, 56, 43, 199, 25, 247, 249, 122, 59, 142, 152, 20, 49, 147, 13, 132, 249, 203, 251, 146, 116, 96, 88, 81, 232, 45, 106, 100, 187, 99, 73, 32, 203, 134, 30, 223, 100, 179, 179, 128, 81, 242, 25, 85, 137, 125, 96, 153, 240, 224, 86, 20, 206, 24, 26, 197, 233, 164, 158, 50, 222, 103, 197, 211, 144, 101, 182, 205, 201, 51, 23, 231, 125, 229, 130, 61, 139, 204, 195, 243, 69, 38, 185, 187, 48, 249, 140, 107, 137, 39, 234, 21, 13, 43, 24, 112, 108, 109, 15, 25, 57, 55, 127, 40, 152, 238, 227, 96, 86, 157, 114, 35, 52, 54, 38, 140, 85, 42, 119, 53, 99, 35, 133, 208, 240, 65, 171, 8, 71, 255, 243, 248, 176, 166, 17, 178, 92, 62, 203, 56, 158, 31, 169, 223, 123, 7, 118, 216, 166, 132, 83, 62, 112, 160, 99, 244, 132, 29, 2, 130, 1, 1, 0, 228, 158, 249, 243, 243, 94, 42, 189, 87, 61, 152, 139, 197, 122, 33, 97, 4, 39, 135, 66, 219, 225, 11, 70, 103, 92, 115, 10, 8, 225, 5, 2, 220, 32, 23, 147, 56, 111, 237, 98, 48, 174, 122, 207, 109, 152, 187, 125, 220, 186, 73, 127, 42, 82, 39, 228, 163, 12, 188, 36, 71, 107, 52, 235, 223, 200, 7, 38, 6, 167, 28, 158, 26, 213, 126, 186, 90, 152, 133, 44, 53, 156, 61, 130, 92, 163, 3, 27, 35, 185, 141, 112, 236, 246, 210, 107, 75, 245, 33, 126, 134, 215, 41, 1, 244, 220, 36, 93, 22, 232, 50, 62, 68, 141, 153, 118, 62, 1, 167, 197, 202, 113, 187, 196, 186, 251, 161, 128, 66, 211, 145, 103, 133, 69, 207, 155, 117, 65, 76, 251, 125, 43, 224, 105, 171, 6, 29, 254, 31, 111, 144, 5, 158, 166, 180, 143, 163, 205, 212, 151, 7, 11, 50, 234, 82, 37, 143, 75, 104, 124, 97, 69, 220, 246, 202, 45, 25, 40, 220, 23, 92, 116, 112, 114, 204, 198, 140, 48, 111, 191, 53, 28, 9, 134, 234, 90, 168, 243, 108, 75, 197, 99, 162, 173, 31, 194, 97, 224, 184, 76, 227, 170, 199, 106, 129, 14, 77, 234, 231, 38, 192, 197, 233, 174, 150, 240, 55, 252, 241, 27, 97, 169, 49, 49, 115, 9, 218, 65, 253, 14, 253, 217, 91, 141, 44, 68, 32, 247, 219, 199, 31, 45, 212, 68, 46, 131, 2, 130, 1, 1, 0, 225, 142, 199, 187, 155, 88, 2, 114, 225, 49, 123, 144, 170, 63, 93, 130, 165, 55, 62, 71, 10, 97, 208, 169, 239, 23, 58, 127, 176, 33, 216, 253, 137, 36, 119, 216, 207, 140, 248, 68, 62, 196, 207, 87, 139, 200, 210, 179, 186, 86, 124, 3, 243, 213, 29, 72, 229, 73, 152, 145, 145, 166, 19, 4, 1, 26, 36, 58, 213, 239, 67, 250, 112, 85, 174, 11, 165, 169, 3, 70, 81, 17, 13, 85, 236, 72, 43, 66, 112, 13, 108, 98, 11, 107, 196, 44, 61, 182, 50, 133, 36, 46, 225, 137, 65, 212, 140, 16, 171, 159, 206, 155, 60, 149, 6, 216, 22, 3, 176, 25, 32, 195, 51, 50, 195, 19, 208, 91, 129, 254, 39, 254, 129, 106, 33, 6, 57, 145, 55, 235, 225, 210, 158, 57, 85, 71, 250, 81, 110, 122, 243, 239, 216, 154, 0, 197, 152, 198, 27, 131, 85, 5, 179, 187, 63, 79, 10, 205, 122, 115, 209, 210, 30, 204, 59, 128, 129, 242, 19, 253, 188, 146, 232, 102, 186, 40, 69, 204, 243, 34, 57, 99, 61, 188, 50, 229, 180, 70, 244, 34, 95, 141, 50, 116, 190, 24, 253, 49, 68, 247, 145, 29, 97, 29, 93, 71, 37, 81, 148, 230, 32, 91, 125, 55, 193, 42, 123, 201, 25, 34, 58, 248, 128, 204, 225, 149, 38, 248, 29, 17, 230, 22, 236, 234, 207, 92, 124, 232, 225, 22, 96, 2, 32, 146, 27, 49, 2, 130, 1, 1, 0, 129, 62, 34, 61, 183, 242, 31, 37, 68, 193, 108, 144, 111, 133, 248, 130, 184, 239, 131, 182, 215, 72, 164, 176, 27, 84, 151, 48, 48, 14, 205, 95, 109, 131, 178, 240, 38, 50, 152, 55, 47, 32, 36, 11, 73, 128, 211, 85, 118, 199, 213, 46, 207, 132, 252, 74, 115, 166, 138, 97, 212, 2, 22, 59, 214, 25, 101, 121, 40, 191, 166, 28, 247, 60, 132, 84, 227, 76, 95, 212, 187, 69, 229, 59, 226, 20, 193, 119, 193, 61, 111, 105, 76, 124, 200, 61, 162, 6, 36, 246, 59, 82, 61, 59, 126, 234, 72, 160, 91, 135, 206, 135, 135, 7, 169, 158, 191, 180, 253, 220, 129, 242, 195, 220, 150, 124, 20, 51, 199, 19, 133, 154, 201, 43, 203, 14, 174, 61, 201, 64, 78, 229, 212, 10, 200, 133, 63, 197, 94, 142, 26, 20, 35, 57, 72, 207, 255, 33, 40, 50, 108, 231, 246, 211, 162, 182, 219, 8, 29, 60, 91, 93, 60, 106, 67, 167, 53, 22, 245, 61, 59, 166, 19, 191, 194, 101, 231, 240, 165, 235, 169, 33, 125, 125, 72, 213, 17, 183, 243, 27, 238, 173, 193, 212, 47, 37, 27, 98, 7, 174, 103, 242, 46, 163, 213, 235, 121, 62, 247, 135, 223, 232, 194, 143, 81, 130, 225, 147, 219, 213, 199, 226, 247, 13, 102, 100, 70, 127, 145, 136, 189, 22, 248, 123, 153, 111, 182, 87, 136, 102, 76, 9, 3, 123, 187, 243, 2, 130, 1, 0, 36, 121, 149, 41, 189, 115, 193, 110, 98, 69, 30, 145, 9, 231, 177, 98, 120, 118, 126, 102, 62, 220, 58, 207, 73, 211, 60, 15, 24, 107, 208, 95, 29, 107, 40, 190, 182, 84, 106, 17, 217, 198, 210, 27, 233, 227, 153, 252, 128, 181, 44, 145, 101, 156, 7, 209, 23, 149, 66, 78, 109, 145, 138, 13, 241, 174, 198, 3, 26, 222, 15, 241, 120, 176, 54, 190, 97, 80, 215, 99, 49, 62, 204, 135, 226, 32, 141, 102, 251, 32, 152, 108, 113, 237, 59, 142, 30, 185, 195, 135, 145, 1, 86, 115, 56, 253, 215, 186, 221, 202, 196, 36, 227, 118, 177, 130, 60, 59, 56, 190, 198, 157, 142, 18, 96, 43, 218, 199, 150, 42, 174, 44, 198, 65, 103, 139, 167, 177, 46, 26, 155, 248, 209, 56, 155, 209, 204, 42, 89, 224, 212, 75, 80, 135, 106, 203, 4, 81, 181, 85, 128, 247, 73, 134, 41, 48, 183, 57, 127, 28, 234, 26, 244, 177, 159, 113, 90, 249, 120, 32, 248, 134, 79, 99, 123, 155, 173, 201, 185, 216, 166, 32, 152, 181, 6, 154, 118, 18, 181, 245, 106, 25, 37, 146, 118, 16, 215, 30, 83, 96, 35, 154, 93, 0, 13, 5, 206, 156, 129, 147, 118, 87, 248, 155, 49, 135, 7, 39, 157, 226, 171, 96, 16, 112, 122, 173, 58, 145, 19, 6, 90, 11, 221, 109, 208, 16, 251, 188, 18, 120, 106, 170, 143, 149, 79, 192]),
+            jwk: {
+                kty: "RSA",
+                n: "2qr2TL2c2JmbsN0OLIRnaAB_ZKb1-Gh9H0qb4lrBuDaqkW_eFPwT-JIsvnNJvDT7BLJ57tTMIj56ZMtv6efSSTWSk9MOoW2J1K_iEretZ2cegB_aRX7qQVjnoFsz9U02BKfAIUT0o_K7b9G08d1rrAUohi_SVQhwObodg7BddMbKUmz70QNIS487LN44WUVnn9OgE9atTYUARNukT0DuQb3J-K20ksTuVujXbSelohDmLobqlGoi5sY_548Qs9BtFmQ2nGuEHNB2zdlZ5EvEqbUFVZ2QboG6jXdoos6qcwdgUvAhj1Hz10Ngic_RFqL7bNDoIOzNp66hdA35uxbwuaygZ16ikxoPj7eTYud1hrkyQCgeGw2YhCiKIE6eos_U5dL7WHRD5aSkkzsgXtnF8pVmStsuf0QcdAoC-eeCex0tSTgRw9AtGTz8Yr1tGQD9l_580zAXnE6jmrwRRQ68EEA7vohGov3tnG8pGyg_zcxeADLtPlfTc1tEwmh3SGrioDClioYCipm1JvkweEgP9eMPpEC8SgRU1VNDSVe1SF4uNsH8vA7PHFKfg6juqJEc5ht-l10FYER-Qq6bZXsU2oNcfE5SLDeLTWmxiHmxK00M8ABMFIV5gUkPoMiWcl87O6XwzA2chsIERp7Vb-Vn2O-EELiXzv7lPhc6fTGQ0Nc",
+                e: "AQAB",
+                d: "uXPRXBhcE5-DWabBRKQuhxgU8ype5gTISWefeYP7U96ZHqu_sBByZ5ihdgyU9pgAZGVx4Ep9rnVKnH2lNr2zrP9Qhyqy99nM0aMxmypIWLAuP__DwLj4t99M4sU29c48CAq1egHfccSFjzpNuetOTCA71EJuokt70pm0OmGzgTyvjuR7VTLxd5PMXitBowSn8_cphmnFpT8tkTiuy8CH0R3DU7MOuINomDD1s8-yPBcVAVTPUnwJiauNuzestLQKMLlhT5wn-cAbYk36XRKdgkjSc2AkhHRl4WDqT1nzWYdh_DVIYSLiKSktkPO9ovMrRYiPtozfhl0m9SR9Ll0wXtcnnDlWXc_MSGpw18vmUBSJ4PIhkiFsvLn-db3wUkA8uve-iqqfk0sxlGWughWx03kGmZDmprWbXugCBHfsI4X93w4exznXH_tapxPnmjbhVUQR6p41MvO2lcHWPLwGJgLIoejBHpnn3TmMN0UjFZki7q9B_dJ3fXh0mX9DzAlC0sil1NgCPhMPq02393_giinQquMknrBvgKxGSfGUrDKuflCx611ZZlRM3R7YMX2OIy1g4DyhPzBVjxRMtm8PnIs3m3Hi-O-C_PHF93w9J8Wqd0yIw7SpavDqZXLPC6Cqi8K7MBZyVECXHtRj1bBqT-h_xZmFCDjSU0NqfOdgApE",
+                p: "9NrXwq4kY9kBBOwLoFZVQc4kJI_NbKa_W9FLdQdRIbMsZZHXJ3XDUR9vJAcaaR75WwIC7X6N55nVtWTq28Bys9flJ9RrCTfciOntHEphBhYaL5ZTUl-6khYmsOf_psff2VaOOCvHGff5ejuOmBQxkw2E-cv7knRgWFHoLWpku2NJIMuGHt9ks7OAUfIZVYl9YJnw4FYUzhgaxemknjLeZ8XTkGW2zckzF-d95YI9i8zD80Umubsw-YxriSfqFQ0rGHBsbQ8ZOTd_KJju42BWnXIjNDYmjFUqdzVjI4XQ8EGrCEf_8_iwphGyXD7LOJ4fqd97B3bYpoRTPnCgY_SEHQ",
+                q: "5J758_NeKr1XPZiLxXohYQQnh0Lb4QtGZ1xzCgjhBQLcIBeTOG_tYjCues9tmLt93LpJfypSJ-SjDLwkR2s069_IByYGpxyeGtV-ulqYhSw1nD2CXKMDGyO5jXDs9tJrS_UhfobXKQH03CRdFugyPkSNmXY-AafFynG7xLr7oYBC05FnhUXPm3VBTPt9K-BpqwYd_h9vkAWeprSPo83UlwcLMupSJY9LaHxhRdz2yi0ZKNwXXHRwcszGjDBvvzUcCYbqWqjzbEvFY6KtH8Jh4LhM46rHaoEOTernJsDF6a6W8Df88RthqTExcwnaQf0O_dlbjSxEIPfbxx8t1EQugw",
+                dp: "4Y7Hu5tYAnLhMXuQqj9dgqU3PkcKYdCp7xc6f7Ah2P2JJHfYz4z4RD7Ez1eLyNKzulZ8A_PVHUjlSZiRkaYTBAEaJDrV70P6cFWuC6WpA0ZREQ1V7EgrQnANbGILa8QsPbYyhSQu4YlB1IwQq5_OmzyVBtgWA7AZIMMzMsMT0FuB_if-gWohBjmRN-vh0p45VUf6UW568-_YmgDFmMYbg1UFs7s_TwrNenPR0h7MO4CB8hP9vJLoZrooRczzIjljPbwy5bRG9CJfjTJ0vhj9MUT3kR1hHV1HJVGU5iBbfTfBKnvJGSI6-IDM4ZUm-B0R5hbs6s9cfOjhFmACIJIbMQ",
+                dq: "gT4iPbfyHyVEwWyQb4X4grjvg7bXSKSwG1SXMDAOzV9tg7LwJjKYNy8gJAtJgNNVdsfVLs-E_Epzpoph1AIWO9YZZXkov6Yc9zyEVONMX9S7ReU74hTBd8E9b2lMfMg9ogYk9jtSPTt-6kigW4fOh4cHqZ6_tP3cgfLD3JZ8FDPHE4WaySvLDq49yUBO5dQKyIU_xV6OGhQjOUjP_yEoMmzn9tOittsIHTxbXTxqQ6c1FvU9O6YTv8Jl5_Cl66khfX1I1RG38xvurcHULyUbYgeuZ_Iuo9XreT73h9_owo9RguGT29XH4vcNZmRGf5GIvRb4e5lvtleIZkwJA3u78w",
+                qi: "JHmVKb1zwW5iRR6RCeexYnh2fmY-3DrPSdM8Dxhr0F8dayi-tlRqEdnG0hvp45n8gLUskWWcB9EXlUJObZGKDfGuxgMa3g_xeLA2vmFQ12MxPsyH4iCNZvsgmGxx7TuOHrnDh5EBVnM4_de63crEJON2sYI8Ozi-xp2OEmAr2seWKq4sxkFni6exLhqb-NE4m9HMKlng1EtQh2rLBFG1VYD3SYYpMLc5fxzqGvSxn3Fa-Xgg-IZPY3ubrcm52KYgmLUGmnYStfVqGSWSdhDXHlNgI5pdAA0FzpyBk3ZX-JsxhwcnneKrYBBweq06kRMGWgvdbdAQ-7wSeGqqj5VPwA"
+            }
+        },
+
+    };
+
+    // combinations to test
+    var testVectors = [
+        {name: "RSA-OAEP", privateUsages: ["decrypt", "unwrapKey"], publicUsages: ["encrypt", "wrapKey"]},
+        {name: "RSA-PSS",  privateUsages: ["sign"], publicUsages: ["verify"]},
+        {name: "RSASSA-PKCS1-v1_5",  privateUsages: ["sign"], publicUsages: ["verify"]}
+    ];
+
+    // TESTS ARE HERE:
+    // Test every test vector, along with all available key data
+    testVectors.forEach(function(vector) {
+        sizes.forEach(function(size) {
+
+            hashes.forEach(function(hash) {
+                [true, false].forEach(function(extractable) {
+
+                    // Test public keys first
+                    allValidUsages(vector.publicUsages, []).forEach(function(usages) {
+                        ['spki', 'jwk'].forEach(function(format) {
+                            var algorithm = {name: vector.name, hash: hash};
+                            var data = keyData[size];
+                            if (format === "jwk") { // Not all fields used for public keys
+                                data = {jwk: {kty: keyData[size].jwk.kty, n: keyData[size].jwk.n, e: keyData[size].jwk.e}};
+                            }
+
+                            testFormat(format, algorithm, data, size, usages, extractable);
+                        });
+
+                    });
+
+                    // Next, test private keys
+                    allValidUsages(vector.privateUsages, []).forEach(function(usages) {
+                        ['pkcs8', 'jwk'].forEach(function(format) {
+                            var algorithm = {name: vector.name, hash: hash};
+                            var data = keyData[size];
+
+                            testFormat(format, algorithm, data, size, usages, extractable);
+                        });
+                    });
+                });
+            });
+
+        });
+    });
+
+
+    // Test importKey with a given key format and other parameters. If
+    // extrable is true, export the key and verify that it matches the input.
+    function testFormat(format, algorithm, keyData, keySize, usages, extractable) {
+        promise_test(function(test) {
+            return subtle.importKey(format, keyData[format], algorithm, extractable, usages).
+            then(function(key) {
+                assert_equals(key.constructor, CryptoKey, "Imported a CryptoKey object");
+                if (!extractable) {
+                    return;
+                }
+
+                return subtle.exportKey(format, key).
+                then(function(result) {
+                    if (format !== "jwk") {
+                        assert_true(equalBuffers(keyData[format], result), "Round trip works");
+                    } else {
+                        assert_true(equalJwk(keyData[format], result), "Round trip works");
+                    }
+                }, function(err) {
+                    assert_unreached("Threw an unexpected error: " + err.toString());
+                });
+            }, function(err) {
+                assert_unreached("Threw an unexpected error: " + err.toString());
+            });
+        }, "Good parameters: " + keySize.toString() + " bits " + parameterString(format, keyData[format], algorithm, extractable, usages));
+    }
+
+
+
+    // Helper methods follow:
+
+    // Are two array buffers the same?
+    function equalBuffers(a, b) {
+        if (a.byteLength !== b.byteLength) {
+            return false;
+        }
+
+        var aBytes = new Uint8Array(a);
+        var bBytes = new Uint8Array(b);
+
+        for (var i=0; i<a.byteLength; i++) {
+            if (aBytes[i] !== bBytes[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    // Are two Jwk objects "the same"? That is, does the object returned include
+    // matching values for each property that was expected? It's okay if the
+    // returned object has extra methods; they aren't checked.
+    function equalJwk(expected, got) {
+        var fields = Object.keys(expected);
+        var fieldName;
+
+        for(var i=0; i<fields.length; i++) {
+            fieldName = fields[i];
+            if (!(fieldName in got)) {
+                return false;
+            }
+            if (expected[fieldName] !== got[fieldName]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    // Build minimal Jwk objects from raw key data and algorithm specifications
+    function jwkData(keyData, algorithm) {
+        var result = {
+            kty: "oct",
+            k: byteArrayToUnpaddedBase64(keyData)
+        };
+
+        if (algorithm.name.substring(0, 3) === "AES") {
+            result.alg = "A" + (8 * keyData.byteLength).toString() + algorithm.name.substring(4);
+        } else if (algorithm.name === "HMAC") {
+            result.alg = "HS" + algorithm.hash.substring(4);
+        }
+        return result;
+    }
+
+    // Jwk format wants Base 64 without the typical padding at the end.
+    function byteArrayToUnpaddedBase64(byteArray){
+        var binaryString = "";
+        for (var i=0; i<byteArray.byteLength; i++){
+            binaryString += String.fromCharCode(byteArray[i]);
+        }
+        var base64String = btoa(binaryString);
+
+        return base64String.replace(/=/g, "");
+    }
+
+    // Want to test every valid combination of usages. Start by creating a list
+    // of all non-empty subsets to possible usages.
+    function allNonemptySubsetsOf(arr) {
+        var results = [];
+        var firstElement;
+        var remainingElements;
+
+        for(var i=0; i<arr.length; i++) {
+            firstElement = arr[i];
+            remainingElements = arr.slice(i+1);
+            results.push([firstElement]);
+
+            if (remainingElements.length > 0) {
+                allNonemptySubsetsOf(remainingElements).forEach(function(combination) {
+                    combination.push(firstElement);
+                    results.push(combination);
+                });
+            }
+        }
+
+        return results;
+    }
+
+    // Return a list of all valid usage combinations, given the possible ones
+    // and the ones that are required for a particular operation.
+    function allValidUsages(possibleUsages, requiredUsages) {
+        var allUsages = [];
+
+        allNonemptySubsetsOf(possibleUsages).forEach(function(usage) {
+            for (var i=0; i<requiredUsages.length; i++) {
+                if (!usage.includes(requiredUsages[i])) {
+                    return;
+                }
+            }
+            allUsages.push(usage);
+        });
+
+        return allUsages;
+    }
+
+    // Convert method parameters to a string to uniquely name each test
+    function parameterString(format, data, algorithm, extractable, usages) {
+        if ("byteLength" in data) {
+            data = "buffer(" + data.byteLength.toString() + ")";
+        } else {
+            data = "object(" + Object.keys(data).join(", ") + ")";
+        }
+        var result = "(" +
+                        objectToString(format) + ", " +
+                        objectToString(data) + ", " +
+                        objectToString(algorithm) + ", " +
+                        objectToString(extractable) + ", " +
+                        objectToString(usages) +
+                     ")";
+
+        return result;
+    }
+
+    // Character representation of any object we may use as a parameter.
+    function objectToString(obj) {
+        var keyValuePairs = [];
+
+        if (Array.isArray(obj)) {
+            return "[" + obj.map(function(elem){return objectToString(elem);}).join(", ") + "]";
+        } else if (typeof obj === "object") {
+            Object.keys(obj).sort().forEach(function(keyName) {
+                keyValuePairs.push(keyName + ": " + objectToString(obj[keyName]));
+            });
+            return "{" + keyValuePairs.join(", ") + "}";
+        } else if (typeof obj === "undefined") {
+            return "undefined";
+        } else {
+            return obj.toString();
+        }
+
+        var keyValuePairs = [];
+
+        Object.keys(obj).sort().forEach(function(keyName) {
+            var value = obj[keyName];
+            if (typeof value === "object") {
+                value = objectToString(value);
+            } else if (typeof value === "array") {
+                value = "[" + value.map(function(elem){return objectToString(elem);}).join(", ") + "]";
+            } else {
+                value = value.toString();
+            }
+
+            keyValuePairs.push(keyName + ": " + value);
+        });
+
+        return "{" + keyValuePairs.join(", ") + "}";
+    }
+
+    return; // from run_test
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/getRandomValues.worker.js b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/symmetric_importKey.https.worker.js
similarity index 60%
copy from src/third_party/web_platform_tests/WebCryptoAPI/getRandomValues.worker.js
copy to src/third_party/web_platform_tests/WebCryptoAPI/import_export/symmetric_importKey.https.worker.js
index cc26356..bedd082 100644
--- a/src/third_party/web_platform_tests/WebCryptoAPI/getRandomValues.worker.js
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/symmetric_importKey.https.worker.js
@@ -1,4 +1,4 @@
 importScripts("/resources/testharness.js");
-importScripts("getRandomValues.js");
+importScripts("symmetric_importKey.js");
 run_test();
 done();
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/import_export/symmetric_importKey.js b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/symmetric_importKey.js
new file mode 100644
index 0000000..1221c60
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/symmetric_importKey.js
@@ -0,0 +1,242 @@
+// Test importKey and exportKey for non-PKC algorithms. Only "happy paths" are
+// currently tested - those where the operation should succeed.
+
+function run_test() {
+    var subtle = crypto.subtle;
+
+    // keying material for algorithms that can use any bit string.
+    var rawKeyData = [
+        new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
+        new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+                        17, 18, 19, 20, 21, 22, 23, 24]),
+        new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+                        17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32])
+    ];
+
+    // combinations of algorithms, usages, parameters, and formats to test
+    var testVectors = [
+        {name: "AES-CTR",               legalUsages: ["encrypt", "decrypt"],      extractable: [true, false], formats: ["raw", "jwk"]},
+        {name: "AES-CBC",               legalUsages: ["encrypt", "decrypt"],      extractable: [true, false], formats: ["raw", "jwk"]},
+        {name: "AES-GCM",               legalUsages: ["encrypt", "decrypt"],      extractable: [true, false], formats: ["raw", "jwk"]},
+        {name: "AES-KW",                legalUsages: ["wrapKey", "unwrapKey"],    extractable: [true, false], formats: ["raw", "jwk"]},
+        {name: "HMAC", hash: "SHA-1",   legalUsages: ["sign", "verify"],          extractable: [false],       formats: ["raw", "jwk"]},
+        {name: "HMAC", hash: "SHA-256", legalUsages: ["sign", "verify"],          extractable: [false],       formats: ["raw", "jwk"]},
+        {name: "HMAC", hash: "SHA-384", legalUsages: ["sign", "verify"],          extractable: [false],       formats: ["raw", "jwk"]},
+        {name: "HMAC", hash: "SHA-512", legalUsages: ["sign", "verify"],          extractable: [false],       formats: ["raw", "jwk"]},
+        {name: "HKDF",                  legalUsages: ["deriveBits", "deriveKey"], extractable: [false],       formats: ["raw"]},
+        {name: "PBKDF2",                legalUsages: ["deriveBits", "deriveKey"], extractable: [false],       formats: ["raw"]}
+    ];
+
+
+
+    // TESTS ARE HERE:
+    // Test every test vector, along with all available key data
+    testVectors.forEach(function(vector) {
+        var algorithm = {name: vector.name};
+        if ("hash" in vector) {
+            algorithm.hash = vector.hash;
+        }
+
+        rawKeyData.forEach(function(keyData) {
+            // Generate all combinations of valid usages for testing
+            allValidUsages(vector.legalUsages, []).forEach(function(usages) {
+                // Try each legal value of the extractable parameter
+                vector.extractable.forEach(function(extractable) {
+                    vector.formats.forEach(function(format) {
+                        var data = keyData;
+                        if (format === "jwk") {
+                            data = jwkData(keyData, algorithm);
+                        }
+                        testFormat(format, algorithm, data, keyData.length * 8, usages, extractable);
+                    });
+                });
+            });
+
+        });
+    });
+
+    // Test importKey with a given key format and other parameters. If
+    // extrable is true, export the key and verify that it matches the input.
+    function testFormat(format, algorithm, keyData, keySize, usages, extractable) {
+        promise_test(function(test) {
+            return subtle.importKey(format, keyData, algorithm, extractable, usages).
+            then(function(key) {
+                assert_equals(key.constructor, CryptoKey, "Imported a CryptoKey object");
+                if (!extractable) {
+                    return;
+                }
+
+                return subtle.exportKey(format, key).
+                then(function(result) {
+                    if (format !== "jwk") {
+                        assert_true(equalBuffers(keyData, result), "Round trip works");
+                    } else {
+                        assert_true(equalJwk(keyData, result), "Round trip works");
+                    }
+                }, function(err) {
+                    assert_unreached("Threw an unexpected error: " + err.toString());
+                });
+            }, function(err) {
+                assert_unreached("Threw an unexpected error: " + err.toString());
+            });
+        }, "Good parameters: " + keySize.toString() + " bits " + parameterString(format, keyData, algorithm, extractable, usages));
+    }
+
+
+
+    // Helper methods follow:
+
+    // Are two array buffers the same?
+    function equalBuffers(a, b) {
+        if (a.byteLength !== b.byteLength) {
+            return false;
+        }
+
+        var aBytes = new Uint8Array(a);
+        var bBytes = new Uint8Array(b);
+
+        for (var i=0; i<a.byteLength; i++) {
+            if (aBytes[i] !== bBytes[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    // Are two Jwk objects "the same"? That is, does the object returned include
+    // matching values for each property that was expected? It's okay if the
+    // returned object has extra methods; they aren't checked.
+    function equalJwk(expected, got) {
+        var fields = Object.keys(expected);
+        var fieldName;
+
+        for(var i=0; i<fields.length; i++) {
+            fieldName = fields[i];
+            if (!(fieldName in got)) {
+                return false;
+            }
+            if (expected[fieldName] !== got[fieldName]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    // Build minimal Jwk objects from raw key data and algorithm specifications
+    function jwkData(keyData, algorithm) {
+        var result = {
+            kty: "oct",
+            k: byteArrayToUnpaddedBase64(keyData)
+        };
+
+        if (algorithm.name.substring(0, 3) === "AES") {
+            result.alg = "A" + (8 * keyData.byteLength).toString() + algorithm.name.substring(4);
+        } else if (algorithm.name === "HMAC") {
+            result.alg = "HS" + algorithm.hash.substring(4);
+        }
+        return result;
+    }
+
+    // Jwk format wants Base 64 without the typical padding at the end.
+    function byteArrayToUnpaddedBase64(byteArray){
+        var binaryString = "";
+        for (var i=0; i<byteArray.byteLength; i++){
+            binaryString += String.fromCharCode(byteArray[i]);
+        }
+        var base64String = btoa(binaryString);
+
+        return base64String.replace(/=/g, "");
+    }
+
+    // Want to test every valid combination of usages. Start by creating a list
+    // of all non-empty subsets to possible usages.
+    function allNonemptySubsetsOf(arr) {
+        var results = [];
+        var firstElement;
+        var remainingElements;
+
+        for(var i=0; i<arr.length; i++) {
+            firstElement = arr[i];
+            remainingElements = arr.slice(i+1);
+            results.push([firstElement]);
+
+            if (remainingElements.length > 0) {
+                allNonemptySubsetsOf(remainingElements).forEach(function(combination) {
+                    combination.push(firstElement);
+                    results.push(combination);
+                });
+            }
+        }
+
+        return results;
+    }
+
+    // Return a list of all valid usage combinations, given the possible ones
+    // and the ones that are required for a particular operation.
+    function allValidUsages(possibleUsages, requiredUsages) {
+        var allUsages = [];
+
+        allNonemptySubsetsOf(possibleUsages).forEach(function(usage) {
+            for (var i=0; i<requiredUsages.length; i++) {
+                if (!usage.includes(requiredUsages[i])) {
+                    return;
+                }
+            }
+            allUsages.push(usage);
+        });
+
+        return allUsages;
+    }
+
+    // Convert method parameters to a string to uniquely name each test
+    function parameterString(format, data, algorithm, extractable, usages) {
+        var result = "(" +
+                        objectToString(format) + ", " +
+                        objectToString(data) + ", " +
+                        objectToString(algorithm) + ", " +
+                        objectToString(extractable) + ", " +
+                        objectToString(usages) +
+                     ")";
+
+        return result;
+    }
+
+    // Character representation of any object we may use as a parameter.
+    function objectToString(obj) {
+        var keyValuePairs = [];
+
+        if (Array.isArray(obj)) {
+            return "[" + obj.map(function(elem){return objectToString(elem);}).join(", ") + "]";
+        } else if (typeof obj === "object") {
+            Object.keys(obj).sort().forEach(function(keyName) {
+                keyValuePairs.push(keyName + ": " + objectToString(obj[keyName]));
+            });
+            return "{" + keyValuePairs.join(", ") + "}";
+        } else if (typeof obj === "undefined") {
+            return "undefined";
+        } else {
+            return obj.toString();
+        }
+
+        var keyValuePairs = [];
+
+        Object.keys(obj).sort().forEach(function(keyName) {
+            var value = obj[keyName];
+            if (typeof value === "object") {
+                value = objectToString(value);
+            } else if (typeof value === "array") {
+                value = "[" + value.map(function(elem){return objectToString(elem);}).join(", ") + "]";
+            } else {
+                value = value.toString();
+            }
+
+            keyValuePairs.push(keyName + ": " + value);
+        });
+
+        return "{" + keyValuePairs.join(", ") + "}";
+    }
+
+    return; // from run_test
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/import_export/test_ec_importKey.https.html b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/test_ec_importKey.https.html
new file mode 100644
index 0000000..54eea40
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/test_ec_importKey.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>WebCryptoAPI: importKey() for EC keys</title>
+<link rel="author" title="Charles Engelke" href="mailto:w3c@engelke.com">
+<link rel="help" href=https://w3c.github.io/webcrypto/Overview.html#SubtleCrypto-method-importKey">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="ec_importKey.js"></script>
+<div id="log"></div>
+<script>
+run_test();
+</script>
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/import_export/test_rsa_importKey.https.html b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/test_rsa_importKey.https.html
new file mode 100644
index 0000000..219ad2d
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/test_rsa_importKey.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>WebCryptoAPI: importKey() for RSA keys</title>
+<link rel="author" title="Charles Engelke" href="mailto:w3c@engelke.com">
+<link rel="help" href=https://w3c.github.io/webcrypto/Overview.html#SubtleCrypto-method-importKey">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="rsa_importKey.js"></script>
+<div id="log"></div>
+<script>
+run_test();
+</script>
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/import_export/test_symmetric_importKey.https.html b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/test_symmetric_importKey.https.html
new file mode 100644
index 0000000..e82c4d7
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/import_export/test_symmetric_importKey.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>WebCryptoAPI: importKey() for symmetric keys</title>
+<link rel="author" title="Charles Engelke" href="mailto:w3c@engelke.com">
+<link rel="help" href=https://w3c.github.io/webcrypto/Overview.html#SubtleCrypto-method-importKey">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="symmetric_importKey.js"></script>
+<div id="log"></div>
+<script>
+run_test();
+</script>
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/secure_context/crypto-subtle-secure-context-available.https.sub.html b/src/third_party/web_platform_tests/WebCryptoAPI/secure_context/crypto-subtle-secure-context-available.https.sub.html
new file mode 100644
index 0000000..67f7ff0
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/secure_context/crypto-subtle-secure-context-available.https.sub.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+  <script>
+    async_test(function(t) {
+      assert_true(typeof crypto.subtle !== 'undefined');
+      t.done();
+    }, "Secure context window has access to crypto.subtle");
+    async_test(function(t) {
+      var w = new Worker('../util/worker-report-crypto-subtle-presence.js');
+
+      w.onmessage = t.step_func(function (e) {
+        if (e.data.msg_type == 'subtle_crypto_found') {
+          assert_equals(e.data.msg_value, true);
+          t.done();
+        }
+      });
+
+    }, "Secure context worker has access to crypto.subtle");
+  </script>
+</body>
+</html>
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/ecdsa.https.worker.js b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/ecdsa.https.worker.js
new file mode 100644
index 0000000..f379d46
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/ecdsa.https.worker.js
@@ -0,0 +1,5 @@
+importScripts("/resources/testharness.js");
+importScripts("ecdsa_vectors.js");
+importScripts("ecdsa.js");
+
+run_test();
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/ecdsa.js b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/ecdsa.js
new file mode 100644
index 0000000..208b81c
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/ecdsa.js
@@ -0,0 +1,481 @@
+
+function run_test() {
+    setup({explicit_done: true});
+
+    var subtle = self.crypto.subtle; // Change to test prefixed implementations
+
+    // When are all these tests really done? When all the promises they use have resolved.
+    var all_promises = [];
+
+    // Source file [algorithm_name]_vectors.js provides the getTestVectors method
+    // for the algorithm that drives these tests.
+    var testVectors = getTestVectors();
+
+    // Test verification first, because signing tests rely on that working
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            var algorithm = {name: vector.algorithmName, hash: vector.hashName};
+            promise_test(function(test) {
+                var operation = subtle.verify(algorithm, vector.publicKey, vector.signature, vector.plaintext)
+                .then(function(is_verified) {
+                    assert_true(is_verified, "Signature verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                return operation;
+            }, vector.name + " verification");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested verification.
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Test verification with an altered buffer after call
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            var algorithm = {name: vector.algorithmName, hash: vector.hashName};
+            promise_test(function(test) {
+                var signature = copyBuffer(vector.signature);
+                var operation = subtle.verify(algorithm, vector.publicKey, signature, vector.plaintext)
+                .then(function(is_verified) {
+                    assert_true(is_verified, "Signature verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                signature[0] = 255 - signature[0];
+                return operation;
+            }, vector.name + " verification with altered signature after call");
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification with altered signature after call");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for successful verification even if plaintext is altered after call.
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            var algorithm = {name: vector.algorithmName, hash: vector.hashName};
+            promise_test(function(test) {
+                var plaintext = copyBuffer(vector.plaintext);
+                var operation = subtle.verify(algorithm, vector.publicKey, vector.signature, plaintext)
+                .then(function(is_verified) {
+                    assert_true(is_verified, "Signature verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                plaintext[0] = 255 - plaintext[0];
+                return operation;
+            }, vector.name + " with altered plaintext after call");
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " with altered plaintext after call");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for failures due to using privateKey to verify.
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            var algorithm = {name: vector.algorithmName, hash: vector.hashName};
+            promise_test(function(test) {
+                return subtle.verify(algorithm, vector.privateKey, vector.signature, vector.plaintext)
+                .then(function(plaintext) {
+                    assert_unreached("Should have thrown error for using privateKey to verify in " + vector.name + ": " + err.message + "'");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw InvalidAccessError instead of '" + err.message + "'");
+                });
+            }, vector.name + " using privateKey to verify");
+
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " using privateKey to verify");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for failures due to using publicKey to sign.
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            var algorithm = {name: vector.algorithmName, hash: vector.hashName};
+            promise_test(function(test) {
+                return subtle.sign(algorithm, vector.publicKey, vector.plaintext)
+                .then(function(signature) {
+                    assert_unreached("Should have thrown error for using publicKey to sign in " + vector.name + ": " + err.message + "'");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw InvalidAccessError instead of '" + err.message + "'");
+                });
+            }, vector.name + " using publicKey to sign");
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " using publicKey to sign");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for failures due to no "verify" usage.
+    testVectors.forEach(function(originalVector) {
+        var vector = Object.assign({}, originalVector);
+
+        var promise = importVectorKeys(vector, [], ["sign"])
+        .then(function(vectors) {
+            var algorithm = {name: vector.algorithmName, hash: vector.hashName};
+            promise_test(function(test) {
+                return subtle.verify(algorithm, vector.publicKey, vector.signature, vector.plaintext)
+                .then(function(plaintext) {
+                    assert_unreached("Should have thrown error for no verify usage in " + vector.name + ": " + err.message + "'");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw InvalidAccessError instead of '" + err.message + "'");
+                });
+            }, vector.name + " no verify usage");
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " no verify usage");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for successful signing and verification.
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            var algorithm = {name: vector.algorithmName, hash: vector.hashName};
+            promise_test(function(test) {
+                return subtle.sign(algorithm, vector.privateKey, vector.plaintext)
+                .then(function(signature) {
+                    // Can we verify the signature?
+                    return subtle.verify(algorithm, vector.publicKey, signature, vector.plaintext)
+                    .then(function(is_verified) {
+                        assert_true(is_verified, "Round trip verification works");
+                        return signature;
+                    }, function(err) {
+                        assert_unreached("verify error for test " + vector.name + ": " + err.message + "'");
+                    });
+                }, function(err) {
+                    assert_unreached("sign error for test " + vector.name + ": '" + err.message + "'");
+                });
+            }, vector.name + " round trip");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested signing or verifying
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " round trip");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Test signing with the wrong algorithm
+    testVectors.forEach(function(vector) {
+        // Want to get the key for the wrong algorithm
+        var promise = subtle.generateKey({name: "HMAC", hash: "SHA-1"}, false, ["sign", "verify"])
+        .then(function(wrongKey) {
+            var algorithm = {name: vector.algorithmName, hash: vector.hashName};
+            return importVectorKeys(vector, ["verify"], ["sign"])
+            .then(function(vectors) {
+                promise_test(function(test) {
+                    var operation = subtle.sign(algorithm, wrongKey, vector.plaintext)
+                    .then(function(signature) {
+                        assert_unreached("Signing should not have succeeded for " + vector.name);
+                    }, function(err) {
+                        assert_equals(err.name, "InvalidAccessError", "Should have thrown InvalidAccessError instead of '" + err.message + "'");
+                    });
+
+                    return operation;
+                }, vector.name + " signing with wrong algorithm name");
+
+            }, function(err) {
+                // We need a failed test if the importVectorKey operation fails, so
+                // we know we never tested verification.
+                promise_test(function(test) {
+                    assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+                }, "importVectorKeys step: " + vector.name + " signing with wrong algorithm name");
+            });
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("Generate wrong key for test " + vector.name + " failed: '" + err.message + "'");
+            }, "generate wrong key step: " + vector.name + " signing with wrong algorithm name");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Test verification with the wrong algorithm
+    testVectors.forEach(function(vector) {
+        // Want to get the key for the wrong algorithm
+        var promise = subtle.generateKey({name: "HMAC", hash: "SHA-1"}, false, ["sign", "verify"])
+        .then(function(wrongKey) {
+            return importVectorKeys(vector, ["verify"], ["sign"])
+            .then(function(vectors) {
+                var algorithm = {name: vector.algorithmName, hash: vector.hashName};
+                promise_test(function(test) {
+                    var operation = subtle.verify(algorithm, wrongKey, vector.signature, vector.plaintext)
+                    .then(function(signature) {
+                        assert_unreached("Verifying should not have succeeded for " + vector.name);
+                    }, function(err) {
+                        assert_equals(err.name, "InvalidAccessError", "Should have thrown InvalidAccessError instead of '" + err.message + "'");
+                    });
+
+                    return operation;
+                }, vector.name + " verifying with wrong algorithm name");
+
+            }, function(err) {
+                // We need a failed test if the importVectorKey operation fails, so
+                // we know we never tested verification.
+                promise_test(function(test) {
+                    assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+                }, "importVectorKeys step: " + vector.name + " verifying with wrong algorithm name");
+            });
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("Generate wrong key for test " + vector.name + " failed: '" + err.message + "'");
+            }, "generate wrong key step: " + vector.name + " verifying with wrong algorithm name");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Test verification fails with wrong signature
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            var algorithm = {name: vector.algorithmName, hash: vector.hashName};
+            var signature = copyBuffer(vector.signature);
+            signature[0] = 255 - signature[0];
+            promise_test(function(test) {
+                var operation = subtle.verify(algorithm, vector.publicKey, signature, vector.plaintext)
+                .then(function(is_verified) {
+                    assert_false(is_verified, "Signature NOT verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                return operation;
+            }, vector.name + " verification failure due to altered signature");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested verification.
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification failure due to altered signature");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Test verification fails with wrong hash
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            var hashName = "SHA-1";
+            if (vector.hashName === "SHA-1") {
+                hashName = "SHA-256"
+            }
+            var algorithm = {name: vector.algorithmName, hash: hashName};
+            promise_test(function(test) {
+                var operation = subtle.verify(algorithm, vector.publicKey, vector.signature, vector.plaintext)
+                .then(function(is_verified) {
+                    assert_false(is_verified, "Signature NOT verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                return operation;
+            }, vector.name + " verification failure due to wrong hash");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested verification.
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification failure due to wrong hash");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Test verification fails with bad hash name
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            // use the wrong name for the hash
+            var hashName = vector.hashName.substring(0, 3) + vector.hashName.substring(4);
+            var algorithm = {name: vector.algorithmName, hash: hashName};
+            promise_test(function(test) {
+                var operation = subtle.verify(algorithm, vector.publicKey, vector.signature, vector.plaintext)
+                .then(function(is_verified) {
+                    assert_unreached("Verification should throw an error");
+                }, function(err) {
+                    assert_equals(err.name, "NotSupportedError", "Correctly throws NotSupportedError for illegal hash name")
+                });
+
+                return operation;
+            }, vector.name + " verification failure due to bad hash name");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested verification.
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification failure due to bad hash name");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Test verification fails with short (odd length) signature
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            var algorithm = {name: vector.algorithmName, hash: vector.hashName};
+            var signature = vector.signature.slice(1); // Skip the first byte
+            promise_test(function(test) {
+                var operation = subtle.verify(algorithm, vector.publicKey, signature, vector.plaintext)
+                .then(function(is_verified) {
+                    assert_false(is_verified, "Signature NOT verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                return operation;
+            }, vector.name + " verification failure due to shortened signature");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested verification.
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification failure due to shortened signature");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Test verification fails with wrong plaintext
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            var algorithm = {name: vector.algorithmName, hash: vector.hashName};
+            var plaintext = copyBuffer(vector.plaintext);
+            plaintext[0] = 255 - plaintext[0];
+            promise_test(function(test) {
+                var operation = subtle.verify(algorithm, vector.publicKey, vector.signature, plaintext)
+                .then(function(is_verified) {
+                    assert_false(is_verified, "Signature NOT verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                return operation;
+            }, vector.name + " verification failure due to altered plaintext");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested verification.
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification failure due to altered plaintext");
+        });
+
+        all_promises.push(promise);
+    });
+
+
+    Promise.all(all_promises)
+    .then(function() {done();})
+    .catch(function() {done();})
+    return;
+
+    // A test vector has all needed fields for signing and verifying, EXCEPT that the
+    // key field may be null. This function replaces that null with the Correct
+    // CryptoKey object.
+    //
+    // Returns a Promise that yields an updated vector on success.
+    function importVectorKeys(vector, publicKeyUsages, privateKeyUsages) {
+        var publicPromise, privatePromise;
+
+        if (vector.publicKey !== null) {
+            publicPromise = new Promise(function(resolve, reject) {
+                resolve(vector);
+            });
+        } else {
+            publicPromise = subtle.importKey(vector.publicKeyFormat, vector.publicKeyBuffer, {name: vector.algorithmName, namedCurve: vector.namedCurve}, false, publicKeyUsages)
+            .then(function(key) {
+                vector.publicKey = key;
+                return vector;
+            });        // Returns a copy of the sourceBuffer it is sent.
+        }
+
+        if (vector.privateKey !== null) {
+            privatePromise = new Promise(function(resolve, reject) {
+                resolve(vector);
+            });
+        } else {
+            privatePromise = subtle.importKey(vector.privateKeyFormat, vector.privateKeyBuffer, {name: vector.algorithmName, namedCurve: vector.namedCurve}, false, privateKeyUsages)
+            .then(function(key) {
+                vector.privateKey = key;
+                return vector;
+            });
+        }
+
+        return Promise.all([publicPromise, privatePromise]);
+    }
+
+    // Returns a copy of the sourceBuffer it is sent.
+    function copyBuffer(sourceBuffer) {
+        var source = new Uint8Array(sourceBuffer);
+        var copy = new Uint8Array(sourceBuffer.byteLength)
+
+        for (var i=0; i<source.byteLength; i++) {
+            copy[i] = source[i];
+        }
+
+        return copy;
+    }
+
+    function equalBuffers(a, b) {
+        if (a.byteLength !== b.byteLength) {
+            return false;
+        }
+
+        var aBytes = new Uint8Array(a);
+        var bBytes = new Uint8Array(b);
+
+        for (var i=0; i<a.byteLength; i++) {
+            if (aBytes[i] !== bBytes[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    return;
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/ecdsa_vectors.js b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/ecdsa_vectors.js
new file mode 100644
index 0000000..e605821
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/ecdsa_vectors.js
@@ -0,0 +1,105 @@
+
+// ecdsa_vectors.js
+
+// Data for testing ECDSA with every curve currently in the WebCryptoAPI recommendation.
+
+// The following function returns an array of test vectors
+// for the subtleCrypto encrypt method.
+//
+// Each test vector has the following fields:
+//     name - a unique name for this vector
+//     publicKeyBuffer - an arrayBuffer with the key data
+//     publicKeyFormat - "spki" "jwk"
+//     publicKey - a CryptoKey object for the keyBuffer. INITIALLY null! You must fill this in first to use it!
+//     privateKeyBuffer - an arrayBuffer with the key data
+//     privateKeyFormat - "pkcs8" or "jwk"
+//     privateKey - a CryptoKey object for the keyBuffer. INITIALLY null! You must fill this in first to use it!
+//     algorithmName - the name of the AlgorithmIdentifier parameter to provide to encrypt
+//     namedCurve - the curve used
+//     hashName - the hash function to sign with
+//     plaintext - the text to encrypt
+//     signature - the expected signature
+function getTestVectors() {
+    var pkcs8 = {
+        "P-256": new Uint8Array([48, 129, 135, 2, 1, 0, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 4, 109, 48, 107, 2, 1, 1, 4, 32, 230, 238, 207, 158, 98, 108, 202, 142, 24, 7, 155, 146, 197, 238, 38, 158, 84, 202, 18, 142, 175, 212, 137, 71, 255, 81, 171, 160, 10, 192, 229, 214, 161, 68, 3, 66, 0, 4, 10, 5, 30, 56, 111, 103, 196, 166, 225, 229, 203, 238, 125, 55, 116, 91, 88, 142, 190, 114, 15, 117, 89, 22, 40, 111, 150, 41, 105, 122, 57, 23, 17, 216, 106, 234, 201, 103, 8, 210, 58, 38, 35, 216, 198, 237, 187, 84, 217, 164, 63, 100, 6, 105, 49, 128, 15, 53, 29, 158, 117, 235, 238, 30]),
+        "P-384": new Uint8Array([48, 129, 182, 2, 1, 0, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 34, 4, 129, 158, 48, 129, 155, 2, 1, 1, 4, 48, 2, 169, 160, 216, 153, 239, 168, 126, 117, 100, 17, 9, 7, 233, 216, 44, 33, 189, 98, 101, 163, 122, 189, 154, 111, 219, 15, 128, 236, 132, 77, 211, 161, 66, 83, 32, 214, 125, 220, 48, 245, 219, 116, 239, 185, 162, 230, 97, 161, 100, 3, 98, 0, 4, 29, 49, 157, 105, 45, 202, 95, 87, 84, 186, 123, 50, 193, 22, 66, 198, 216, 210, 180, 251, 130, 73, 195, 242, 20, 215, 30, 144, 181, 37, 41, 102, 217, 127, 123, 235, 31, 170, 177, 228, 243, 226, 96, 85, 73, 194, 238, 219, 82, 3, 41, 179, 190, 166, 181, 229, 86, 36, 161, 81, 80, 161, 105, 102, 99, 95, 25, 22, 239, 4, 221, 117, 142, 105, 64, 157, 6, 51, 203, 75, 37, 153, 65, 121, 178, 42, 118, 156, 116, 52, 54, 145, 14, 121, 153, 81]),
+        "P-521": new Uint8Array([48, 129, 238, 2, 1, 0, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 35, 4, 129, 214, 48, 129, 211, 2, 1, 1, 4, 66, 1, 83, 62, 97, 143, 152, 234, 209, 181, 19, 236, 136, 120, 200, 130, 13, 55, 122, 54, 216, 240, 63, 43, 160, 70, 201, 49, 130, 90, 61, 53, 135, 48, 192, 178, 96, 51, 219, 183, 247, 228, 163, 212, 67, 74, 3, 94, 36, 183, 7, 249, 18, 71, 102, 23, 110, 26, 240, 184, 93, 242, 46, 170, 186, 156, 37, 161, 129, 137, 3, 129, 134, 0, 4, 0, 166, 222, 236, 251, 72, 145, 23, 241, 228, 28, 196, 160, 100, 7, 61, 134, 115, 8, 110, 81, 219, 37, 8, 110, 141, 183, 100, 212, 239, 246, 10, 173, 99, 88, 253, 207, 150, 122, 198, 132, 89, 39, 94, 42, 128, 79, 142, 238, 183, 228, 196, 40, 75, 20, 81, 192, 165, 234, 118, 254, 112, 7, 172, 5, 71, 1, 197, 237, 218, 249, 168, 158, 124, 79, 220, 201, 36, 199, 55, 216, 245, 133, 218, 151, 3, 169, 84, 194, 59, 231, 193, 74, 175, 166, 102, 84, 178, 86, 119, 10, 147, 142, 127, 38, 231, 0, 198, 3, 147, 28, 123, 208, 189, 181, 208, 99, 44, 125, 30, 171, 70, 111, 9, 217, 118, 194, 74, 50, 62, 27, 124])
+    };
+
+    var spki = {
+        "P-256": new Uint8Array([48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 10, 5, 30, 56, 111, 103, 196, 166, 225, 229, 203, 238, 125, 55, 116, 91, 88, 142, 190, 114, 15, 117, 89, 22, 40, 111, 150, 41, 105, 122, 57, 23, 17, 216, 106, 234, 201, 103, 8, 210, 58, 38, 35, 216, 198, 237, 187, 84, 217, 164, 63, 100, 6, 105, 49, 128, 15, 53, 29, 158, 117, 235, 238, 30]),
+        "P-384": new Uint8Array([48, 118, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 34, 3, 98, 0, 4, 29, 49, 157, 105, 45, 202, 95, 87, 84, 186, 123, 50, 193, 22, 66, 198, 216, 210, 180, 251, 130, 73, 195, 242, 20, 215, 30, 144, 181, 37, 41, 102, 217, 127, 123, 235, 31, 170, 177, 228, 243, 226, 96, 85, 73, 194, 238, 219, 82, 3, 41, 179, 190, 166, 181, 229, 86, 36, 161, 81, 80, 161, 105, 102, 99, 95, 25, 22, 239, 4, 221, 117, 142, 105, 64, 157, 6, 51, 203, 75, 37, 153, 65, 121, 178, 42, 118, 156, 116, 52, 54, 145, 14, 121, 153, 81]),
+        "P-521": new Uint8Array([48, 129, 155, 48, 16, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 5, 43, 129, 4, 0, 35, 3, 129, 134, 0, 4, 0, 166, 222, 236, 251, 72, 145, 23, 241, 228, 28, 196, 160, 100, 7, 61, 134, 115, 8, 110, 81, 219, 37, 8, 110, 141, 183, 100, 212, 239, 246, 10, 173, 99, 88, 253, 207, 150, 122, 198, 132, 89, 39, 94, 42, 128, 79, 142, 238, 183, 228, 196, 40, 75, 20, 81, 192, 165, 234, 118, 254, 112, 7, 172, 5, 71, 1, 197, 237, 218, 249, 168, 158, 124, 79, 220, 201, 36, 199, 55, 216, 245, 133, 218, 151, 3, 169, 84, 194, 59, 231, 193, 74, 175, 166, 102, 84, 178, 86, 119, 10, 147, 142, 127, 38, 231, 0, 198, 3, 147, 28, 123, 208, 189, 181, 208, 99, 44, 125, 30, 171, 70, 111, 9, 217, 118, 194, 74, 50, 62, 27, 124])
+    };
+
+    // plaintext
+    var plaintext = new Uint8Array([95, 77, 186, 79, 50, 12, 12, 232, 118, 114, 90, 252, 229, 251, 210, 91, 248, 62, 90, 113, 37, 160, 140, 175, 231, 60, 62, 186, 196, 33, 119, 157, 249, 213, 93, 24, 12, 58, 233, 148, 38, 69, 225, 216, 47, 238, 140, 157, 41, 75, 60, 177, 160, 138, 153, 49, 32, 27, 60, 14, 129, 252, 71, 202, 207, 131, 21, 162, 175, 102, 50, 65, 19, 195, 182, 98, 48, 195, 70, 8, 196, 244, 89, 54, 52, 206, 2, 178, 103, 54, 34, 119, 240, 168, 64, 202, 116, 188, 61, 26, 98, 54, 149, 44, 94, 215, 170, 248, 168, 254, 203, 221, 250, 117, 132, 230, 151, 140, 234, 93, 42, 91, 159, 183, 241, 180, 140, 139, 11, 229, 138, 48, 82, 2, 117, 77, 131, 118, 16, 115, 116, 121, 60, 240, 38, 170, 238, 83, 0, 114, 125, 131, 108, 215, 30, 113, 179, 69, 221, 178, 228, 68, 70, 255, 197, 185, 1, 99, 84, 19, 137, 13, 145, 14, 163, 128, 152, 74, 144, 25, 16, 49, 50, 63, 22, 219, 204, 157, 107, 225, 104, 184, 72, 133, 56, 76, 160, 62, 18, 96, 10, 193, 194, 72, 2, 138, 243, 114, 108, 201, 52, 99, 136, 46, 168, 192, 42, 171]);
+
+    // For verification tests.
+    var signatures = {
+        "P-256": {
+            "SHA-1": new Uint8Array([172, 224, 125, 170, 52, 83, 158, 179, 85, 149, 130, 217, 59, 201, 0, 251, 237, 196, 51, 243, 218, 231, 211, 136, 157, 249, 219, 16, 140, 178, 145, 16, 177, 104, 68, 179, 88, 49, 219, 184, 212, 202, 109, 248, 110, 64, 202, 129, 7, 173, 226, 88, 194, 69, 164, 158, 120, 120, 128, 3, 115, 14, 181, 197]),
+            "SHA-256": new Uint8Array([83, 223, 63, 226, 42, 29, 106, 105, 225, 145, 197, 180, 118, 154, 109, 110, 66, 67, 47, 251, 53, 190, 203, 65, 207, 36, 19, 57, 49, 122, 124, 118, 59, 74, 222, 134, 42, 235, 180, 229, 134, 24, 205, 81, 171, 156, 100, 218, 127, 242, 126, 53, 27, 77, 249, 101, 157, 132, 244, 30, 67, 30, 64, 12]),
+            "SHA-384": new Uint8Array([235, 111, 173, 249, 151, 252, 218, 129, 123, 117, 136, 26, 162, 115, 247, 110, 169, 145, 95, 189, 228, 98, 32, 82, 34, 94, 154, 197, 47, 83, 124, 137, 215, 71, 222, 247, 135, 22, 221, 238, 77, 247, 223, 194, 42, 158, 175, 224, 76, 182, 56, 138, 97, 196, 238, 109, 42, 102, 13, 71, 1, 43, 56, 92]),
+            "SHA-512": new Uint8Array([74, 201, 175, 173, 69, 107, 160, 142, 203, 41, 225, 5, 73, 146, 6, 40, 93, 130, 129, 35, 156, 171, 190, 161, 12, 10, 234, 123, 7, 5, 112, 97, 57, 183, 15, 52, 94, 215, 79, 255, 175, 222, 66, 234, 253, 180, 62, 161, 7, 11, 249, 37, 118, 185, 13, 102, 67, 84, 101, 189, 73, 132, 110, 206])
+        },
+        "P-384": {
+            "SHA-1": new Uint8Array([101, 254, 7, 14, 195, 234, 195, 82, 80, 208, 11, 158, 230, 219, 77, 45, 173, 213, 243, 187, 185, 196, 149, 200, 103, 29, 42, 13, 43, 153, 20, 159, 178, 79, 136, 175, 7, 78, 11, 144, 50, 104, 179, 208, 237, 95, 14, 20, 104, 87, 150, 178, 143, 227, 75, 45, 142, 220, 223, 16, 132, 91, 36, 207, 121, 179, 54, 39, 216, 189, 44, 129, 98, 28, 181, 30, 3, 12, 33, 164, 58, 187, 10, 135, 64, 250, 194, 111, 133, 34, 230, 131, 195, 103, 172, 150]),
+            "SHA-256": new Uint8Array([75, 194, 223, 234, 59, 205, 164, 251, 180, 253, 146, 123, 3, 15, 155, 128, 177, 245, 210, 173, 155, 183, 170, 6, 41, 56, 105, 87, 113, 32, 178, 177, 208, 239, 17, 204, 217, 254, 208, 113, 74, 171, 54, 190, 246, 57, 40, 247, 132, 245, 60, 126, 9, 223, 147, 233, 179, 229, 176, 200, 131, 207, 114, 9, 81, 180, 254, 35, 130, 199, 132, 46, 220, 252, 212, 93, 149, 106, 114, 210, 154, 64, 48, 160, 56, 169, 0, 230, 247, 221, 133, 122, 86, 80, 211, 232]),
+            "SHA-384": new Uint8Array([13, 217, 194, 199, 240, 182, 244, 217, 50, 130, 84, 169, 2, 232, 115, 116, 179, 192, 146, 25, 94, 107, 226, 26, 161, 166, 220, 216, 235, 166, 15, 123, 11, 56, 196, 0, 109, 250, 33, 70, 212, 233, 253, 35, 220, 51, 97, 121, 151, 64, 23, 73, 58, 31, 79, 116, 238, 207, 228, 85, 190, 61, 169, 237, 153, 100, 29, 129, 97, 13, 254, 180, 104, 182, 7, 218, 148, 29, 87, 20, 231, 181, 26, 238, 44, 69, 170, 14, 156, 77, 160, 33, 178, 55, 0, 144]),
+            "SHA-512": new Uint8Array([114, 251, 219, 54, 159, 211, 76, 28, 84, 38, 77, 7, 244, 250, 205, 105, 176, 46, 66, 6, 248, 168, 187, 37, 155, 136, 42, 48, 92, 86, 253, 226, 211, 81, 7, 228, 147, 197, 60, 214, 180, 175, 11, 49, 48, 111, 77, 3, 253, 67, 207, 199, 98, 161, 3, 14, 23, 163, 215, 117, 69, 58, 18, 18, 177, 66, 159, 123, 61, 147, 6, 106, 95, 66, 161, 11, 19, 140, 209, 119, 220, 9, 97, 110, 130, 125, 89, 136, 34, 215, 141, 70, 39, 183, 84, 230])
+        },
+        "P-521": {
+            "SHA-1": new Uint8Array([1, 120, 26, 23, 166, 14, 67, 18, 105, 96, 253, 57, 110, 18, 16, 145, 108, 33, 21, 202, 68, 40, 217, 104, 56, 156, 75, 70, 193, 85, 54, 116, 206, 147, 123, 142, 33, 112, 12, 230, 9, 50, 174, 15, 87, 92, 161, 135, 221, 89, 119, 32, 219, 131, 158, 177, 242, 12, 126, 51, 148, 120, 117, 89, 220, 213, 0, 32, 126, 87, 13, 245, 199, 228, 173, 159, 192, 165, 247, 32, 101, 233, 206, 28, 158, 61, 18, 202, 94, 109, 217, 244, 79, 225, 40, 86, 27, 117, 244, 34, 108, 79, 173, 242, 61, 131, 83, 108, 198, 105, 234, 64, 152, 227, 115, 182, 203, 145, 156, 139, 92, 252, 5, 5, 166, 125, 150, 178, 118, 164, 106, 61]),
+            "SHA-256": new Uint8Array([1, 116, 219, 167, 123, 20, 215, 63, 102, 245, 113, 103, 134, 163, 229, 168, 215, 201, 49, 68, 94, 109, 50, 10, 146, 41, 217, 97, 216, 161, 179, 239, 209, 26, 94, 163, 60, 121, 73, 90, 197, 153, 187, 182, 138, 100, 26, 132, 157, 88, 216, 62, 248, 84, 204, 38, 95, 166, 201, 23, 223, 246, 238, 67, 90, 103, 1, 179, 213, 82, 125, 172, 32, 251, 10, 112, 51, 195, 254, 121, 116, 78, 172, 239, 123, 63, 252, 39, 182, 77, 200, 99, 248, 111, 66, 152, 44, 178, 34, 146, 69, 254, 157, 228, 138, 165, 158, 182, 83, 212, 73, 112, 134, 217, 17, 165, 189, 39, 14, 149, 197, 30, 126, 152, 247, 165, 134, 63, 199, 251, 6, 92]),
+            "SHA-384": new Uint8Array([1, 247, 125, 177, 229, 19, 120, 225, 23, 197, 184, 190, 200, 160, 63, 150, 87, 210, 68, 197, 78, 131, 121, 8, 191, 113, 1, 37, 95, 65, 81, 82, 93, 158, 137, 207, 127, 84, 99, 27, 51, 104, 145, 157, 56, 36, 255, 159, 127, 120, 254, 129, 35, 154, 26, 159, 222, 43, 122, 131, 233, 92, 166, 160, 202, 17, 1, 185, 139, 29, 164, 237, 0, 236, 118, 147, 103, 233, 149, 139, 128, 71, 212, 127, 146, 171, 139, 255, 150, 241, 51, 11, 249, 72, 201, 34, 9, 1, 27, 140, 219, 180, 150, 212, 100, 219, 185, 22, 114, 14, 183, 2, 189, 173, 146, 140, 153, 185, 128, 183, 101, 4, 224, 173, 28, 18, 180, 168, 87, 49, 199, 12]),
+            "SHA-512": new Uint8Array([0, 178, 202, 175, 103, 152, 81, 154, 157, 54, 219, 250, 254, 120, 107, 47, 186, 28, 194, 172, 185, 149, 147, 193, 119, 179, 110, 58, 28, 238, 183, 2, 39, 90, 226, 60, 252, 202, 10, 173, 120, 246, 182, 222, 230, 180, 113, 139, 149, 208, 209, 167, 21, 170, 51, 120, 71, 14, 80, 181, 22, 193, 142, 15, 51, 5, 1, 240, 7, 30, 106, 50, 134, 127, 167, 15, 105, 92, 211, 156, 78, 135, 225, 66, 185, 228, 19, 77, 56, 116, 11, 214, 254, 227, 84, 165, 117, 22, 126, 19, 82, 78, 148, 131, 38, 55, 145, 15, 225, 30, 83, 168, 95, 178, 27, 145, 173, 184, 27, 177, 119, 156, 78, 43, 139, 200, 124, 113, 125, 195, 80, 132])
+        }
+    }
+
+    // Old ASN.1 signatures below.
+    // var signatures = {
+    //     "P-256": {
+    //         "SHA-1": new Uint8Array([48, 70, 2, 33, 0, 189, 178, 29, 63, 162, 177, 41, 146, 224, 212, 75, 195, 12, 201, 193, 68, 61, 21, 122, 25, 40, 54, 22, 203, 197, 247, 160, 97, 3, 157, 35, 146, 2, 33, 0, 202, 253, 208, 131, 220, 167, 213, 121, 60, 56, 76, 111, 93, 197, 64, 54, 149, 82, 23, 255, 65, 206, 208, 154, 16, 52, 250, 3, 135, 178, 223, 248]),
+    //         "SHA-256": new Uint8Array([48, 68, 2, 32, 91, 78, 119, 119, 168, 102, 87, 56, 106, 33, 140, 190, 53, 232, 207, 81, 251, 156, 33, 85, 156, 6, 1, 183, 61, 254, 248, 113, 89, 191, 223, 202, 2, 32, 9, 130, 207, 194, 45, 48, 4, 134, 19, 133, 121, 124, 93, 141, 29, 63, 26, 0, 167, 132, 123, 80, 240, 184, 69, 182, 18, 111, 211, 211, 139, 209]),
+    //         "SHA-384": new Uint8Array([48, 69, 2, 32, 62, 124, 63, 100, 198, 132, 82, 37, 86, 53, 94, 121, 230, 167, 204, 146, 92, 56, 129, 66, 185, 242, 140, 181, 218, 239, 217, 133, 15, 166, 13, 86, 2, 33, 0, 164, 128, 5, 101, 173, 76, 227, 174, 140, 27, 28, 83, 80, 176, 202, 44, 0, 137, 37, 16, 150, 14, 29, 149, 22, 134, 1, 2, 45, 15, 91, 154]),
+    //         "SHA-512": new Uint8Array([48, 70, 2, 33, 0, 163, 149, 177, 250, 180, 46, 8, 35, 168, 219, 191, 25, 152, 174, 171, 100, 155, 171, 41, 170, 10, 113, 108, 160, 26, 11, 161, 69, 216, 74, 105, 155, 2, 33, 0, 236, 60, 103, 71, 26, 48, 70, 157, 54, 252, 27, 92, 152, 227, 103, 164, 153, 71, 71, 155, 103, 109, 38, 163, 158, 118, 238, 66, 50, 43, 29, 14])
+    //     },
+    //     "P-384": {
+    //         "SHA-1": new Uint8Array([48, 100, 2, 48, 95, 88, 156, 202, 5, 12, 93, 174, 109, 126, 105, 41, 101, 6, 111, 143, 36, 14, 7, 57, 84, 139, 59, 112, 224, 57, 250, 236, 77, 184, 59, 102, 21, 149, 236, 134, 202, 147, 140, 244, 27, 204, 55, 75, 109, 245, 40, 218, 2, 48, 25, 244, 151, 221, 217, 106, 152, 238, 40, 59, 188, 50, 235, 147, 226, 44, 121, 16, 69, 231, 204, 59, 42, 174, 23, 80, 130, 170, 204, 34, 208, 154, 135, 143, 164, 94, 62, 226, 14, 100, 213, 229, 40, 176, 31, 148, 125, 75]),
+    //         "SHA-256": new Uint8Array([48, 102, 2, 49, 0, 171, 16, 188, 253, 115, 108, 16, 69, 39, 187, 21, 188, 22, 86, 146, 2, 212, 145, 7, 120, 218, 186, 149, 139, 205, 55, 114, 208, 25, 183, 127, 2, 198, 234, 151, 193, 94, 12, 173, 170, 234, 130, 83, 193, 214, 110, 108, 72, 2, 49, 0, 136, 132, 142, 128, 157, 111, 141, 240, 49, 203, 203, 32, 121, 165, 57, 138, 81, 95, 64, 235, 251, 241, 59, 203, 214, 169, 17, 153, 112, 115, 91, 51, 66, 206, 172, 143, 39, 0, 217, 68, 242, 172, 86, 155, 174, 24, 39, 155]),
+    //         "SHA-384": new Uint8Array([48, 102, 2, 49, 0, 227, 80, 5, 74, 3, 89, 195, 243, 249, 127, 97, 9, 62, 159, 116, 170, 52, 181, 161, 160, 213, 16, 10, 137, 120, 40, 244, 151, 155, 52, 2, 111, 41, 199, 65, 146, 146, 121, 176, 101, 240, 37, 147, 163, 92, 102, 70, 79, 2, 49, 0, 223, 182, 48, 0, 17, 216, 189, 37, 249, 104, 74, 195, 177, 87, 106, 14, 127, 86, 0, 139, 238, 6, 13, 130, 146, 12, 26, 166, 204, 169, 194, 27, 81, 170, 212, 2, 128, 235, 59, 159, 120, 79, 141, 151, 188, 132, 170, 70]),
+    //         "SHA-512": new Uint8Array([48, 102, 2, 49, 0, 188, 136, 210, 146, 118, 251, 132, 224, 144, 121, 109, 86, 162, 216, 12, 148, 108, 169, 42, 79, 32, 152, 167, 20, 173, 176, 28, 67, 219, 93, 52, 167, 76, 140, 102, 244, 118, 146, 193, 134, 116, 26, 83, 43, 230, 241, 215, 135, 2, 49, 0, 178, 120, 154, 88, 189, 55, 9, 240, 26, 169, 201, 53, 83, 207, 11, 6, 83, 54, 194, 126, 249, 188, 189, 32, 88, 190, 228, 166, 66, 104, 103, 243, 64, 214, 153, 84, 80, 175, 20, 205, 9, 85, 74, 233, 90, 184, 240, 153])
+    //     },
+    //     "P-521": {
+    //         "SHA-1": new Uint8Array([48, 129, 136, 2, 66, 1, 0, 159, 229, 63, 6, 27, 187, 208, 6, 90, 246, 116, 10, 87, 207, 237, 166, 143, 68, 223, 98, 232, 90, 95, 143, 20, 240, 164, 112, 19, 199, 4, 203, 196, 231, 179, 203, 229, 64, 51, 58, 224, 124, 97, 41, 235, 202, 28, 201, 52, 61, 76, 166, 233, 197, 247, 58, 37, 115, 146, 150, 142, 108, 176, 94, 2, 66, 1, 4, 164, 11, 249, 164, 172, 86, 59, 39, 111, 61, 210, 100, 176, 168, 243, 146, 236, 28, 21, 25, 97, 28, 56, 201, 159, 24, 97, 217, 178, 5, 13, 221, 64, 6, 39, 168, 54, 129, 3, 86, 157, 104, 87, 241, 92, 158, 142, 170, 27, 126, 138, 255, 44, 33, 161, 49, 192, 230, 186, 70, 42, 189, 124, 5]),
+    //         "SHA-256": new Uint8Array([48, 129, 134, 2, 65, 115, 189, 109, 44, 118, 67, 34, 176, 16, 126, 246, 157, 34, 188, 209, 65, 231, 207, 180, 139, 53, 97, 110, 157, 19, 55, 35, 134, 90, 160, 20, 252, 130, 210, 179, 22, 76, 3, 142, 212, 71, 48, 251, 64, 18, 148, 199, 234, 163, 193, 120, 13, 153, 63, 174, 253, 58, 34, 130, 88, 138, 194, 248, 173, 53, 2, 65, 63, 0, 229, 139, 245, 33, 197, 245, 98, 139, 59, 87, 144, 16, 220, 183, 237, 125, 136, 134, 143, 146, 195, 0, 209, 105, 217, 20, 121, 76, 64, 87, 232, 86, 87, 136, 117, 237, 39, 83, 248, 3, 50, 236, 152, 121, 37, 116, 93, 91, 241, 235, 152, 95, 177, 217, 45, 247, 66, 193, 248, 131, 205, 132, 74]),
+    //         "SHA-384": new Uint8Array([48, 129, 136, 2, 66, 0, 252, 248, 24, 253, 24, 36, 120, 84, 72, 47, 246, 13, 78, 112, 200, 131, 7, 131, 73, 235, 36, 93, 54, 219, 233, 242, 85, 1, 198, 187, 17, 17, 109, 13, 47, 204, 137, 224, 17, 6, 225, 178, 133, 98, 248, 53, 151, 33, 230, 160, 42, 208, 30, 230, 154, 108, 227, 123, 216, 215, 35, 179, 17, 91, 187, 2, 66, 1, 110, 43, 180, 40, 222, 59, 177, 3, 70, 177, 175, 118, 222, 31, 1, 46, 196, 237, 187, 15, 96, 241, 216, 136, 195, 194, 45, 163, 194, 92, 159, 179, 101, 194, 90, 141, 78, 28, 31, 199, 233, 228, 180, 223, 23, 171, 62, 247, 157, 62, 126, 90, 198, 132, 197, 34, 140, 227, 79, 190, 153, 137, 225, 226, 32]),
+    //         "SHA-512": new Uint8Array([48, 129, 136, 2, 66, 0, 228, 69, 122, 14, 172, 82, 52, 181, 42, 214, 42, 107, 227, 154, 253, 177, 145, 236, 231, 251, 71, 46, 202, 46, 59, 63, 76, 195, 63, 130, 8, 50, 116, 179, 181, 203, 234, 27, 203, 55, 188, 239, 122, 107, 167, 163, 190, 141, 174, 35, 22, 176, 173, 157, 212, 49, 21, 69, 72, 100, 78, 131, 147, 57, 223, 2, 66, 1, 107, 241, 89, 194, 8, 164, 44, 33, 11, 173, 236, 115, 153, 16, 90, 155, 164, 247, 232, 18, 226, 223, 62, 75, 246, 178, 66, 176, 51, 74, 161, 74, 76, 14, 227, 217, 19, 114, 36, 76, 168, 151, 191, 20, 58, 179, 162, 205, 140, 156, 227, 88, 59, 161, 245, 61, 170, 211, 254, 99, 120, 17, 174, 175, 52])
+    //     }
+    // };
+
+    var vectors = [];
+    ["P-256", "P-384", "P-521"].forEach(function(curveName) {
+        ["SHA-1", "SHA-256", "SHA-384", "SHA-512"].forEach(function(hashName) {
+            var vector = {
+                name: "ECDSA " + curveName + " with " + hashName,
+                publicKeyBuffer: spki[curveName],
+                publicKeyFormat: "spki",
+                publicKey: null,
+                privateKeyBuffer: pkcs8[curveName],
+                privateKeyFormat: "pkcs8",
+                privateKey: null,
+                algorithmName: "ECDSA",
+                namedCurve: curveName,
+                hashName: hashName,
+                plaintext: plaintext,
+                signature: signatures[curveName][hashName]
+            };
+
+            vectors.push(vector);
+        })
+    });
+
+    return vectors;
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/hmac.https.worker.js b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/hmac.https.worker.js
new file mode 100644
index 0000000..79d8f21
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/hmac.https.worker.js
@@ -0,0 +1,5 @@
+importScripts("/resources/testharness.js");
+importScripts("hmac_vectors.js");
+importScripts("hmac.js");
+
+run_test();
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/hmac.js b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/hmac.js
new file mode 100644
index 0000000..e8f9ec5
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/hmac.js
@@ -0,0 +1,349 @@
+
+function run_test() {
+    setup({explicit_done: true});
+
+    var subtle = self.crypto.subtle; // Change to test prefixed implementations
+
+    // When are all these tests really done? When all the promises they use have resolved.
+    var all_promises = [];
+
+    // Source file hmac_vectors.js provides the getTestVectors method
+    // for the algorithm that drives these tests.
+    var testVectors = getTestVectors();
+
+    // Test verification first, because signing tests rely on that working
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify", "sign"])
+        .then(function(vector) {
+            promise_test(function(test) {
+                var operation = subtle.verify({name: "HMAC", hash: vector.hash}, vector.key, vector.signature, vector.plaintext)
+                .then(function(is_verified) {
+                    assert_true(is_verified, "Signature verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                return operation;
+            }, vector.name + " verification");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested verification.
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Test verification with an altered buffer after call
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify", "sign"])
+        .then(function(vector) {
+            promise_test(function(test) {
+                var signature = copyBuffer(vector.signature);
+                var operation = subtle.verify({name: "HMAC", hash: vector.hash}, vector.key, signature, vector.plaintext)
+                .then(function(is_verified) {
+                    assert_true(is_verified, "Signature is not verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                signature[0] = 255 - signature[0];
+                return operation;
+            }, vector.name + " verification with altered signature after call");
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification with altered signature after call");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for successful verification even if plaintext is altered after call.
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify", "sign"])
+        .then(function(vector) {
+            promise_test(function(test) {
+                var plaintext = copyBuffer(vector.plaintext);
+                var operation = subtle.verify({name: "HMAC", hash: vector.hash}, vector.key, vector.signature, plaintext)
+                .then(function(is_verified) {
+                    assert_true(is_verified, "Signature verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                plaintext[0] = 255 - plaintext[0];
+                return operation;
+            }, vector.name + " with altered plaintext after call");
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " with altered plaintext");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for failures due to no "verify" usage.
+    testVectors.forEach(function(originalVector) {
+        var vector = Object.assign({}, originalVector);
+
+        var promise = importVectorKeys(vector, ["sign"])
+        .then(function(vector) {
+            promise_test(function(test) {
+                return subtle.verify({name: "HMAC", hash: vector.hash}, vector.key, vector.signature, vector.plaintext)
+                .then(function(plaintext) {
+                    assert_unreached("Should have thrown error for no verify usage in " + vector.name + ": " + err.message + "'");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw InvalidAccessError instead of '" + err.message + "'");
+                });
+            }, vector.name + " no verify usage");
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " no verify usage");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for successful signing and verification.
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify", "sign"])
+        .then(function(vectors) {
+            promise_test(function(test) {
+                return subtle.sign({name: "HMAC", hash: vector.hash}, vector.key, vector.plaintext)
+                .then(function(signature) {
+                    // Can we get the verify the new signature?
+                    return subtle.verify({name: "HMAC", hash: vector.hash}, vector.key, signature, vector.plaintext)
+                    .then(function(is_verified) {
+                        assert_true(is_verified, "Round trip verifies");
+                        return signature;
+                    }, function(err) {
+                        assert_unreached("verify error for test " + vector.name + ": " + err.message + "'");
+                    });
+                });
+            }, vector.name + " round trip");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested signing or verifying
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " round trip");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Test signing with the wrong algorithm
+    testVectors.forEach(function(vector) {
+        // Want to get the key for the wrong algorithm
+        var promise = subtle.generateKey({name: "ECDSA", namedCurve: "P-256", hash: "SHA-256"}, false, ["sign", "verify"])
+        .then(function(wrongKey) {
+            return importVectorKeys(vector, ["verify", "sign"])
+            .then(function(vectors) {
+                promise_test(function(test) {
+                    var operation = subtle.sign({name: "HMAC", hash: vector.hash}, wrongKey.privateKey, vector.plaintext)
+                    .then(function(signature) {
+                        assert_unreached("Signing should not have succeeded for " + vector.name);
+                    }, function(err) {
+                        assert_equals(err.name, "InvalidAccessError", "Should have thrown InvalidAccessError instead of '" + err.message + "'");
+                    });
+
+                    return operation;
+                }, vector.name + " signing with wrong algorithm name");
+
+            }, function(err) {
+                // We need a failed test if the importVectorKey operation fails, so
+                // we know we never tested verification.
+                promise_test(function(test) {
+                    assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+                }, "importVectorKeys step: " + vector.name + " signing with wrong algorithm name");
+            });
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("Generate wrong key for test " + vector.name + " failed: '" + err.message + "'");
+            }, "generate wrong key step: " + vector.name + " signing with wrong algorithm name");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Test verification with the wrong algorithm
+    testVectors.forEach(function(vector) {
+        // Want to get the key for the wrong algorithm
+        var promise = subtle.generateKey({name: "ECDSA", namedCurve: "P-256", hash: "SHA-256"}, false, ["sign", "verify"])
+        .then(function(wrongKey) {
+            return importVectorKeys(vector, ["verify", "sign"])
+            .then(function(vector) {
+                promise_test(function(test) {
+                    var operation = subtle.verify({name: "HMAC", hash: vector.hash}, wrongKey.publicKey, vector.signature, vector.plaintext)
+                    .then(function(signature) {
+                        assert_unreached("Verifying should not have succeeded for " + vector.name);
+                    }, function(err) {
+                        assert_equals(err.name, "InvalidAccessError", "Should have thrown InvalidAccessError instead of '" + err.message + "'");
+                    });
+
+                    return operation;
+                }, vector.name + " verifying with wrong algorithm name");
+
+            }, function(err) {
+                // We need a failed test if the importVectorKey operation fails, so
+                // we know we never tested verification.
+                promise_test(function(test) {
+                    assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+                }, "importVectorKeys step: " + vector.name + " verifying with wrong algorithm name");
+            });
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("Generate wrong key for test " + vector.name + " failed: '" + err.message + "'");
+            }, "generate wrong key step: " + vector.name + " verifying with wrong algorithm name");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Verification should fail if the plaintext is changed
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify", "sign"])
+        .then(function(vector) {
+            var plaintext = copyBuffer(vector.plaintext);
+            plaintext[0] = 255 - plaintext[0];
+            promise_test(function(test) {
+                var operation = subtle.verify({name: "HMAC", hash: vector.hash}, vector.key, vector.signature, plaintext)
+                .then(function(is_verified) {
+                    assert_false(is_verified, "Signature is NOT verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                return operation;
+            }, vector.name + " verification failure due to wrong plaintext");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested verification.
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification failure due to wrong plaintext");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Verification should fail if the signature is changed
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify", "sign"])
+        .then(function(vector) {
+            var signature = copyBuffer(vector.signature);
+            signature[0] = 255 - signature[0];
+            promise_test(function(test) {
+                var operation = subtle.verify({name: "HMAC", hash: vector.hash}, vector.key, signature, vector.plaintext)
+                .then(function(is_verified) {
+                    assert_false(is_verified, "Signature is NOT verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                return operation;
+            }, vector.name + " verification failure due to wrong signature");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested verification.
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification failure due to wrong signature");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Verification should fail if the signature is wrong length
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify", "sign"])
+        .then(function(vector) {
+            var signature = vector.signature.slice(1); // Drop first byte
+            promise_test(function(test) {
+                var operation = subtle.verify({name: "HMAC", hash: vector.hash}, vector.key, signature, vector.plaintext)
+                .then(function(is_verified) {
+                    assert_false(is_verified, "Signature is NOT verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                return operation;
+            }, vector.name + " verification failure due to short signature");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested verification.
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification failure due to short signature");
+        });
+
+        all_promises.push(promise);
+    });
+
+
+
+    Promise.all(all_promises)
+    .then(function() {done();})
+    .catch(function() {done();})
+    return;
+
+    // A test vector has all needed fields for signing and verifying, EXCEPT that the
+    // key field may be null. This function replaces that null with the Correct
+    // CryptoKey object.
+    //
+    // Returns a Promise that yields an updated vector on success.
+    function importVectorKeys(vector, keyUsages) {
+        if (vector.key !== null) {
+            return new Promise(function(resolve, reject) {
+                resolve(vector);
+            });
+        } else {
+            return subtle.importKey("raw", vector.keyBuffer, {name: "HMAC", hash: vector.hash}, false, keyUsages)
+            .then(function(key) {
+                vector.key = key;
+                return vector;
+            });
+        }
+    }
+
+    // Returns a copy of the sourceBuffer it is sent.
+    function copyBuffer(sourceBuffer) {
+        var source = new Uint8Array(sourceBuffer);
+        var copy = new Uint8Array(sourceBuffer.byteLength)
+
+        for (var i=0; i<source.byteLength; i++) {
+            copy[i] = source[i];
+        }
+
+        return copy;
+    }
+
+    function equalBuffers(a, b) {
+        if (a.byteLength !== b.byteLength) {
+            return false;
+        }
+
+        var aBytes = new Uint8Array(a);
+        var bBytes = new Uint8Array(b);
+
+        for (var i=0; i<a.byteLength; i++) {
+            if (aBytes[i] !== bBytes[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    return;
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/hmac_vectors.js b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/hmac_vectors.js
new file mode 100644
index 0000000..de9642b
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/hmac_vectors.js
@@ -0,0 +1,39 @@
+
+function getTestVectors() {
+    var plaintext = new Uint8Array([95, 77, 186, 79, 50, 12, 12, 232, 118, 114, 90, 252, 229, 251, 210, 91, 248, 62, 90, 113, 37, 160, 140, 175, 231, 60, 62, 186, 196, 33, 119, 157, 249, 213, 93, 24, 12, 58, 233, 148, 38, 69, 225, 216, 47, 238, 140, 157, 41, 75, 60, 177, 160, 138, 153, 49, 32, 27, 60, 14, 129, 252, 71, 202, 207, 131, 21, 162, 175, 102, 50, 65, 19, 195, 182, 98, 48, 195, 70, 8, 196, 244, 89, 54, 52, 206, 2, 178, 103, 54, 34, 119, 240, 168, 64, 202, 116, 188, 61, 26, 98, 54, 149, 44, 94, 215, 170, 248, 168, 254, 203, 221, 250, 117, 132, 230, 151, 140, 234, 93, 42, 91, 159, 183, 241, 180, 140, 139, 11, 229, 138, 48, 82, 2, 117, 77, 131, 118, 16, 115, 116, 121, 60, 240, 38, 170, 238, 83, 0, 114, 125, 131, 108, 215, 30, 113, 179, 69, 221, 178, 228, 68, 70, 255, 197, 185, 1, 99, 84, 19, 137, 13, 145, 14, 163, 128, 152, 74, 144, 25, 16, 49, 50, 63, 22, 219, 204, 157, 107, 225, 104, 184, 72, 133, 56, 76, 160, 62, 18, 96, 10, 193, 194, 72, 2, 138, 243, 114, 108, 201, 52, 99, 136, 46, 168, 192, 42, 171]);
+
+    var raw = {
+        "SHA-1": new Uint8Array([71, 162, 7, 70, 209, 113, 121, 219, 101, 224, 167, 157, 237, 255, 199, 253, 241, 129, 8, 27]),
+        "SHA-256": new Uint8Array([229, 136, 236, 8, 17, 70, 61, 118, 114, 65, 223, 16, 116, 180, 122, 228, 7, 27, 81, 242, 206, 54, 83, 123, 166, 156, 205, 195, 253, 194, 183, 168]),
+        "SHA-384": new Uint8Array([107, 29, 162, 142, 171, 31, 88, 42, 217, 113, 142, 255, 224, 94, 35, 213, 253, 44, 152, 119, 162, 217, 68, 63, 144, 190, 192, 147, 190, 206, 46, 167, 210, 53, 76, 208, 189, 197, 225, 71, 210, 233, 0, 147, 115, 73, 68, 136]),
+        "SHA-512": new Uint8Array([93, 204, 53, 148, 67, 170, 246, 82, 250, 19, 117, 214, 179, 230, 31, 220, 242, 155, 180, 162, 139, 213, 211, 220, 250, 64, 248, 47, 144, 107, 178, 128, 4, 85, 219, 3, 181, 211, 31, 185, 114, 161, 90, 109, 1, 3, 162, 78, 86, 209, 86, 161, 25, 192, 229, 161, 233, 42, 68, 195, 197, 101, 124, 249])
+    };
+
+    var signatures = {
+        "SHA-1": new Uint8Array([5, 51, 144, 42, 153, 248, 82, 78, 229, 10, 240, 29, 56, 222, 220, 225, 51, 217, 140, 160]),
+        "SHA-256": new Uint8Array([133, 164, 12, 234, 46, 7, 140, 40, 39, 163, 149, 63, 251, 102, 194, 123, 41, 26, 71, 43, 13, 112, 160, 0, 11, 69, 216, 35, 128, 62, 235, 84]),
+        "SHA-384": new Uint8Array([33, 124, 61, 80, 240, 186, 154, 109, 110, 174, 30, 253, 215, 165, 24, 254, 46, 56, 128, 181, 130, 164, 13, 6, 30, 144, 153, 193, 224, 38, 239, 88, 130, 84, 139, 93, 92, 236, 221, 85, 152, 217, 155, 107, 111, 48, 87, 255]),
+        "SHA-512": new Uint8Array([97, 251, 39, 140, 63, 251, 12, 206, 43, 241, 207, 114, 61, 223, 216, 239, 31, 147, 28, 12, 97, 140, 37, 144, 115, 36, 96, 89, 57, 227, 249, 162, 198, 244, 175, 105, 11, 218, 52, 7, 220, 47, 87, 112, 246, 160, 164, 75, 149, 77, 100, 163, 50, 227, 238, 8, 33, 171, 248, 43, 127, 62, 153, 193])
+    };
+
+    // Each test vector has the following fields:
+    //     name - a unique name for this vector
+    //     keyBuffer - an arrayBuffer with the key data
+    //     key - a CryptoKey object for the keyBuffer. INITIALLY null! You must fill this in first to use it!
+    //     hashName - the hash function to sign with
+    //     plaintext - the text to encrypt
+    //     signature - the expected signature
+    var vectors = [];
+    Object.keys(raw).forEach(function(hashName) {
+        vectors.push({
+            name: "HMAC with " + hashName,
+            hash: hashName,
+            keyBuffer: raw[hashName],
+            key: null,
+            plaintext: plaintext,
+            signature: signatures[hashName]
+        });
+    });
+
+    return vectors;
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/rsa.js b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/rsa.js
new file mode 100644
index 0000000..251f57e
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/rsa.js
@@ -0,0 +1,401 @@
+
+function run_test() {
+    setup({explicit_done: true});
+
+    var subtle = self.crypto.subtle; // Change to test prefixed implementations
+
+    // When are all these tests really done? When all the promises they use have resolved.
+    var all_promises = [];
+
+    // Source file [algorithm_name]_vectors.js provides the getTestVectors method
+    // for the algorithm that drives these tests.
+    var testVectors = getTestVectors();
+
+    // Test verification first, because signing tests rely on that working
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            promise_test(function(test) {
+                var operation = subtle.verify(vector.algorithm, vector.publicKey, vector.signature, vector.plaintext)
+                .then(function(is_verified) {
+                    assert_true(is_verified, "Signature verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                return operation;
+            }, vector.name + " verification");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested verification.
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Test verification with an altered buffer after call
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            promise_test(function(test) {
+                var signature = copyBuffer(vector.signature);
+                var operation = subtle.verify(vector.algorithm, vector.publicKey, signature, vector.plaintext)
+                .then(function(is_verified) {
+                    assert_true(is_verified, "Signature verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                signature[0] = 255 - signature[0];
+                return operation;
+            }, vector.name + " verification with altered signature after call");
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification with altered signature after call");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for successful verification even if plaintext is altered after call.
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            promise_test(function(test) {
+                var plaintext = copyBuffer(vector.plaintext);
+                var operation = subtle.verify(vector.algorithm, vector.publicKey, vector.signature, plaintext)
+                .then(function(is_verified) {
+                    assert_true(is_verified, "Signature verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                plaintext[0] = 255 - plaintext[0];
+                return operation;
+            }, vector.name + " with altered plaintext after call");
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " with altered plaintext after call");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for failures due to using privateKey to verify.
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            promise_test(function(test) {
+                return subtle.verify(vector.algorithm, vector.privateKey, vector.signature, vector.plaintext)
+                .then(function(plaintext) {
+                    assert_unreached("Should have thrown error for using privateKey to verify in " + vector.name + ": " + err.message + "'");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw InvalidAccessError instead of '" + err.message + "'");
+                });
+            }, vector.name + " using privateKey to verify");
+
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " using privateKey to verify");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for failures due to using publicKey to sign.
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            promise_test(function(test) {
+                return subtle.sign(vector.algorithm, vector.publicKey, vector.plaintext)
+                .then(function(signature) {
+                    assert_unreached("Should have thrown error for using publicKey to sign in " + vector.name + ": " + err.message + "'");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw InvalidAccessError instead of '" + err.message + "'");
+                });
+            }, vector.name + " using publicKey to sign");
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " using publicKey to sign");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for failures due to no "verify" usage.
+    testVectors.forEach(function(originalVector) {
+        var vector = Object.assign({}, originalVector);
+
+        var promise = importVectorKeys(vector, [], ["sign"])
+        .then(function(vectors) {
+            promise_test(function(test) {
+                return subtle.verify(vector.algorithm, vector.publicKey, vector.signature, vector.plaintext)
+                .then(function(plaintext) {
+                    assert_unreached("Should have thrown error for no verify usage in " + vector.name + ": " + err.message + "'");
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should throw InvalidAccessError instead of '" + err.message + "'");
+                });
+            }, vector.name + " no verify usage");
+        }, function(err) {
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " no verify usage");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Check for successful signing and verification.
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            promise_test(function(test) {
+                return subtle.sign(vector.algorithm, vector.privateKey, vector.plaintext)
+                .then(function(signature) {
+                    // Can we verify the new signature?
+                    return subtle.verify(vector.algorithm, vector.publicKey, signature, vector.plaintext)
+                    .then(function(is_verified) {
+                        assert_true(is_verified, "Round trip verifies");
+                        return signature;
+                    }, function(err) {
+                        assert_unreached("verify error for test " + vector.name + ": " + err.message + "'");
+                    });
+                })
+                .then(function(priorSignature) {
+                    // Will a second signing give us different signature? It should for PSS with non-empty salt
+                    return subtle.sign(vector.algorithm, vector.privateKey, vector.plaintext)
+                    .then(function(signature) {
+                        if ("saltLength" in vector.algorithm && vector.algorithm.saltLength > 0) {
+                            assert_false(equalBuffers(priorSignature, signature), "Two signings with a salt give different signatures")
+                        } else {
+                            assert_true(equalBuffers(priorSignature, signature), "Two signings with empty salt give same signature")
+                        }
+                    }, function(err) {
+                        assert_unreached("second time verify error for test " + vector.name + ": '" + err.message + "'");
+                    });
+                }, function(err) {
+                    assert_unreached("sign error for test " + vector.name + ": '" + err.message + "'");
+                });
+            }, vector.name + " round trip");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested signing or verifying
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " round trip");
+        });
+
+        all_promises.push(promise);
+    });
+
+
+    // Test signing with the wrong algorithm
+    testVectors.forEach(function(vector) {
+        // Want to get the key for the wrong algorithm
+        var alteredVector = Object.assign({}, vector);
+        alteredVector.algorithm = Object.assign({}, vector.algorithm);
+        if (vector.algorithm.name === "RSA-PSS") {
+            alteredVector.algorithm.name = "RSASSA-PKCS1-v1_5";
+        } else {
+            alteredVector.algorithm.name = "RSA-PSS";
+        }
+
+        var promise = importVectorKeys(alteredVector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            promise_test(function(test) {
+                var operation = subtle.sign(vector.algorithm, alteredVector.privateKey, vector.plaintext)
+                .then(function(signature) {
+                    assert_unreached("Signing should not have succeeded for " + vector.name);
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should have thrown InvalidAccessError instead of '" + err.message + "'");
+                });
+
+                return operation;
+            }, vector.name + " signing with wrong algorithm name");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested verification.
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " signing with wrong algorithm name");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Test verification with the wrong algorithm
+    testVectors.forEach(function(vector) {
+        // Want to get the key for the wrong algorithm
+        var alteredVector = Object.assign({}, vector);
+        alteredVector.algorithm = Object.assign({}, vector.algorithm);
+        if (vector.algorithm.name === "RSA-PSS") {
+            alteredVector.algorithm.name = "RSASSA-PKCS1-v1_5";
+        } else {
+            alteredVector.algorithm.name = "RSA-PSS";
+        }
+
+        var promise = importVectorKeys(alteredVector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            // Some tests are sign only
+            if (!("signature" in vector)) {
+                return;
+            }
+            promise_test(function(test) {
+                var operation = subtle.verify(vector.algorithm, alteredVector.publicKey, vector.signature, vector.plaintext)
+                .then(function(is_verified) {
+                    assert_unreached("Verification should not have succeeded for " + vector.name);
+                }, function(err) {
+                    assert_equals(err.name, "InvalidAccessError", "Should have thrown InvalidAccessError instead of '" + err.message + "'");
+                });
+
+                return operation;
+            }, vector.name + " verification with wrong algorithm name");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested verification.
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification with wrong algorithm name");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Verification should fail with wrong signature
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            promise_test(function(test) {
+                var signature = copyBuffer(vector.signature);
+                signature[0] = 255 - signature[0];
+                var operation = subtle.verify(vector.algorithm, vector.publicKey, signature, vector.plaintext)
+                .then(function(is_verified) {
+                    assert_false(is_verified, "Signature NOT verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                return operation;
+            }, vector.name + " verification failure with altered signature");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested verification.
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification failure with altered signature");
+        });
+
+        all_promises.push(promise);
+    });
+
+    // Verification should fail with wrong plaintext
+    testVectors.forEach(function(vector) {
+        var promise = importVectorKeys(vector, ["verify"], ["sign"])
+        .then(function(vectors) {
+            promise_test(function(test) {
+                var plaintext = copyBuffer(vector.plaintext);
+                plaintext[0] = 255 - plaintext[0];
+                var operation = subtle.verify(vector.algorithm, vector.publicKey, vector.signature, plaintext)
+                .then(function(is_verified) {
+                    assert_false(is_verified, "Signature NOT verified");
+                }, function(err) {
+                    assert_unreached("Verification should not throw error " + vector.name + ": " + err.message + "'");
+                });
+
+                return operation;
+            }, vector.name + " verification failure with altered plaintext");
+
+        }, function(err) {
+            // We need a failed test if the importVectorKey operation fails, so
+            // we know we never tested verification.
+            promise_test(function(test) {
+                assert_unreached("importVectorKeys failed for " + vector.name + ". Message: ''" + err.message + "''");
+            }, "importVectorKeys step: " + vector.name + " verification failure with altered plaintext");
+        });
+
+        all_promises.push(promise);
+    });
+
+
+    Promise.all(all_promises)
+    .then(function() {done();})
+    .catch(function() {done();})
+
+    // A test vector has all needed fields for signing and verifying, EXCEPT that the
+    // key field may be null. This function replaces that null with the Correct
+    // CryptoKey object.
+    //
+    // Returns a Promise that yields an updated vector on success.
+    function importVectorKeys(vector, publicKeyUsages, privateKeyUsages) {
+        var publicPromise, privatePromise;
+
+        if (vector.publicKey !== null) {
+            publicPromise = new Promise(function(resolve, reject) {
+                resolve(vector);
+            });
+        } else {
+            publicPromise = subtle.importKey(vector.publicKeyFormat, vector.publicKeyBuffer, {name: vector.algorithm.name, hash: vector.hash}, false, publicKeyUsages)
+            .then(function(key) {
+                vector.publicKey = key;
+                return vector;
+            });        // Returns a copy of the sourceBuffer it is sent.
+        }
+
+        if (vector.privateKey !== null) {
+            privatePromise = new Promise(function(resolve, reject) {
+                resolve(vector);
+            });
+        } else {
+            privatePromise = subtle.importKey(vector.privateKeyFormat, vector.privateKeyBuffer, {name: vector.algorithm.name, hash: vector.hash}, false, privateKeyUsages)
+            .then(function(key) {
+                vector.privateKey = key;
+                return vector;
+            });
+        }
+
+        return Promise.all([publicPromise, privatePromise]);
+    }
+
+    // Returns a copy of the sourceBuffer it is sent.
+    function copyBuffer(sourceBuffer) {
+        var source = new Uint8Array(sourceBuffer);
+        var copy = new Uint8Array(sourceBuffer.byteLength)
+
+        for (var i=0; i<source.byteLength; i++) {
+            copy[i] = source[i];
+        }
+
+        return copy;
+    }
+
+    function equalBuffers(a, b) {
+        if (a.byteLength !== b.byteLength) {
+            return false;
+        }
+
+        var aBytes = new Uint8Array(a);
+        var bBytes = new Uint8Array(b);
+
+        for (var i=0; i<a.byteLength; i++) {
+            if (aBytes[i] !== bBytes[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    return;
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/rsa_pkcs.https.worker.js b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/rsa_pkcs.https.worker.js
new file mode 100644
index 0000000..6b4dbee
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/rsa_pkcs.https.worker.js
@@ -0,0 +1,5 @@
+importScripts("/resources/testharness.js");
+importScripts("rsa_pkcs_vectors.js");
+importScripts("rsa.js");
+
+run_test();
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/rsa_pkcs_vectors.js b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/rsa_pkcs_vectors.js
new file mode 100644
index 0000000..71e5d85
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/rsa_pkcs_vectors.js
@@ -0,0 +1,92 @@
+
+// rsa_pkcs_vectors.js
+
+// Data for testing RSASSA-PKCS1-v1_5 with a 2048-bit modulus and 65537 public exponent.
+
+// The following function returns an array of test vectors
+// for the subtleCrypto encrypt method.
+//
+// Each test vector has the following fields:
+//     name - a unique name for this vector
+//     publicKeyBuffer - an arrayBuffer with the key data
+//     publicKeyFormat - "spki" "jwk"
+//     publicKey - a CryptoKey object for the keyBuffer. INITIALLY null! You must fill this in first to use it!
+//     privateKeyBuffer - an arrayBuffer with the key data
+//     privateKeyFormat - "pkcs8" or "jwk"
+//     privateKey - a CryptoKey object for the keyBuffer. INITIALLY null! You must fill this in first to use it!
+//     algorithm - the value of the AlgorithmIdentifier parameter to provide to encrypt
+//     plaintext - the text to encrypt
+//     signature - the expected signature
+function getTestVectors() {
+    var pkcs8 = new Uint8Array([48, 130, 4, 191, 2, 1, 0, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 4, 130, 4, 169, 48, 130, 4, 165, 2, 1, 0, 2, 130, 1, 1, 0, 211, 87, 96, 146, 230, 41, 87, 54, 69, 68, 231, 228, 35, 59, 123, 219, 41, 61, 178, 8, 81, 34, 196, 121, 50, 133, 70, 249, 240, 247, 18, 246, 87, 196, 177, 120, 104, 201, 48, 144, 140, 197, 148, 247, 237, 0, 192, 20, 66, 193, 175, 4, 194, 246, 120, 164, 139, 162, 200, 15, 209, 113, 62, 48, 181, 172, 80, 120, 122, 195, 81, 101, 137, 241, 113, 150, 127, 99, 134, 173, 163, 73, 0, 166, 187, 4, 238, 206, 164, 43, 240, 67, 206, 217, 160, 249, 77, 12, 192, 158, 145, 155, 157, 113, 102, 192, 138, 182, 206, 32, 70, 64, 174, 164, 196, 146, 13, 182, 216, 110, 185, 22, 208, 220, 192, 244, 52, 26, 16, 56, 4, 41, 231, 225, 3, 33, 68, 234, 148, 157, 232, 246, 192, 204, 191, 149, 250, 142, 146, 141, 112, 216, 163, 140, 225, 104, 219, 69, 246, 241, 52, 102, 61, 111, 101, 111, 92, 234, 188, 114, 93, 168, 192, 42, 171, 234, 170, 19, 172, 54, 167, 92, 192, 186, 225, 53, 223, 49, 20, 182, 101, 137, 199, 237, 60, 182, 21, 89, 174, 90, 56, 79, 22, 43, 250, 128, 219, 228, 97, 127, 134, 195, 241, 208, 16, 201, 79, 226, 201, 191, 1, 154, 110, 99, 179, 239, 192, 40, 212, 60, 238, 97, 28, 133, 236, 38, 60, 144, 108, 70, 55, 114, 198, 145, 27, 25, 238, 192, 150, 202, 118, 236, 94, 49, 225, 227, 2, 3, 1, 0, 1, 2, 130, 1, 1, 0, 139, 55, 92, 203, 135, 200, 37, 197, 255, 61, 83, 208, 9, 145, 110, 150, 65, 5, 126, 24, 82, 114, 39, 160, 122, 178, 38, 190, 16, 136, 129, 58, 59, 56, 187, 123, 72, 243, 119, 5, 81, 101, 250, 42, 147, 57, 210, 77, 198, 103, 213, 197, 186, 52, 39, 230, 164, 129, 23, 110, 172, 21, 255, 212, 144, 104, 49, 30, 28, 40, 59, 159, 58, 142, 12, 184, 9, 180, 99, 12, 80, 170, 143, 62, 69, 166, 11, 53, 158, 25, 191, 140, 187, 94, 202, 214, 78, 118, 31, 16, 149, 116, 63, 243, 106, 175, 92, 240, 236, 185, 127, 237, 173, 221, 166, 11, 91, 243, 93, 129, 26, 117, 184, 34, 35, 12, 250, 160, 25, 47, 173, 64, 84, 126, 39, 84, 72, 170, 51, 22, 191, 142, 43, 76, 224, 133, 79, 199, 112, 139, 83, 123, 162, 45, 19, 33, 11, 9, 174, 195, 122, 39, 89, 239, 192, 130, 161, 83, 27, 35, 169, 23, 48, 3, 125, 222, 78, 242, 107, 95, 150, 239, 220, 195, 159, 211, 76, 52, 90, 213, 28, 187, 228, 79, 229, 139, 138, 59, 78, 201, 151, 134, 108, 8, 109, 255, 27, 136, 49, 239, 10, 31, 234, 38, 60, 247, 218, 205, 3, 192, 76, 188, 194, 178, 121, 229, 127, 165, 185, 83, 153, 107, 251, 29, 214, 136, 23, 175, 127, 180, 44, 222, 247, 165, 41, 74, 87, 250, 194, 184, 173, 115, 159, 27, 2, 153, 2, 129, 129, 0, 251, 248, 51, 194, 198, 49, 201, 112, 36, 12, 142, 116, 133, 240, 106, 62, 162, 168, 72, 34, 81, 26, 134, 39, 221, 70, 78, 248, 175, 175, 113, 72, 209, 164, 37, 182, 184, 101, 125, 221, 82, 70, 131, 43, 142, 83, 48, 32, 197, 187, 181, 104, 133, 90, 106, 236, 62, 66, 33, 215, 147, 241, 220, 91, 47, 37, 132, 226, 65, 94, 72, 233, 162, 189, 41, 43, 19, 64, 49, 249, 156, 142, 180, 47, 192, 188, 208, 68, 155, 242, 44, 230, 222, 201, 112, 20, 239, 229, 172, 147, 235, 232, 53, 135, 118, 86, 37, 44, 187, 177, 108, 65, 91, 103, 177, 132, 210, 40, 69, 104, 162, 119, 213, 147, 53, 88, 92, 253, 2, 129, 129, 0, 214, 184, 206, 39, 199, 41, 93, 93, 22, 252, 53, 112, 237, 100, 200, 218, 147, 3, 250, 210, 148, 136, 193, 166, 94, 154, 215, 17, 249, 3, 112, 24, 125, 187, 253, 129, 49, 109, 105, 100, 139, 200, 140, 197, 200, 53, 81, 175, 255, 69, 222, 186, 207, 182, 17, 5, 247, 9, 228, 195, 8, 9, 185, 0, 49, 235, 214, 134, 36, 68, 150, 198, 246, 158, 105, 46, 189, 200, 20, 246, 66, 57, 244, 173, 21, 117, 110, 203, 120, 197, 165, 176, 153, 49, 219, 24, 48, 119, 197, 70, 163, 140, 76, 116, 56, 137, 173, 61, 62, 208, 121, 181, 98, 46, 208, 18, 15, 160, 225, 249, 59, 89, 61, 183, 216, 82, 224, 95, 2, 129, 128, 56, 135, 75, 157, 131, 247, 129, 120, 206, 45, 158, 252, 23, 92, 131, 137, 127, 214, 127, 48, 107, 191, 166, 159, 100, 238, 52, 35, 104, 206, 212, 124, 128, 195, 241, 206, 23, 122, 117, 141, 100, 186, 251, 12, 151, 134, 164, 66, 133, 250, 1, 205, 236, 53, 7, 205, 238, 125, 201, 183, 226, 178, 29, 60, 187, 204, 16, 14, 238, 153, 103, 132, 59, 5, 115, 41, 253, 204, 166, 41, 152, 237, 15, 17, 179, 140, 232, 176, 171, 199, 222, 57, 1, 124, 113, 207, 208, 174, 87, 84, 108, 85, 145, 68, 205, 208, 175, 208, 100, 95, 126, 168, 255, 7, 185, 116, 209, 237, 68, 253, 31, 142, 0, 245, 96, 191, 109, 69, 2, 129, 129, 0, 133, 41, 239, 144, 115, 207, 143, 123, 95, 249, 226, 26, 186, 223, 58, 65, 115, 211, 144, 6, 112, 223, 175, 89, 66, 106, 188, 223, 4, 147, 193, 61, 47, 29, 27, 70, 184, 36, 166, 172, 24, 148, 179, 217, 37, 37, 12, 24, 30, 52, 114, 193, 96, 120, 5, 110, 177, 154, 141, 40, 247, 31, 48, 128, 146, 117, 52, 129, 212, 148, 68, 253, 247, 140, 158, 166, 194, 68, 7, 220, 1, 142, 119, 211, 175, 239, 56, 91, 47, 247, 67, 158, 150, 35, 121, 65, 51, 45, 212, 70, 206, 190, 255, 219, 68, 4, 254, 79, 113, 89, 81, 97, 208, 22, 64, 44, 51, 77, 15, 87, 198, 26, 190, 79, 249, 244, 203, 249, 2, 129, 129, 0, 135, 216, 119, 8, 212, 103, 99, 228, 204, 190, 178, 209, 233, 113, 46, 91, 240, 33, 109, 112, 222, 148, 32, 165, 178, 6, 155, 116, 89, 185, 159, 93, 159, 127, 47, 173, 124, 215, 154, 174, 230, 122, 127, 154, 52, 67, 126, 60, 121, 168, 74, 240, 205, 141, 233, 223, 242, 104, 235, 12, 71, 147, 245, 1, 249, 136, 213, 64, 246, 211, 71, 92, 32, 121, 184, 34, 122, 35, 217, 104, 222, 196, 227, 198, 101, 3, 24, 113, 147, 69, 150, 48, 71, 43, 253, 182, 186, 29, 231, 134, 199, 151, 250, 111, 78, 166, 90, 42, 132, 25, 38, 47, 41, 103, 136, 86, 203, 115, 201, 189, 75, 200, 155, 94, 4, 27, 34, 119]);
+    var spki = new Uint8Array([48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0, 48, 130, 1, 10, 2, 130, 1, 1, 0, 211, 87, 96, 146, 230, 41, 87, 54, 69, 68, 231, 228, 35, 59, 123, 219, 41, 61, 178, 8, 81, 34, 196, 121, 50, 133, 70, 249, 240, 247, 18, 246, 87, 196, 177, 120, 104, 201, 48, 144, 140, 197, 148, 247, 237, 0, 192, 20, 66, 193, 175, 4, 194, 246, 120, 164, 139, 162, 200, 15, 209, 113, 62, 48, 181, 172, 80, 120, 122, 195, 81, 101, 137, 241, 113, 150, 127, 99, 134, 173, 163, 73, 0, 166, 187, 4, 238, 206, 164, 43, 240, 67, 206, 217, 160, 249, 77, 12, 192, 158, 145, 155, 157, 113, 102, 192, 138, 182, 206, 32, 70, 64, 174, 164, 196, 146, 13, 182, 216, 110, 185, 22, 208, 220, 192, 244, 52, 26, 16, 56, 4, 41, 231, 225, 3, 33, 68, 234, 148, 157, 232, 246, 192, 204, 191, 149, 250, 142, 146, 141, 112, 216, 163, 140, 225, 104, 219, 69, 246, 241, 52, 102, 61, 111, 101, 111, 92, 234, 188, 114, 93, 168, 192, 42, 171, 234, 170, 19, 172, 54, 167, 92, 192, 186, 225, 53, 223, 49, 20, 182, 101, 137, 199, 237, 60, 182, 21, 89, 174, 90, 56, 79, 22, 43, 250, 128, 219, 228, 97, 127, 134, 195, 241, 208, 16, 201, 79, 226, 201, 191, 1, 154, 110, 99, 179, 239, 192, 40, 212, 60, 238, 97, 28, 133, 236, 38, 60, 144, 108, 70, 55, 114, 198, 145, 27, 25, 238, 192, 150, 202, 118, 236, 94, 49, 225, 227, 2, 3, 1, 0, 1]);
+
+    // plaintext
+    var plaintext = new Uint8Array([95, 77, 186, 79, 50, 12, 12, 232, 118, 114, 90, 252, 229, 251, 210, 91, 248, 62, 90, 113, 37, 160, 140, 175, 231, 60, 62, 186, 196, 33, 119, 157, 249, 213, 93, 24, 12, 58, 233, 148, 38, 69, 225, 216, 47, 238, 140, 157, 41, 75, 60, 177, 160, 138, 153, 49, 32, 27, 60, 14, 129, 252, 71, 202, 207, 131, 21, 162, 175, 102, 50, 65, 19, 195, 182, 98, 48, 195, 70, 8, 196, 244, 89, 54, 52, 206, 2, 178, 103, 54, 34, 119, 240, 168, 64, 202, 116, 188, 61, 26, 98, 54, 149, 44, 94, 215, 170, 248, 168, 254, 203, 221, 250, 117, 132, 230, 151, 140, 234, 93, 42, 91, 159, 183, 241, 180, 140, 139, 11, 229, 138, 48, 82, 2, 117, 77, 131, 118, 16, 115, 116, 121, 60, 240, 38, 170, 238, 83, 0, 114, 125, 131, 108, 215, 30, 113, 179, 69, 221, 178, 228, 68, 70, 255, 197, 185, 1, 99, 84, 19, 137, 13, 145, 14, 163, 128, 152, 74, 144, 25, 16, 49, 50, 63, 22, 219, 204, 157, 107, 225, 104, 184, 72, 133, 56, 76, 160, 62, 18, 96, 10, 193, 194, 72, 2, 138, 243, 114, 108, 201, 52, 99, 136, 46, 168, 192, 42, 171]);
+
+    // For verification tests.
+    var signatures = {
+        "sha-1": new Uint8Array([83, 46, 47, 27, 105, 204, 46, 232, 71, 46, 242, 143, 127, 54, 168, 26, 36, 205, 228, 238, 131, 133, 138, 125, 23, 5, 74, 195, 96, 44, 152, 221, 67, 46, 59, 54, 144, 68, 9, 53, 7, 43, 183, 192, 49, 230, 128, 112, 29, 25, 185, 124, 181, 81, 13, 134, 201, 190, 219, 231, 209, 192, 104, 57, 206, 237, 138, 59, 106, 201, 86, 65, 49, 196, 81, 43, 187, 171, 222, 35, 123, 77, 170, 41, 250, 13, 60, 151, 72, 123, 72, 168, 254, 233, 214, 59, 80, 86, 157, 198, 183, 209, 8, 80, 200, 50, 5, 89, 52, 59, 133, 55, 182, 18, 20, 167, 228, 84, 58, 113, 77, 101, 226, 28, 78, 71, 130, 148, 235, 66, 70, 206, 166, 104, 227, 81, 252, 224, 180, 225, 24, 199, 88, 190, 79, 220, 70, 199, 179, 34, 107, 191, 64, 181, 179, 149, 13, 98, 184, 189, 170, 79, 107, 183, 106, 48, 34, 43, 163, 39, 52, 237, 93, 244, 172, 141, 79, 255, 167, 85, 113, 5, 8, 122, 106, 207, 186, 91, 72, 81, 97, 99, 187, 145, 104, 100, 232, 44, 184, 97, 235, 145, 13, 207, 111, 26, 219, 173, 83, 153, 175, 212, 151, 251, 122, 251, 127, 117, 218, 131, 200, 5, 146, 234, 26, 222, 62, 56, 3, 180, 187, 104, 49, 185, 51, 41, 124, 15, 204, 195, 105, 55, 228, 96, 24, 121, 127, 202, 133, 148, 125, 41, 198, 162, 122, 129]),
+        "sha-256": new Uint8Array([19, 48, 106, 186, 37, 29, 238, 82, 100, 89, 194, 131, 82, 164, 41, 205, 216, 85, 84, 199, 228, 166, 121, 10, 44, 110, 68, 180, 94, 187, 196, 160, 10, 10, 85, 36, 77, 98, 166, 13, 223, 14, 215, 212, 45, 119, 20, 241, 47, 120, 58, 198, 112, 157, 14, 39, 2, 219, 38, 146, 174, 170, 204, 90, 92, 219, 190, 193, 115, 25, 140, 170, 176, 209, 79, 232, 133, 208, 168, 218, 31, 22, 106, 150, 92, 158, 37, 212, 132, 112, 180, 136, 77, 92, 146, 164, 216, 68, 23, 68, 5, 86, 143, 74, 192, 52, 13, 246, 196, 16, 252, 68, 207, 126, 230, 213, 155, 166, 52, 249, 198, 36, 12, 150, 181, 154, 6, 252, 238, 255, 77, 210, 150, 34, 231, 249, 131, 174, 191, 0, 236, 242, 65, 241, 201, 18, 207, 213, 220, 110, 238, 185, 79, 157, 145, 97, 19, 232, 67, 169, 133, 85, 194, 66, 87, 248, 195, 237, 171, 31, 39, 131, 159, 140, 201, 169, 99, 232, 184, 84, 101, 165, 110, 193, 216, 118, 34, 90, 224, 1, 251, 212, 36, 71, 1, 226, 228, 125, 129, 181, 87, 132, 126, 56, 39, 227, 59, 54, 243, 245, 232, 254, 223, 164, 154, 13, 52, 208, 29, 189, 175, 132, 250, 92, 117, 47, 2, 2, 168, 202, 178, 196, 204, 44, 181, 7, 111, 101, 55, 217, 194, 45, 53, 55, 56, 233, 179, 156, 151, 2, 107, 5, 156, 233, 93, 137]),
+        "sha-384": new Uint8Array([53, 79, 205, 28, 98, 226, 54, 45, 78, 139, 206, 223, 81, 80, 247, 178, 123, 236, 51, 171, 50, 162, 121, 117, 52, 90, 76, 140, 254, 178, 52, 102, 155, 196, 171, 175, 129, 231, 25, 221, 244, 193, 175, 174, 69, 67, 44, 183, 174, 185, 19, 60, 189, 135, 141, 231, 102, 232, 114, 98, 129, 120, 163, 58, 194, 10, 2, 138, 125, 140, 43, 100, 26, 92, 82, 59, 22, 187, 230, 94, 178, 11, 171, 51, 28, 152, 58, 150, 27, 174, 109, 230, 78, 107, 144, 119, 170, 137, 200, 70, 184, 214, 157, 207, 113, 1, 71, 141, 16, 117, 26, 59, 135, 178, 165, 222, 44, 166, 206, 113, 160, 191, 237, 127, 88, 122, 33, 110, 5, 58, 30, 83, 196, 162, 172, 233, 60, 38, 27, 68, 15, 236, 212, 51, 13, 215, 203, 215, 146, 184, 57, 133, 2, 178, 162, 8, 69, 164, 194, 145, 141, 135, 42, 172, 245, 11, 48, 39, 19, 87, 1, 154, 88, 174, 24, 129, 158, 117, 196, 142, 158, 248, 8, 16, 134, 15, 160, 73, 100, 119, 108, 160, 75, 32, 3, 41, 103, 77, 202, 83, 32, 180, 0, 245, 23, 134, 78, 113, 224, 135, 182, 139, 129, 223, 97, 62, 226, 75, 172, 52, 220, 242, 230, 69, 149, 225, 44, 121, 144, 112, 243, 247, 25, 217, 61, 120, 67, 182, 149, 146, 52, 108, 252, 32, 198, 187, 249, 49, 7, 242, 121, 214, 32, 126, 198, 87]),
+        "sha-512": new Uint8Array([98, 41, 183, 8, 151, 248, 98, 11, 99, 84, 135, 205, 74, 169, 150, 38, 152, 49, 255, 41, 49, 210, 135, 20, 240, 30, 88, 177, 101, 241, 8, 44, 49, 152, 184, 244, 81, 120, 140, 253, 62, 213, 154, 120, 248, 52, 209, 28, 226, 133, 209, 5, 28, 66, 165, 206, 160, 34, 127, 222, 254, 41, 52, 68, 194, 81, 142, 190, 92, 176, 5, 91, 234, 75, 88, 6, 240, 235, 161, 182, 101, 2, 42, 99, 190, 68, 192, 136, 254, 154, 210, 99, 37, 215, 159, 124, 65, 237, 151, 249, 9, 205, 76, 162, 131, 40, 228, 196, 169, 222, 141, 166, 124, 53, 220, 28, 133, 183, 30, 214, 255, 170, 249, 157, 116, 178, 184, 142, 159, 95, 5, 167, 50, 246, 104, 140, 153, 59, 88, 160, 237, 53, 232, 240, 161, 6, 212, 232, 177, 179, 96, 227, 52, 65, 92, 116, 46, 148, 103, 88, 35, 219, 15, 210, 94, 34, 207, 247, 166, 51, 92, 112, 225, 147, 35, 93, 205, 164, 138, 221, 104, 88, 98, 107, 217, 99, 17, 230, 15, 126, 94, 164, 73, 27, 108, 30, 98, 72, 175, 225, 43, 187, 213, 79, 136, 105, 176, 67, 165, 176, 68, 69, 98, 129, 63, 10, 152, 179, 0, 53, 111, 48, 110, 107, 120, 58, 41, 243, 190, 201, 124, 164, 14, 162, 0, 98, 202, 184, 146, 110, 197, 217, 106, 163, 135, 204, 132, 130, 26, 109, 114, 184, 234, 18, 110, 125])
+    };
+
+    var vectors = [
+        {
+            name: "RSASSA-PKCS1-v1_5 with SHA-1",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSASSA-PKCS1-v1_5"},
+            hash: "SHA-1",
+            plaintext: plaintext,
+            signature: signatures["sha-1"]
+        },
+        {
+            name: "RSASSA-PKCS1-v1_5 with SHA-256",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSASSA-PKCS1-v1_5"},
+            hash: "SHA-256",
+            plaintext: plaintext,
+            signature: signatures["sha-256"]
+        },
+        {
+            name: "RSASSA-PKCS1-v1_5 with SHA-384",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSASSA-PKCS1-v1_5"},
+            hash: "SHA-384",
+            plaintext: plaintext,
+            signature: signatures["sha-384"]
+        },
+        {
+            name: "RSASSA-PKCS1-v1_5 with SHA-512",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSASSA-PKCS1-v1_5"},
+            hash: "SHA-512",
+            plaintext: plaintext,
+            signature: signatures["sha-512"]
+        }
+    ];
+
+
+    return vectors;
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/rsa_pss.https.worker.js b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/rsa_pss.https.worker.js
new file mode 100644
index 0000000..fdf7b99
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/rsa_pss.https.worker.js
@@ -0,0 +1,5 @@
+importScripts("/resources/testharness.js");
+importScripts("rsa_pss_vectors.js");
+importScripts("rsa.js");
+
+run_test();
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/rsa_pss_vectors.js b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/rsa_pss_vectors.js
new file mode 100644
index 0000000..c3ce779
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/rsa_pss_vectors.js
@@ -0,0 +1,147 @@
+
+// rsa_pss_vectors.js
+
+// Data for testing RSA-PSS with a 2048-bit modulus and 65537 public exponent.
+
+// The following function returns an array of test vectors
+// for the subtleCrypto encrypt method.
+//
+// Each test vector has the following fields:
+//     name - a unique name for this vector
+//     publicKeyBuffer - an arrayBuffer with the key data
+//     publicKeyFormat - "spki" "jwk"
+//     publicKey - a CryptoKey object for the keyBuffer. INITIALLY null! You must fill this in first to use it!
+//     privateKeyBuffer - an arrayBuffer with the key data
+//     privateKeyFormat - "pkcs8" or "jwk"
+//     privateKey - a CryptoKey object for the keyBuffer. INITIALLY null! You must fill this in first to use it!
+//     algorithm - the value of the AlgorithmIdentifier parameter to provide to encrypt
+//     plaintext - the text to encrypt
+//     signature - the expected signature
+function getTestVectors() {
+    var pkcs8 = new Uint8Array([48, 130, 4, 191, 2, 1, 0, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 4, 130, 4, 169, 48, 130, 4, 165, 2, 1, 0, 2, 130, 1, 1, 0, 211, 87, 96, 146, 230, 41, 87, 54, 69, 68, 231, 228, 35, 59, 123, 219, 41, 61, 178, 8, 81, 34, 196, 121, 50, 133, 70, 249, 240, 247, 18, 246, 87, 196, 177, 120, 104, 201, 48, 144, 140, 197, 148, 247, 237, 0, 192, 20, 66, 193, 175, 4, 194, 246, 120, 164, 139, 162, 200, 15, 209, 113, 62, 48, 181, 172, 80, 120, 122, 195, 81, 101, 137, 241, 113, 150, 127, 99, 134, 173, 163, 73, 0, 166, 187, 4, 238, 206, 164, 43, 240, 67, 206, 217, 160, 249, 77, 12, 192, 158, 145, 155, 157, 113, 102, 192, 138, 182, 206, 32, 70, 64, 174, 164, 196, 146, 13, 182, 216, 110, 185, 22, 208, 220, 192, 244, 52, 26, 16, 56, 4, 41, 231, 225, 3, 33, 68, 234, 148, 157, 232, 246, 192, 204, 191, 149, 250, 142, 146, 141, 112, 216, 163, 140, 225, 104, 219, 69, 246, 241, 52, 102, 61, 111, 101, 111, 92, 234, 188, 114, 93, 168, 192, 42, 171, 234, 170, 19, 172, 54, 167, 92, 192, 186, 225, 53, 223, 49, 20, 182, 101, 137, 199, 237, 60, 182, 21, 89, 174, 90, 56, 79, 22, 43, 250, 128, 219, 228, 97, 127, 134, 195, 241, 208, 16, 201, 79, 226, 201, 191, 1, 154, 110, 99, 179, 239, 192, 40, 212, 60, 238, 97, 28, 133, 236, 38, 60, 144, 108, 70, 55, 114, 198, 145, 27, 25, 238, 192, 150, 202, 118, 236, 94, 49, 225, 227, 2, 3, 1, 0, 1, 2, 130, 1, 1, 0, 139, 55, 92, 203, 135, 200, 37, 197, 255, 61, 83, 208, 9, 145, 110, 150, 65, 5, 126, 24, 82, 114, 39, 160, 122, 178, 38, 190, 16, 136, 129, 58, 59, 56, 187, 123, 72, 243, 119, 5, 81, 101, 250, 42, 147, 57, 210, 77, 198, 103, 213, 197, 186, 52, 39, 230, 164, 129, 23, 110, 172, 21, 255, 212, 144, 104, 49, 30, 28, 40, 59, 159, 58, 142, 12, 184, 9, 180, 99, 12, 80, 170, 143, 62, 69, 166, 11, 53, 158, 25, 191, 140, 187, 94, 202, 214, 78, 118, 31, 16, 149, 116, 63, 243, 106, 175, 92, 240, 236, 185, 127, 237, 173, 221, 166, 11, 91, 243, 93, 129, 26, 117, 184, 34, 35, 12, 250, 160, 25, 47, 173, 64, 84, 126, 39, 84, 72, 170, 51, 22, 191, 142, 43, 76, 224, 133, 79, 199, 112, 139, 83, 123, 162, 45, 19, 33, 11, 9, 174, 195, 122, 39, 89, 239, 192, 130, 161, 83, 27, 35, 169, 23, 48, 3, 125, 222, 78, 242, 107, 95, 150, 239, 220, 195, 159, 211, 76, 52, 90, 213, 28, 187, 228, 79, 229, 139, 138, 59, 78, 201, 151, 134, 108, 8, 109, 255, 27, 136, 49, 239, 10, 31, 234, 38, 60, 247, 218, 205, 3, 192, 76, 188, 194, 178, 121, 229, 127, 165, 185, 83, 153, 107, 251, 29, 214, 136, 23, 175, 127, 180, 44, 222, 247, 165, 41, 74, 87, 250, 194, 184, 173, 115, 159, 27, 2, 153, 2, 129, 129, 0, 251, 248, 51, 194, 198, 49, 201, 112, 36, 12, 142, 116, 133, 240, 106, 62, 162, 168, 72, 34, 81, 26, 134, 39, 221, 70, 78, 248, 175, 175, 113, 72, 209, 164, 37, 182, 184, 101, 125, 221, 82, 70, 131, 43, 142, 83, 48, 32, 197, 187, 181, 104, 133, 90, 106, 236, 62, 66, 33, 215, 147, 241, 220, 91, 47, 37, 132, 226, 65, 94, 72, 233, 162, 189, 41, 43, 19, 64, 49, 249, 156, 142, 180, 47, 192, 188, 208, 68, 155, 242, 44, 230, 222, 201, 112, 20, 239, 229, 172, 147, 235, 232, 53, 135, 118, 86, 37, 44, 187, 177, 108, 65, 91, 103, 177, 132, 210, 40, 69, 104, 162, 119, 213, 147, 53, 88, 92, 253, 2, 129, 129, 0, 214, 184, 206, 39, 199, 41, 93, 93, 22, 252, 53, 112, 237, 100, 200, 218, 147, 3, 250, 210, 148, 136, 193, 166, 94, 154, 215, 17, 249, 3, 112, 24, 125, 187, 253, 129, 49, 109, 105, 100, 139, 200, 140, 197, 200, 53, 81, 175, 255, 69, 222, 186, 207, 182, 17, 5, 247, 9, 228, 195, 8, 9, 185, 0, 49, 235, 214, 134, 36, 68, 150, 198, 246, 158, 105, 46, 189, 200, 20, 246, 66, 57, 244, 173, 21, 117, 110, 203, 120, 197, 165, 176, 153, 49, 219, 24, 48, 119, 197, 70, 163, 140, 76, 116, 56, 137, 173, 61, 62, 208, 121, 181, 98, 46, 208, 18, 15, 160, 225, 249, 59, 89, 61, 183, 216, 82, 224, 95, 2, 129, 128, 56, 135, 75, 157, 131, 247, 129, 120, 206, 45, 158, 252, 23, 92, 131, 137, 127, 214, 127, 48, 107, 191, 166, 159, 100, 238, 52, 35, 104, 206, 212, 124, 128, 195, 241, 206, 23, 122, 117, 141, 100, 186, 251, 12, 151, 134, 164, 66, 133, 250, 1, 205, 236, 53, 7, 205, 238, 125, 201, 183, 226, 178, 29, 60, 187, 204, 16, 14, 238, 153, 103, 132, 59, 5, 115, 41, 253, 204, 166, 41, 152, 237, 15, 17, 179, 140, 232, 176, 171, 199, 222, 57, 1, 124, 113, 207, 208, 174, 87, 84, 108, 85, 145, 68, 205, 208, 175, 208, 100, 95, 126, 168, 255, 7, 185, 116, 209, 237, 68, 253, 31, 142, 0, 245, 96, 191, 109, 69, 2, 129, 129, 0, 133, 41, 239, 144, 115, 207, 143, 123, 95, 249, 226, 26, 186, 223, 58, 65, 115, 211, 144, 6, 112, 223, 175, 89, 66, 106, 188, 223, 4, 147, 193, 61, 47, 29, 27, 70, 184, 36, 166, 172, 24, 148, 179, 217, 37, 37, 12, 24, 30, 52, 114, 193, 96, 120, 5, 110, 177, 154, 141, 40, 247, 31, 48, 128, 146, 117, 52, 129, 212, 148, 68, 253, 247, 140, 158, 166, 194, 68, 7, 220, 1, 142, 119, 211, 175, 239, 56, 91, 47, 247, 67, 158, 150, 35, 121, 65, 51, 45, 212, 70, 206, 190, 255, 219, 68, 4, 254, 79, 113, 89, 81, 97, 208, 22, 64, 44, 51, 77, 15, 87, 198, 26, 190, 79, 249, 244, 203, 249, 2, 129, 129, 0, 135, 216, 119, 8, 212, 103, 99, 228, 204, 190, 178, 209, 233, 113, 46, 91, 240, 33, 109, 112, 222, 148, 32, 165, 178, 6, 155, 116, 89, 185, 159, 93, 159, 127, 47, 173, 124, 215, 154, 174, 230, 122, 127, 154, 52, 67, 126, 60, 121, 168, 74, 240, 205, 141, 233, 223, 242, 104, 235, 12, 71, 147, 245, 1, 249, 136, 213, 64, 246, 211, 71, 92, 32, 121, 184, 34, 122, 35, 217, 104, 222, 196, 227, 198, 101, 3, 24, 113, 147, 69, 150, 48, 71, 43, 253, 182, 186, 29, 231, 134, 199, 151, 250, 111, 78, 166, 90, 42, 132, 25, 38, 47, 41, 103, 136, 86, 203, 115, 201, 189, 75, 200, 155, 94, 4, 27, 34, 119]);
+    var spki = new Uint8Array([48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0, 48, 130, 1, 10, 2, 130, 1, 1, 0, 211, 87, 96, 146, 230, 41, 87, 54, 69, 68, 231, 228, 35, 59, 123, 219, 41, 61, 178, 8, 81, 34, 196, 121, 50, 133, 70, 249, 240, 247, 18, 246, 87, 196, 177, 120, 104, 201, 48, 144, 140, 197, 148, 247, 237, 0, 192, 20, 66, 193, 175, 4, 194, 246, 120, 164, 139, 162, 200, 15, 209, 113, 62, 48, 181, 172, 80, 120, 122, 195, 81, 101, 137, 241, 113, 150, 127, 99, 134, 173, 163, 73, 0, 166, 187, 4, 238, 206, 164, 43, 240, 67, 206, 217, 160, 249, 77, 12, 192, 158, 145, 155, 157, 113, 102, 192, 138, 182, 206, 32, 70, 64, 174, 164, 196, 146, 13, 182, 216, 110, 185, 22, 208, 220, 192, 244, 52, 26, 16, 56, 4, 41, 231, 225, 3, 33, 68, 234, 148, 157, 232, 246, 192, 204, 191, 149, 250, 142, 146, 141, 112, 216, 163, 140, 225, 104, 219, 69, 246, 241, 52, 102, 61, 111, 101, 111, 92, 234, 188, 114, 93, 168, 192, 42, 171, 234, 170, 19, 172, 54, 167, 92, 192, 186, 225, 53, 223, 49, 20, 182, 101, 137, 199, 237, 60, 182, 21, 89, 174, 90, 56, 79, 22, 43, 250, 128, 219, 228, 97, 127, 134, 195, 241, 208, 16, 201, 79, 226, 201, 191, 1, 154, 110, 99, 179, 239, 192, 40, 212, 60, 238, 97, 28, 133, 236, 38, 60, 144, 108, 70, 55, 114, 198, 145, 27, 25, 238, 192, 150, 202, 118, 236, 94, 49, 225, 227, 2, 3, 1, 0, 1]);
+
+    // plaintext for RSA-PSS
+    var plaintext = new Uint8Array([95, 77, 186, 79, 50, 12, 12, 232, 118, 114, 90, 252, 229, 251, 210, 91, 248, 62, 90, 113, 37, 160, 140, 175, 231, 60, 62, 186, 196, 33, 119, 157, 249, 213, 93, 24, 12, 58, 233, 148, 38, 69, 225, 216, 47, 238, 140, 157, 41, 75, 60, 177, 160, 138, 153, 49, 32, 27, 60, 14, 129, 252, 71, 202, 207, 131, 21, 162, 175, 102, 50, 65, 19, 195, 182, 98, 48, 195, 70, 8, 196, 244, 89, 54, 52, 206, 2, 178, 103, 54, 34, 119, 240, 168, 64, 202, 116, 188, 61, 26, 98, 54, 149, 44, 94, 215, 170, 248, 168, 254, 203, 221, 250, 117, 132, 230, 151, 140, 234, 93, 42, 91, 159, 183, 241, 180, 140, 139, 11, 229, 138, 48, 82, 2, 117, 77, 131, 118, 16, 115, 116, 121, 60, 240, 38, 170, 238, 83, 0, 114, 125, 131, 108, 215, 30, 113, 179, 69, 221, 178, 228, 68, 70, 255, 197, 185, 1, 99, 84, 19, 137, 13, 145, 14, 163, 128, 152, 74, 144, 25, 16, 49, 50, 63, 22, 219, 204, 157, 107, 225, 104, 184, 72, 133, 56, 76, 160, 62, 18, 96, 10, 193, 194, 72, 2, 138, 243, 114, 108, 201, 52, 99, 136, 46, 168, 192, 42, 171]);
+
+    // For verification tests. Note that "salted" signatures use a salt length equal to the hash size
+    var signatures = {
+        "sha-1, no salt": new Uint8Array([31, 28, 216, 30, 203, 59, 179, 29, 242, 229, 240, 246, 76, 92, 10, 49, 12, 124, 248, 141, 25, 235, 81, 42, 80, 120, 225, 86, 216, 35, 114, 122, 248, 134, 138, 18, 229, 223, 191, 169, 118, 56, 103, 132, 186, 152, 44, 57, 146, 135, 137, 177, 52, 149, 42, 74, 40, 198, 36, 17, 119, 188, 242, 36, 143, 42, 219, 96, 7, 127, 84, 93, 209, 126, 79, 128, 155, 59, 133, 159, 212, 48, 209, 104, 30, 128, 71, 18, 109, 119, 54, 149, 25, 238, 213, 182, 24, 243, 41, 122, 87, 80, 133, 240, 201, 49, 237, 36, 140, 246, 11, 189, 126, 255, 250, 10, 140, 43, 135, 75, 167, 248, 30, 205, 107, 243, 145, 208, 31, 30, 136, 29, 130, 122, 123, 149, 223, 135, 77, 154, 218, 187, 123, 7, 241, 49, 171, 51, 20, 42, 139, 11, 109, 92, 169, 104, 86, 113, 212, 155, 152, 43, 103, 101, 25, 9, 234, 161, 123, 150, 179, 147, 224, 79, 179, 109, 151, 47, 155, 37, 143, 27, 121, 18, 61, 242, 18, 211, 153, 36, 164, 222, 174, 197, 6, 207, 100, 15, 29, 237, 208, 45, 40, 132, 95, 53, 72, 216, 72, 134, 82, 120, 142, 46, 33, 70, 243, 206, 138, 134, 165, 86, 216, 75, 69, 120, 241, 13, 162, 154, 189, 177, 118, 166, 135, 24, 204, 27, 34, 112, 176, 115, 92, 46, 92, 166, 198, 187, 10, 250, 194, 58, 91, 250, 129, 122]),
+        "sha-256, no salt": new Uint8Array([97, 87, 214, 104, 237, 101, 93, 151, 139, 76, 21, 140, 132, 25, 235, 128, 113, 141, 253, 252, 125, 75, 52, 53, 127, 153, 23, 233, 225, 22, 182, 243, 182, 80, 64, 201, 209, 97, 85, 192, 129, 214, 136, 122, 188, 179, 186, 79, 250, 1, 145, 228, 128, 126, 226, 6, 104, 26, 161, 212, 128, 158, 162, 13, 229, 24, 107, 119, 227, 202, 206, 208, 127, 201, 179, 215, 27, 157, 240, 172, 129, 181, 195, 39, 63, 243, 247, 79, 50, 167, 173, 52, 198, 80, 98, 163, 21, 64, 206, 211, 5, 39, 239, 164, 183, 170, 45, 39, 255, 127, 128, 83, 95, 62, 101, 206, 53, 46, 185, 225, 139, 80, 84, 65, 109, 233, 89, 53, 74, 77, 204, 203, 37, 66, 227, 58, 131, 88, 237, 166, 32, 168, 101, 61, 214, 69, 143, 86, 171, 148, 254, 225, 220, 1, 239, 66, 251, 137, 88, 170, 19, 72, 16, 228, 216, 254, 29, 212, 254, 238, 106, 240, 71, 66, 248, 13, 165, 121, 56, 117, 167, 138, 42, 76, 192, 141, 78, 10, 104, 171, 3, 241, 192, 34, 160, 232, 167, 211, 9, 96, 137, 146, 210, 78, 205, 215, 232, 241, 137, 94, 62, 92, 211, 110, 73, 144, 107, 83, 25, 50, 217, 255, 149, 134, 24, 177, 165, 15, 152, 69, 95, 81, 94, 12, 99, 16, 61, 46, 78, 22, 81, 175, 197, 102, 235, 156, 173, 30, 126, 250, 225, 169, 117, 12, 56, 128]),
+        "sha-384, no salt": new Uint8Array([123, 149, 170, 182, 179, 76, 9, 98, 210, 40, 64, 158, 48, 223, 155, 4, 60, 27, 11, 170, 218, 8, 231, 61, 136, 116, 34, 85, 43, 143, 21, 34, 226, 228, 43, 242, 185, 255, 44, 108, 154, 163, 235, 12, 210, 55, 6, 24, 232, 241, 163, 104, 115, 89, 94, 0, 189, 231, 90, 156, 224, 98, 236, 50, 181, 246, 57, 79, 34, 103, 163, 245, 193, 24, 64, 255, 146, 230, 225, 91, 243, 28, 197, 62, 145, 124, 168, 239, 192, 137, 95, 177, 18, 194, 239, 143, 104, 28, 187, 106, 65, 1, 82, 246, 233, 48, 202, 255, 31, 38, 14, 49, 249, 131, 84, 46, 104, 205, 21, 222, 161, 126, 211, 19, 156, 172, 115, 81, 6, 251, 5, 252, 22, 59, 46, 208, 90, 13, 237, 147, 144, 89, 161, 12, 92, 215, 97, 158, 33, 178, 210, 6, 144, 121, 148, 39, 75, 52, 164, 218, 239, 161, 206, 89, 182, 179, 25, 247, 57, 85, 160, 145, 138, 94, 35, 126, 27, 191, 218, 219, 69, 201, 7, 165, 0, 131, 87, 126, 113, 146, 129, 136, 69, 153, 91, 74, 109, 63, 241, 151, 142, 15, 154, 66, 105, 88, 83, 40, 46, 53, 195, 183, 129, 51, 179, 224, 198, 36, 18, 90, 255, 20, 161, 135, 61, 25, 143, 99, 4, 255, 236, 127, 193, 207, 42, 222, 204, 108, 209, 75, 31, 137, 177, 166, 55, 247, 46, 209, 255, 93, 231, 198, 180, 217, 101, 153]),
+        "sha-512, no salt": new Uint8Array([175, 27, 192, 127, 167, 10, 221, 25, 243, 206, 31, 27, 239, 141, 252, 110, 36, 175, 67, 103, 28, 251, 151, 230, 184, 105, 232, 107, 126, 240, 53, 80, 166, 88, 19, 24, 255, 246, 68, 154, 250, 139, 103, 231, 62, 42, 106, 20, 226, 6, 119, 216, 176, 103, 20, 90, 132, 66, 37, 116, 174, 12, 253, 42, 93, 255, 112, 198, 215, 233, 127, 106, 14, 22, 101, 5, 7, 158, 180, 38, 74, 67, 196, 147, 242, 235, 63, 176, 111, 172, 192, 27, 230, 7, 116, 194, 119, 100, 106, 40, 8, 18, 71, 103, 150, 34, 178, 32, 34, 126, 146, 73, 117, 72, 103, 170, 143, 225, 128, 64, 21, 196, 249, 135, 0, 152, 46, 218, 64, 232, 77, 11, 160, 51, 108, 244, 79, 88, 47, 184, 120, 19, 116, 128, 78, 143, 180, 62, 185, 213, 119, 172, 244, 114, 53, 135, 163, 154, 43, 74, 158, 22, 139, 118, 118, 50, 183, 165, 84, 247, 123, 197, 39, 40, 33, 201, 56, 192, 153, 75, 22, 47, 116, 130, 99, 111, 127, 250, 197, 100, 161, 155, 215, 51, 244, 135, 120, 1, 220, 50, 77, 196, 113, 150, 239, 18, 202, 154, 143, 73, 33, 165, 73, 108, 214, 115, 121, 53, 202, 85, 91, 115, 70, 109, 221, 129, 126, 175, 240, 63, 237, 160, 235, 45, 97, 46, 60, 219, 89, 177, 152, 158, 239, 253, 193, 129, 1, 212, 110, 86, 185, 255, 92, 145, 249, 93]),
+        "sha-1, salted": new Uint8Array([31, 96, 138, 113, 209, 136, 76, 254, 33, 131, 180, 144, 55, 170, 133, 85, 176, 19, 154, 138, 18, 103, 165, 197, 185, 204, 226, 7, 1, 242, 173, 75, 189, 91, 50, 151, 64, 191, 243, 26, 204, 195, 75, 249, 175, 209, 67, 154, 5, 54, 187, 50, 182, 212, 39, 210, 105, 104, 219, 201, 233, 200, 13, 33, 17, 217, 72, 196, 129, 203, 23, 49, 119, 138, 205, 49, 16, 70, 50, 65, 196, 242, 59, 62, 19, 184, 85, 209, 98, 203, 21, 56, 81, 41, 15, 217, 95, 120, 21, 25, 226, 206, 249, 55, 69, 164, 19, 207, 238, 200, 233, 79, 186, 120, 34, 183, 37, 212, 116, 67, 24, 69, 140, 246, 180, 169, 23, 182, 91, 21, 238, 111, 84, 185, 195, 145, 246, 6, 74, 158, 3, 31, 112, 9, 245, 146, 68, 156, 11, 70, 213, 69, 122, 39, 153, 203, 14, 189, 120, 161, 2, 160, 85, 238, 4, 112, 178, 96, 194, 179, 216, 255, 189, 238, 15, 212, 118, 68, 130, 32, 144, 236, 85, 174, 98, 51, 190, 16, 98, 244, 65, 196, 50, 237, 60, 39, 94, 116, 214, 32, 19, 38, 129, 236, 46, 128, 30, 155, 91, 106, 204, 26, 215, 31, 137, 53, 56, 143, 126, 44, 3, 55, 13, 18, 233, 68, 227, 65, 140, 42, 182, 59, 180, 42, 190, 27, 185, 230, 149, 48, 240, 36, 88, 186, 40, 64, 11, 54, 128, 111, 247, 141, 165, 121, 26, 206]),
+        "sha-256, salted": new Uint8Array([140, 61, 3, 189, 232, 196, 45, 148, 83, 99, 27, 11, 170, 200, 158, 98, 150, 218, 32, 84, 55, 19, 192, 4, 223, 53, 188, 26, 111, 174, 32, 90, 178, 191, 88, 83, 105, 104, 144, 115, 205, 238, 52, 90, 214, 226, 120, 59, 45, 218, 24, 123, 73, 121, 234, 4, 87, 70, 55, 88, 21, 110, 16, 62, 237, 208, 239, 24, 52, 211, 91, 214, 173, 84, 13, 155, 139, 34, 95, 209, 119, 14, 81, 78, 160, 175, 53, 247, 7, 242, 231, 160, 56, 43, 230, 245, 237, 157, 107, 89, 29, 83, 108, 225, 33, 91, 23, 239, 62, 235, 69, 11, 180, 138, 0, 23, 73, 124, 103, 190, 2, 64, 71, 10, 221, 210, 137, 26, 129, 168, 241, 207, 110, 128, 227, 248, 55, 254, 66, 55, 98, 146, 223, 85, 91, 139, 5, 147, 27, 105, 83, 5, 151, 250, 227, 109, 205, 1, 177, 200, 23, 103, 212, 236, 212, 202, 240, 107, 239, 192, 53, 34, 75, 221, 42, 94, 107, 137, 213, 21, 57, 35, 90, 201, 85, 112, 231, 87, 219, 215, 15, 220, 21, 4, 0, 1, 176, 123, 147, 123, 240, 20, 140, 204, 0, 95, 76, 39, 42, 207, 95, 143, 192, 150, 163, 125, 38, 32, 142, 150, 172, 52, 28, 45, 29, 33, 44, 68, 214, 213, 21, 108, 147, 79, 102, 239, 66, 253, 186, 199, 122, 32, 134, 129, 85, 11, 4, 139, 70, 110, 50, 199, 108, 122, 123, 7]),
+        "sha-384, salted": new Uint8Array([121, 247, 40, 75, 180, 33, 109, 230, 132, 41, 133, 78, 219, 66, 24, 239, 120, 173, 23, 64, 132, 133, 103, 55, 115, 21, 219, 136, 103, 161, 87, 51, 199, 4, 46, 139, 249, 7, 98, 230, 115, 201, 12, 14, 44, 88, 198, 197, 206, 244, 151, 86, 139, 217, 42, 109, 33, 150, 18, 196, 117, 108, 85, 250, 196, 85, 7, 248, 22, 8, 188, 39, 32, 218, 78, 237, 213, 178, 62, 31, 60, 135, 64, 198, 180, 205, 126, 76, 240, 224, 67, 66, 177, 132, 193, 17, 1, 153, 230, 80, 128, 215, 59, 152, 94, 97, 29, 102, 248, 233, 121, 144, 129, 110, 73, 23, 186, 219, 176, 66, 93, 217, 67, 131, 137, 46, 42, 169, 109, 228, 219, 13, 224, 147, 106, 238, 132, 213, 72, 42, 61, 163, 27, 39, 49, 159, 67, 131, 15, 196, 135, 3, 204, 125, 78, 174, 219, 32, 253, 48, 50, 61, 191, 63, 34, 96, 141, 181, 22, 55, 211, 179, 5, 179, 25, 121, 98, 101, 141, 128, 147, 92, 38, 109, 51, 204, 251, 41, 117, 144, 98, 31, 74, 150, 124, 114, 69, 233, 43, 1, 88, 192, 220, 234, 148, 62, 42, 206, 113, 158, 189, 177, 150, 169, 186, 231, 223, 62, 217, 204, 98, 118, 94, 39, 182, 53, 113, 116, 62, 40, 160, 83, 141, 176, 130, 92, 173, 37, 57, 235, 93, 229, 230, 163, 32, 168, 139, 87, 62, 193, 151, 44, 38, 64, 21, 48]),
+        "sha-512, salted": new Uint8Array([183, 79, 48, 153, 216, 7, 135, 17, 139, 31, 157, 231, 159, 194, 7, 137, 62, 13, 45, 117, 196, 17, 15, 75, 21, 155, 133, 186, 7, 214, 58, 2, 86, 252, 60, 208, 246, 108, 232, 217, 162, 227, 207, 122, 61, 90, 123, 156, 11, 239, 172, 102, 56, 137, 74, 62, 54, 206, 117, 230, 73, 238, 6, 157, 216, 221, 152, 170, 139, 96, 36, 116, 201, 139, 20, 187, 3, 73, 45, 229, 81, 169, 232, 231, 121, 52, 239, 155, 104, 69, 131, 147, 79, 33, 141, 149, 118, 190, 36, 11, 92, 79, 54, 46, 175, 94, 1, 64, 200, 234, 146, 99, 144, 133, 166, 38, 150, 83, 80, 93, 207, 160, 4, 34, 109, 185, 246, 50, 119, 101, 58, 100, 161, 130, 110, 75, 171, 177, 122, 181, 77, 216, 84, 61, 207, 28, 232, 9, 112, 109, 104, 22, 230, 167, 95, 248, 70, 163, 212, 193, 141, 17, 189, 235, 31, 49, 177, 13, 85, 163, 121, 91, 100, 150, 49, 158, 110, 117, 21, 4, 216, 106, 78, 123, 182, 83, 91, 159, 4, 21, 232, 21, 216, 199, 137, 197, 177, 227, 135, 242, 168, 192, 15, 239, 110, 50, 116, 98, 203, 126, 82, 91, 143, 148, 91, 229, 177, 114, 72, 224, 224, 164, 216, 85, 211, 151, 226, 45, 6, 124, 228, 83, 147, 115, 223, 186, 70, 209, 121, 146, 80, 175, 199, 15, 83, 80, 6, 202, 205, 39, 102, 245, 221, 207, 143, 145])
+    };
+
+    var vectors = [
+        {
+            name: "RSA-PSS with SHA-1 and no salt",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-PSS", saltLength: 0},
+            hash: "SHA-1",
+            plaintext: plaintext,
+            signature: signatures["sha-1, no salt"]
+        },
+        {
+            name: "RSA-PSS with SHA-256 and no salt",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-PSS", saltLength: 0},
+            hash: "SHA-256",
+            plaintext: plaintext,
+            signature: signatures["sha-256, no salt"]
+        },
+        {
+            name: "RSA-PSS with SHA-384 and no salt",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-PSS", saltLength: 0},
+            hash: "SHA-384",
+            plaintext: plaintext,
+            signature: signatures["sha-384, no salt"]
+        },
+        {
+            name: "RSA-PSS with SHA-512 and no salt",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-PSS", saltLength: 0},
+            hash: "SHA-512",
+            plaintext: plaintext,
+            signature: signatures["sha-512, no salt"]
+        },
+        {
+            name: "RSA-PSS with SHA-1, salted",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-PSS", saltLength: 20},
+            hash: "SHA-1",
+            plaintext: plaintext,
+            signature: signatures["sha-1, salted"]
+        },
+        {
+            name: "RSA-PSS with SHA-256, salted",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-PSS", saltLength: 32},
+            hash: "SHA-256",
+            plaintext: plaintext,
+            signature: signatures["sha-256, salted"]
+        },
+        {
+            name: "RSA-PSS with SHA-384, salted",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-PSS", saltLength: 48},
+            hash: "SHA-384",
+            plaintext: plaintext,
+            signature: signatures["sha-384, salted"]
+        },
+        {
+            name: "RSA-PSS with SHA-512, salted",
+            publicKeyBuffer: spki,
+            publicKeyFormat: "spki",
+            privateKey: null,
+            privateKeyBuffer: pkcs8,
+            privateKeyFormat: "pkcs8",
+            publicKey: null,
+            algorithm: {name: "RSA-PSS", saltLength: 64},
+            hash: "SHA-512",
+            plaintext: plaintext,
+            signature: signatures["sha-512, salted"]
+        }
+    ];
+
+    return vectors;
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/test_ecdsa.https.html b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/test_ecdsa.https.html
new file mode 100644
index 0000000..5f121f1
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/test_ecdsa.https.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>WebCryptoAPI: sign() and verify() Using ECDSA</title>
+<link rel="author" title="Charles Engelke" href="mailto:w3c@engelke.com">
+<link rel="help" href="https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#SubtleCrypto-method-sign">
+<link rel="help" href="https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#SubtleCrypto-method-verify">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script src="ecdsa_vectors.js"></script>
+<script src="ecdsa.js"></script>
+
+<h1>sign and verify Tests for ECDSA</h1>
+
+<div id="log"></div>
+<script>
+run_test();
+</script>
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/test_hmac.https.html b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/test_hmac.https.html
new file mode 100644
index 0000000..6d5ff92
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/test_hmac.https.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>WebCryptoAPI: sign() and verify() Using HMAC</title>
+<link rel="author" title="Charles Engelke" href="mailto:w3c@engelke.com">
+<link rel="help" href="https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#SubtleCrypto-method-sign">
+<link rel="help" href="https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#SubtleCrypto-method-verify">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script src="hmac_vectors.js"></script>
+<script src="hmac.js"></script>
+
+<h1>sign and verify Tests for HMAC</h1>
+
+<div id="log"></div>
+<script>
+run_test();
+</script>
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/test_rsa_pkcs.https.html b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/test_rsa_pkcs.https.html
new file mode 100644
index 0000000..de428c0
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/test_rsa_pkcs.https.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>WebCryptoAPI: sign() and verify() Using RSASSA-PKCS1-v1_5</title>
+<link rel="author" title="Charles Engelke" href="mailto:w3c@engelke.com">
+<link rel="help" href="https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#SubtleCrypto-method-sign">
+<link rel="help" href="https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#SubtleCrypto-method-verify">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script src="rsa_pkcs_vectors.js"></script>
+<script src="rsa.js"></script>
+
+<h1>sign and verify Tests for RSASSA-PKCS1-v1_5</h1>
+
+<div id="log"></div>
+<script>
+run_test();
+</script>
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/test_rsa_pss.https.html b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/test_rsa_pss.https.html
new file mode 100644
index 0000000..3a2034f
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/sign_verify/test_rsa_pss.https.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>WebCryptoAPI: sign() and verify() Using RSA-PSS</title>
+<link rel="author" title="Charles Engelke" href="mailto:w3c@engelke.com">
+<link rel="help" href="https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#SubtleCrypto-method-sign">
+<link rel="help" href="https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#SubtleCrypto-method-verify">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script src="rsa_pss_vectors.js"></script>
+<script src="rsa.js"></script>
+
+<h1>sign and verify Tests for RSA-PSS</h1>
+
+<div id="log"></div>
+<script>
+run_test();
+</script>
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/test_getRandomValues.html b/src/third_party/web_platform_tests/WebCryptoAPI/test_getRandomValues.html
deleted file mode 100644
index a5cd537..0000000
--- a/src/third_party/web_platform_tests/WebCryptoAPI/test_getRandomValues.html
+++ /dev/null
@@ -1,12 +0,0 @@
-<!DOCTYPE HTML>
-<meta charset=utf-8>
-<title>WebCryptoAPI: getRandomValues()</title>
-<link rel="author" title="Sunil Yoo" href="mailto:usuanday83@gmail.com">
-<link rel="help" href="https://dvcs.w3.org/hg/webcrypto-api/raw-file/tip/spec/Overview.html#dfn-Crypto-method-getRandomValues">
-<script src="/resources/testharness.js"></script>
-<script src="/resources/testharnessreport.js"></script>
-<script src="getRandomValues.js"></script>
-<div id="log"></div>
-<script>
-run_test();
-</script>
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/tools/generate.py b/src/third_party/web_platform_tests/WebCryptoAPI/tools/generate.py
new file mode 100644
index 0000000..728f914
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/tools/generate.py
@@ -0,0 +1,76 @@
+# script to generate the generateKey tests
+
+import os
+
+here = os.path.dirname(__file__)
+
+successes_html = """<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>WebCryptoAPI: generateKey() Successful Calls</title>
+<link rel="author" title="Charles Engelke" href="mailto:w3c@engelke.com">
+<link rel="help" href="https://www.w3.org/TR/WebCryptoAPI/#dfn-SubtleCrypto-method-generateKey">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script src="/WebCryptoAPI/util/helpers.js"></script>
+<script src="successes.js"></script>
+
+<h1>generateKey Tests for Good Parameters</h1>
+<p>
+    <strong>Warning!</strong> RSA key generation is intrinsically
+    very slow, so the related tests can take up to
+    several minutes to complete, depending on browser!
+</p>
+
+<div id="log"></div>
+<script>
+run_test([%s]);
+</script>"""
+
+failures_html = """<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>WebCryptoAPI: generateKey() for Failures</title>
+<link rel="author" title="Charles Engelke" href="mailto:w3c@engelke.com">
+<link rel="help" href="https://www.w3.org/TR/WebCryptoAPI/#dfn-SubtleCrypto-method-generateKey">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script src="/WebCryptoAPI/util/helpers.js"></script>
+<script src="failures.js"></script>
+
+<h1>generateKey Tests for Bad Parameters</h1>
+
+<div id="log"></div>
+<script>
+run_test([%s]);
+</script>
+"""
+
+successes_worker = """// META: timeout=long
+importScripts("/resources/testharness.js");
+importScripts("../util/helpers.js");
+importScripts("successes.js");
+
+run_test([%s]);
+done();"""
+
+failures_worker = """// META: timeout=long
+importScripts("/resources/testharness.js");
+importScripts("../util/helpers.js");
+importScripts("failures.js");
+run_test([%s]);
+done();"""
+
+names = ["AES-CTR", "AES-CBC", "AES-GCM", "AES-KW", "HMAC", "RSASSA-PKCS1-v1_5",
+         "RSA-PSS", "RSA-OAEP", "ECDSA", "ECDH"]
+
+for filename_pattern, template in [("test_successes_%s.https.html", successes_html),
+                                   ("test_failures_%s.https.html", failures_html),
+                                   ("successes_%s.https.worker.js", successes_worker),
+                                   ("failures_%s.https.worker.js", failures_worker)]:
+    for name in names:
+        path = os.path.join(here, os.pardir, "generateKey", filename_pattern % name)
+        with open(path, "w") as f:
+            f.write(template % '"%s"' % name)
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/util/helpers.js b/src/third_party/web_platform_tests/WebCryptoAPI/util/helpers.js
new file mode 100644
index 0000000..c13ce91
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/util/helpers.js
@@ -0,0 +1,231 @@
+//
+// helpers.js
+//
+// Helper functions used by several WebCryptoAPI tests
+//
+
+var registeredAlgorithmNames = [
+    "RSASSA-PKCS1-v1_5",
+    "RSA-PSS",
+    "RSA-OAEP",
+    "ECDSA",
+    "ECDH",
+    "AES-CTR",
+    "AES-CBC",
+    "AES-GCM",
+    "AES-KW",
+    "HMAC",
+    "SHA-1",
+    "SHA-256",
+    "SHA-384",
+    "SHA-512",
+    "HKDF-CTR",
+    "PBKDF2"
+];
+
+
+// Treats an array as a set, and generates an array of all non-empty
+// subsets (which are themselves arrays).
+//
+// The order of members of the "subsets" is not guaranteed.
+function allNonemptySubsetsOf(arr) {
+    var results = [];
+    var firstElement;
+    var remainingElements;
+
+    for(var i=0; i<arr.length; i++) {
+        firstElement = arr[i];
+        remainingElements = arr.slice(i+1);
+        results.push([firstElement]);
+
+        if (remainingElements.length > 0) {
+            allNonemptySubsetsOf(remainingElements).forEach(function(combination) {
+                combination.push(firstElement);
+                results.push(combination);
+            });
+        }
+    }
+
+    return results;
+}
+
+
+// Create a string representation of keyGeneration parameters for
+// test names and labels.
+function objectToString(obj) {
+    var keyValuePairs = [];
+
+    if (Array.isArray(obj)) {
+        return "[" + obj.map(function(elem){return objectToString(elem);}).join(", ") + "]";
+    } else if (typeof obj === "object") {
+        Object.keys(obj).sort().forEach(function(keyName) {
+            keyValuePairs.push(keyName + ": " + objectToString(obj[keyName]));
+        });
+        return "{" + keyValuePairs.join(", ") + "}";
+    } else if (typeof obj === "undefined") {
+        return "undefined";
+    } else {
+        return obj.toString();
+    }
+
+    var keyValuePairs = [];
+
+    Object.keys(obj).sort().forEach(function(keyName) {
+        var value = obj[keyName];
+        if (typeof value === "object") {
+            value = objectToString(value);
+        } else if (typeof value === "array") {
+            value = "[" + value.map(function(elem){return objectToString(elem);}).join(", ") + "]";
+        } else {
+            value = value.toString();
+        }
+
+        keyValuePairs.push(keyName + ": " + value);
+    });
+
+    return "{" + keyValuePairs.join(", ") + "}";
+}
+
+// Is key a CryptoKey object with correct algorithm, extractable, and usages?
+// Is it a secret, private, or public kind of key?
+function assert_goodCryptoKey(key, algorithm, extractable, usages, kind) {
+    var correctUsages = [];
+
+    var registeredAlgorithmName;
+    registeredAlgorithmNames.forEach(function(name) {
+        if (name.toUpperCase() === algorithm.name.toUpperCase()) {
+            registeredAlgorithmName = name;
+        }
+    });
+
+    assert_equals(key.constructor, CryptoKey, "Is a CryptoKey");
+    assert_equals(key.type, kind, "Is a " + kind + " key");
+    if (key.type === "public") {
+        extractable = true; // public keys are always extractable
+    }
+    assert_equals(key.extractable, extractable, "Extractability is correct");
+
+    assert_equals(key.algorithm.name, registeredAlgorithmName, "Correct algorithm name");
+    assert_equals(key.algorithm.length, algorithm.length, "Correct length");
+    if (["HMAC", "RSASSA-PKCS1-v1_5", "RSA-PSS"].includes(registeredAlgorithmName)) {
+        assert_equals(key.algorithm.hash.name.toUpperCase(), algorithm.hash.toUpperCase(), "Correct hash function");
+    }
+
+    // usages is expected to be provided for a key pair, but we are checking
+    // only a single key. The publicKey and privateKey portions of a key pair
+    // recognize only some of the usages appropriate for a key pair.
+    if (key.type === "public") {
+        ["encrypt", "verify", "wrapKey"].forEach(function(usage) {
+            if (usages.includes(usage)) {
+                correctUsages.push(usage);
+            }
+        });
+    } else if (key.type === "private") {
+        ["decrypt", "sign", "unwrapKey", "deriveKey", "deriveBits"].forEach(function(usage) {
+            if (usages.includes(usage)) {
+                correctUsages.push(usage);
+            }
+        });
+    } else {
+        correctUsages = usages;
+    }
+
+    assert_equals((typeof key.usages), "object", key.type + " key.usages is an object");
+    assert_not_equals(key.usages, null, key.type + " key.usages isn't null");
+
+    // The usages parameter could have repeats, but the usages
+    // property of the result should not.
+    var usageCount = 0;
+    key.usages.forEach(function(usage) {
+        usageCount += 1;
+        assert_in_array(usage, correctUsages, "Has " + usage + " usage");
+    });
+    assert_equals(key.usages.length, usageCount, "usages property is correct");
+}
+
+
+// The algorithm parameter is an object with a name and other
+// properties. Given the name, generate all valid parameters.
+function allAlgorithmSpecifiersFor(algorithmName) {
+    var results = [];
+
+    // RSA key generation is slow. Test a minimal set of parameters
+    var hashes = ["SHA-1", "SHA-256"];
+
+    // EC key generation is a lot faster. Check all curves in the spec
+    var curves = ["P-256", "P-384", "P-521"];
+
+    if (algorithmName.toUpperCase().substring(0, 3) === "AES") {
+        // Specifier properties are name and length
+        [128, 192, 256].forEach(function(length) {
+            results.push({name: algorithmName, length: length});
+        });
+    } else if (algorithmName.toUpperCase() === "HMAC") {
+        [
+            {name: "SHA-1", length: 160},
+            {name: "SHA-256", length: 256},
+            {name: "SHA-384", length: 384},
+            {name: "SHA-512", length: 512}
+        ].forEach(function(hashAlgorithm) {
+            results.push({name: algorithmName, hash: hashAlgorithm.name, length: hashAlgorithm.length});
+        });
+    } else if (algorithmName.toUpperCase().substring(0, 3) === "RSA") {
+        hashes.forEach(function(hashName) {
+            results.push({name: algorithmName, hash: hashName, modulusLength: 2048, publicExponent: new Uint8Array([1,0,1])});
+        });
+    } else if (algorithmName.toUpperCase().substring(0, 2) === "EC") {
+        curves.forEach(function(curveName) {
+            results.push({name: algorithmName, namedCurve: curveName});
+        });
+    }
+
+    return results;
+}
+
+
+// Create every possible valid usages parameter, given legal
+// usages. Note that an empty usages parameter is not always valid.
+//
+// There is an optional parameter - mandatoryUsages. If provided,
+// it should be an array containing those usages of which one must be
+// included.
+function allValidUsages(validUsages, emptyIsValid, mandatoryUsages) {
+    if (typeof mandatoryUsages === "undefined") {
+        mandatoryUsages = [];
+    }
+
+    okaySubsets = [];
+    allNonemptySubsetsOf(validUsages).forEach(function(subset) {
+        if (mandatoryUsages.length === 0) {
+            okaySubsets.push(subset);
+        } else {
+            for (var i=0; i<mandatoryUsages.length; i++) {
+                if (subset.includes(mandatoryUsages[i])) {
+                    okaySubsets.push(subset);
+                    return;
+                }
+            }
+        }
+    });
+
+    if (emptyIsValid) {
+        okaySubsets.push([]);
+    }
+
+    okaySubsets.push(validUsages.concat(mandatoryUsages).concat(validUsages)); // Repeated values are allowed
+    return okaySubsets;
+}
+
+
+// Algorithm name specifiers are case-insensitive. Generate several
+// case variations of a given name.
+function allNameVariants(name, slowTest) {
+    var upCaseName = name.toUpperCase();
+    var lowCaseName = name.toLowerCase();
+    var mixedCaseName = upCaseName.substring(0, 1) + lowCaseName.substring(1);
+
+    // for slow tests effectively cut the amount of work in third by only
+    // returning one variation
+    if (slowTest) return [mixedCaseName];
+    return [upCaseName, lowCaseName, mixedCaseName];
+}
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/util/worker-report-crypto-subtle-presence.js b/src/third_party/web_platform_tests/WebCryptoAPI/util/worker-report-crypto-subtle-presence.js
new file mode 100644
index 0000000..22cca7b
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/util/worker-report-crypto-subtle-presence.js
@@ -0,0 +1,3 @@
+subtle_crypto_found = true;
+if (typeof crypto.subtle === 'undefined') subtle_crypto_found = false;
+postMessage({ msg_type: 'subtle_crypto_found', msg_value: subtle_crypto_found });
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/wrapKey_unwrapKey/test_wrapKey_unwrapKey.https.html b/src/third_party/web_platform_tests/WebCryptoAPI/wrapKey_unwrapKey/test_wrapKey_unwrapKey.https.html
new file mode 100644
index 0000000..337e078
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/wrapKey_unwrapKey/test_wrapKey_unwrapKey.https.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="timeout" content="long">
+<title>WebCryptoAPI: wrapKey() and unwrapKey()</title>
+<link rel="author" title="Charles Engelke" href="mailto:w3c@engelke.com">
+<link rel="help" href="https://w3c.github.io/webcrypto/Overview.html#SubtleCrypto-method-wrapKey">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script src="wrapKey_unwrapKey.js"></script>
+
+<h1>wrapKey and unwrapKey Tests</h1>
+
+<div id="log"></div>
+<script>
+run_test();
+</script>
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/wrapKey_unwrapKey/wrapKey_unwrapKey.https.worker.js b/src/third_party/web_platform_tests/WebCryptoAPI/wrapKey_unwrapKey/wrapKey_unwrapKey.https.worker.js
new file mode 100644
index 0000000..4cf813b
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/wrapKey_unwrapKey/wrapKey_unwrapKey.https.worker.js
@@ -0,0 +1,5 @@
+// META: timeout=long
+importScripts("/resources/testharness.js");
+importScripts("wrapKey_unwrapKey.js");
+
+run_test();
diff --git a/src/third_party/web_platform_tests/WebCryptoAPI/wrapKey_unwrapKey/wrapKey_unwrapKey.js b/src/third_party/web_platform_tests/WebCryptoAPI/wrapKey_unwrapKey/wrapKey_unwrapKey.js
new file mode 100644
index 0000000..8f1a602
--- /dev/null
+++ b/src/third_party/web_platform_tests/WebCryptoAPI/wrapKey_unwrapKey/wrapKey_unwrapKey.js
@@ -0,0 +1,517 @@
+// Tests for wrapKey and unwrapKey round tripping
+
+function run_test() {
+    var subtle = self.crypto.subtle;
+
+    var wrappers = [];  // Things we wrap (and upwrap) keys with
+    var keys = [];      // Things to wrap and unwrap
+    var ecdhPeerKey;    // ECDH peer public key needed for non-extractable ECDH key comparison
+
+    // Generate all the keys needed, then iterate over all combinations
+    // to test wrapping and unwrapping.
+    Promise.all([generateWrappingKeys(), generateKeysToWrap(), generateEcdhPeerKey()])
+    .then(function(results) {
+        var promises = [];
+        wrappers.forEach(function(wrapper) {
+            keys.forEach(function(key) {
+                promises.push(testWrapping(wrapper, key));
+            })
+        });
+        return Promise.all(promises);
+    }, function(err) {
+        promise_test(function(test) {
+            assert_unreached("A key failed to generate: " + err.name + ": " + err.message)
+        }, "Could not run all tests")
+    })
+    .then(function() {
+        done();
+    }, function(err) {
+        promise_test(function(test) {
+            assert_unreached("A test failed to run: " + err.name + ": " + err.message)
+        }, "Could not run all tests")
+    });
+
+
+    function generateWrappingKeys() {
+        // There are five algorithms that can be used for wrapKey/unwrapKey.
+        // Generate one key with typical parameters for each kind.
+        //
+        // Note: we don't need cryptographically strong parameters for things
+        // like IV - just any legal value will do.
+        var parameters = [
+            {
+                name: "RSA-OAEP",
+                generateParameters: {name: "RSA-OAEP", modulusLength: 4096, publicExponent: new Uint8Array([1,0,1]), hash: "SHA-256"},
+                wrapParameters: {name: "RSA-OAEP", label: new Uint8Array(8)}
+            },
+            {
+                name: "AES-CTR",
+                generateParameters: {name: "AES-CTR", length: 128},
+                wrapParameters: {name: "AES-CTR", counter: new Uint8Array(16), length: 64}
+            },
+            {
+                name: "AES-CBC",
+                generateParameters: {name: "AES-CBC", length: 128},
+                wrapParameters: {name: "AES-CBC", iv: new Uint8Array(16)}
+            },
+            {
+                name: "AES-GCM",
+                generateParameters: {name: "AES-GCM", length: 128},
+                wrapParameters: {name: "AES-GCM", iv: new Uint8Array(16), additionalData: new Uint8Array(16), tagLength: 64}
+            },
+            {
+                name: "AES-KW",
+                generateParameters: {name: "AES-KW", length: 128},
+                wrapParameters: {name: "AES-KW"}
+            }
+        ];
+
+        return Promise.all(parameters.map(function(params) {
+            return subtle.generateKey(params.generateParameters, true, ["wrapKey", "unwrapKey"])
+            .then(function(key) {
+                var wrapper;
+                if (params.name === "RSA-OAEP") { // we have a key pair, not just a key
+                    wrapper = {wrappingKey: key.publicKey, unwrappingKey: key.privateKey, parameters: params};
+                } else {
+                    wrapper = {wrappingKey: key, unwrappingKey: key, parameters: params};
+                }
+                wrappers.push(wrapper);
+                return true;
+            })
+        }));
+    }
+
+
+    function generateKeysToWrap() {
+        var parameters = [
+            {algorithm: {name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, publicExponent: new Uint8Array([1,0,1]), hash: "SHA-256"}, privateUsages: ["sign"], publicUsages: ["verify"]},
+            {algorithm: {name: "RSA-PSS", modulusLength: 1024, publicExponent: new Uint8Array([1,0,1]), hash: "SHA-256"}, privateUsages: ["sign"], publicUsages: ["verify"]},
+            {algorithm: {name: "RSA-OAEP", modulusLength: 1024, publicExponent: new Uint8Array([1,0,1]), hash: "SHA-256"}, privateUsages: ["decrypt"], publicUsages: ["encrypt"]},
+            {algorithm: {name: "ECDSA", namedCurve: "P-256"}, privateUsages: ["sign"], publicUsages: ["verify"]},
+            {algorithm: {name: "ECDH", namedCurve: "P-256"}, privateUsages: ["deriveBits"], publicUsages: []},
+            {algorithm: {name: "AES-CTR", length: 128}, usages: ["encrypt", "decrypt"]},
+            {algorithm: {name: "AES-CBC", length: 128}, usages: ["encrypt", "decrypt"]},
+            {algorithm: {name: "AES-GCM", length: 128}, usages: ["encrypt", "decrypt"]},
+            {algorithm: {name: "AES-KW", length: 128}, usages: ["wrapKey", "unwrapKey"]},
+            {algorithm: {name: "HMAC", length: 128, hash: "SHA-256"}, usages: ["sign", "verify"]}
+        ];
+
+        return Promise.all(parameters.map(function(params) {
+            var usages;
+            if ("usages" in params) {
+                usages = params.usages;
+            } else {
+                usages = params.publicUsages.concat(params.privateUsages);
+            }
+
+            return subtle.generateKey(params.algorithm, true, usages)
+            .then(function(result) {
+                if (result.constructor === CryptoKey) {
+                    keys.push({name: params.algorithm.name, algorithm: params.algorithm, usages: params.usages, key: result});
+                } else {
+                    keys.push({name: params.algorithm.name + " public key", algorithm: params.algorithm, usages: params.publicUsages, key: result.publicKey});
+                    keys.push({name: params.algorithm.name + " private key", algorithm: params.algorithm, usages: params.privateUsages, key: result.privateKey});
+                }
+                return true;
+            });
+        }));
+    }
+
+    function generateEcdhPeerKey() {
+        return subtle.generateKey({name: "ECDH", namedCurve: "P-256"},true,["deriveBits"])
+        .then(function(result){
+            ecdhPeerKey = result.publicKey;
+        });
+    }
+
+    // Can we successfully "round-trip" (wrap, then unwrap, a key)?
+    function testWrapping(wrapper, toWrap) {
+        var formats;
+
+        if (toWrap.name.includes("private")) {
+            formats = ["pkcs8", "jwk"];
+        } else if (toWrap.name.includes("public")) {
+            formats = ["spki", "jwk"]
+        } else {
+            formats = ["raw", "jwk"]
+        }
+
+        return Promise.all(formats.map(function(fmt) {
+            var originalExport;
+            return subtle.exportKey(fmt, toWrap.key).then(function(exportedKey) {
+                originalExport = exportedKey;
+                if (wrappingIsPossible(originalExport, wrapper.parameters.name)) {
+                    promise_test(function(test) {
+                        return subtle.wrapKey(fmt, toWrap.key, wrapper.wrappingKey, wrapper.parameters.wrapParameters)
+                        .then(function(wrappedResult) {
+                            return subtle.unwrapKey(fmt, wrappedResult, wrapper.unwrappingKey, wrapper.parameters.wrapParameters, toWrap.algorithm, true, toWrap.usages);
+                        }).then(function(unwrappedResult) {
+                            assert_true(unwrappedResult.extractable, "Unwrapped result is extractable");
+                            return subtle.exportKey(fmt, unwrappedResult)
+                        }).then(function(roundTripExport) {
+                            assert_true(equalExport(originalExport, roundTripExport), "Post-wrap export matches original export");
+                        }, function(err) {
+                            assert_unreached("Round trip for extractable key threw an error - " + err.name + ': "' + err.message + '"');
+                        });
+                    }, "Can wrap and unwrap " + toWrap.name + " keys using " + fmt + " and " + wrapper.parameters.name);
+
+                    if (canCompareNonExtractableKeys(toWrap.key)) {
+                        promise_test(function(test){
+                            return subtle.wrapKey(fmt, toWrap.key, wrapper.wrappingKey, wrapper.parameters.wrapParameters)
+                            .then(function(wrappedResult) {
+                                return subtle.unwrapKey(fmt, wrappedResult, wrapper.unwrappingKey, wrapper.parameters.wrapParameters, toWrap.algorithm, false, toWrap.usages);
+                            }).then(function(unwrappedResult){
+                                assert_false(unwrappedResult.extractable, "Unwrapped result is non-extractable");
+                                return equalKeys(toWrap.key, unwrappedResult);
+                            }).then(function(result){
+                                assert_true(result, "Unwrapped key matches original");
+                            }).catch(function(err){
+                                assert_unreached("Round trip for key unwrapped non-extractable threw an error - " + err.name + ': "' + err.message + '"');
+                            });
+                        }, "Can wrap and unwrap " + toWrap.name + " keys as non-extractable using " + fmt + " and " + wrapper.parameters.name);
+
+                        if (fmt === "jwk") {
+                            promise_test(function(test){
+                                var wrappedKey;
+                                return wrapAsNonExtractableJwk(toWrap.key,wrapper).then(function(wrappedResult){
+                                    wrappedKey = wrappedResult;
+                                    return subtle.unwrapKey("jwk", wrappedKey, wrapper.unwrappingKey, wrapper.parameters.wrapParameters, toWrap.algorithm, false, toWrap.usages);
+                                }).then(function(unwrappedResult){
+                                    assert_false(unwrappedResult.extractable, "Unwrapped key is non-extractable");
+                                    return equalKeys(toWrap.key,unwrappedResult);
+                                }).then(function(result){
+                                    assert_true(result, "Unwrapped key matches original");
+                                }).catch(function(err){
+                                    assert_unreached("Round trip for non-extractable key threw an error - " + err.name + ': "' + err.message + '"');
+                                }).then(function(){
+                                    return subtle.unwrapKey("jwk", wrappedKey, wrapper.unwrappingKey, wrapper.parameters.wrapParameters, toWrap.algorithm, true, toWrap.usages);
+                                }).then(function(unwrappedResult){
+                                    assert_unreached("Unwrapping a non-extractable JWK as extractable should fail");
+                                }).catch(function(err){
+                                    assert_equals(err.name, "DataError", "Unwrapping a non-extractable JWK as extractable fails with DataError");
+                                });
+                            }, "Can unwrap " + toWrap.name + " non-extractable keys using jwk and " + wrapper.parameters.name);
+                        }
+                    }
+                }
+            });
+        }));
+    }
+
+    // Implement key wrapping by hand to wrap a key as non-extractable JWK
+    function wrapAsNonExtractableJwk(key, wrapper){
+        var wrappingKey = wrapper.wrappingKey,
+            encryptKey;
+
+        return subtle.exportKey("jwk",wrappingKey)
+        .then(function(jwkWrappingKey){
+            // Update the key generation parameters to work as key import parameters
+            var params = Object.create(wrapper.parameters.generateParameters);
+            if(params.name === "AES-KW") {
+                params.name = "AES-CBC";
+                jwkWrappingKey.alg = "A"+params.length+"CBC";
+            } else if (params.name === "RSA-OAEP") {
+                params.modulusLength = undefined;
+                params.publicExponent = undefined;
+            }
+            jwkWrappingKey.key_ops = ["encrypt"];
+            return subtle.importKey("jwk", jwkWrappingKey, params, true, ["encrypt"]);
+        }).then(function(importedWrappingKey){
+            encryptKey = importedWrappingKey;
+            return subtle.exportKey("jwk",key);
+        }).then(function(exportedKey){
+            exportedKey.ext = false;
+            var jwk = JSON.stringify(exportedKey)
+            if (wrappingKey.algorithm.name === "AES-KW") {
+                return aeskw(encryptKey, str2ab(jwk.slice(0,-1) + " ".repeat(jwk.length%8 ? 8-jwk.length%8 : 0) + "}"));
+            } else {
+                return subtle.encrypt(wrapper.parameters.wrapParameters,encryptKey,str2ab(jwk));
+            }
+        });
+    }
+
+
+    // RSA-OAEP can only wrap relatively small payloads. AES-KW can only
+    // wrap payloads a multiple of 8 bytes long.
+    function wrappingIsPossible(exportedKey, algorithmName) {
+        if ("byteLength" in exportedKey && algorithmName === "AES-KW") {
+            return exportedKey.byteLength % 8 === 0;
+        }
+
+        if ("byteLength" in exportedKey && algorithmName === "RSA-OAEP") {
+            // RSA-OAEP can only encrypt payloads with lengths shorter
+            // than modulusLength - 2*hashLength - 1 bytes long. For
+            // a 4096 bit modulus and SHA-256, that comes to
+            // 4096/8 - 2*(256/8) - 1 = 512 - 2*32 - 1 = 447 bytes.
+            return exportedKey.byteLength <= 446;
+        }
+
+        if ("kty" in exportedKey && algorithmName === "AES-KW") {
+            return JSON.stringify(exportedKey).length % 8 == 0;
+        }
+
+        if ("kty" in exportedKey && algorithmName === "RSA-OAEP") {
+            return JSON.stringify(exportedKey).length <= 478;
+        }
+
+        return true;
+    }
+
+
+    // Helper methods follow:
+
+    // Are two exported keys equal
+    function equalExport(originalExport, roundTripExport) {
+        if ("byteLength" in originalExport) {
+            return equalBuffers(originalExport, roundTripExport);
+        } else {
+            return equalJwk(originalExport, roundTripExport);
+        }
+    }
+
+    // Are two array buffers the same?
+    function equalBuffers(a, b) {
+        if (a.byteLength !== b.byteLength) {
+            return false;
+        }
+
+        var aBytes = new Uint8Array(a);
+        var bBytes = new Uint8Array(b);
+
+        for (var i=0; i<a.byteLength; i++) {
+            if (aBytes[i] !== bBytes[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    // Are two Jwk objects "the same"? That is, does the object returned include
+    // matching values for each property that was expected? It's okay if the
+    // returned object has extra methods; they aren't checked.
+    function equalJwk(expected, got) {
+        var fields = Object.keys(expected);
+        var fieldName;
+
+        for(var i=0; i<fields.length; i++) {
+            fieldName = fields[i];
+            if (!(fieldName in got)) {
+                return false;
+            }
+            if (objectToString(expected[fieldName]) !== objectToString(got[fieldName])) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    // Character representation of any object we may use as a parameter.
+    function objectToString(obj) {
+        var keyValuePairs = [];
+
+        if (Array.isArray(obj)) {
+            return "[" + obj.map(function(elem){return objectToString(elem);}).join(", ") + "]";
+        } else if (typeof obj === "object") {
+            Object.keys(obj).sort().forEach(function(keyName) {
+                keyValuePairs.push(keyName + ": " + objectToString(obj[keyName]));
+            });
+            return "{" + keyValuePairs.join(", ") + "}";
+        } else if (typeof obj === "undefined") {
+            return "undefined";
+        } else {
+            return obj.toString();
+        }
+
+        var keyValuePairs = [];
+
+        Object.keys(obj).sort().forEach(function(keyName) {
+            var value = obj[keyName];
+            if (typeof value === "object") {
+                value = objectToString(value);
+            } else if (typeof value === "array") {
+                value = "[" + value.map(function(elem){return objectToString(elem);}).join(", ") + "]";
+            } else {
+                value = value.toString();
+            }
+
+            keyValuePairs.push(keyName + ": " + value);
+        });
+
+        return "{" + keyValuePairs.join(", ") + "}";
+    }
+
+    // Can we compare key values by using them
+    function canCompareNonExtractableKeys(key){
+        if (key.usages.indexOf("decrypt") !== -1) {
+            return true;
+        }
+        if (key.usages.indexOf("sign") !== -1) {
+            return true;
+        }
+        if (key.usages.indexOf("wrapKey") !== -1) {
+            return true;
+        }
+        if (key.usages.indexOf("deriveBits") !== -1) {
+            return true;
+        }
+        return false;
+    }
+
+    // Compare two keys by using them (works for non-extractable keys)
+    function equalKeys(expected, got){
+        if ( expected.algorithm.name !== got.algorithm.name ) {
+            return Promise.resolve(false);
+        }
+
+        var cryptParams, signParams, wrapParams, deriveParams;
+        switch(expected.algorithm.name){
+            case "AES-CTR" :
+                cryptParams = {name: "AES-CTR", counter: new Uint8Array(16), length: 64};
+                break;
+            case "AES-CBC" :
+                cryptParams = {name: "AES-CBC", iv: new Uint8Array(16) };
+                break;
+            case "AES-GCM" :
+                cryptParams = {name: "AES-GCM", iv: new Uint8Array(16) };
+                break;
+            case "RSA-OAEP" :
+                cryptParams = {name: "RSA-OAEP", label: new Uint8Array(8) };
+                break;
+            case "RSASSA-PKCS1-v1_5" :
+                signParams = {name: "RSASSA-PKCS1-v1_5"};
+                break;
+            case "RSA-PSS" :
+                signParams = {name: "RSA-PSS", saltLength: 32 };
+                break;
+            case "ECDSA" :
+                signParams = {name: "ECDSA", hash: "SHA-256"};
+                break;
+            case "HMAC" :
+                signParams = {name: "HMAC"};
+                break;
+            case "AES-KW" :
+                wrapParams = {name: "AES-KW"};
+                break;
+            case "ECDH" :
+                deriveParams = {name: "ECDH", public: ecdhPeerKey};
+                break;
+            default:
+                throw new Error("Unsupported algorithm for key comparison");
+        }
+
+        if (cryptParams) {
+            return subtle.exportKey("jwk",expected)
+            .then(function(jwkExpectedKey){
+                if (expected.algorithm.name === "RSA-OAEP") {
+                    ["d","p","q","dp","dq","qi","oth"].forEach(function(field){ delete jwkExpectedKey[field]; });
+                }
+                jwkExpectedKey.key_ops = ["encrypt"];
+                return subtle.importKey("jwk", jwkExpectedKey, expected.algorithm, true, ["encrypt"]);
+            }).then(function(expectedEncryptKey){
+                return subtle.encrypt(cryptParams, expectedEncryptKey, new Uint8Array(32));
+            }).then(function(encryptedData){
+                return subtle.decrypt(cryptParams, got, encryptedData);
+            }).then(function(decryptedData){
+                var result = new Uint8Array(decryptedData);
+                return !result.some(x => x);
+            });
+        } else if (signParams) {
+            var verifyKey;
+            return subtle.exportKey("jwk",expected)
+            .then(function(jwkExpectedKey){
+                if (expected.algorithm.name === "RSA-PSS" || expected.algorithm.name === "RSASSA-PKCS1-v1_5") {
+                    ["d","p","q","dp","dq","qi","oth"].forEach(function(field){ delete jwkExpectedKey[field]; });
+                }
+                if (expected.algorithm.name === "ECDSA") {
+                    delete jwkExpectedKey["d"];
+                }
+                jwkExpectedKey.key_ops = ["verify"];
+                return subtle.importKey("jwk", jwkExpectedKey, expected.algorithm, true, ["verify"]);
+            }).then(function(expectedVerifyKey){
+                verifyKey = expectedVerifyKey;
+                return subtle.sign(signParams, got, new Uint8Array(32));
+            }).then(function(signature){
+                return subtle.verify(signParams, verifyKey, signature, new Uint8Array(32));
+            });
+        } else if (wrapParams) {
+            var aKeyToWrap, wrappedWithExpected;
+            return subtle.importKey("raw", new Uint8Array(16), "AES-CBC", true, ["encrypt"])
+            .then(function(key){
+                aKeyToWrap = key;
+                return subtle.wrapKey("raw", aKeyToWrap, expected, wrapParams);
+            }).then(function(wrapResult){
+                wrappedWithExpected = Array.from((new Uint8Array(wrapResult)).values());
+                return subtle.wrapKey("raw", aKeyToWrap, got, wrapParams);
+            }).then(function(wrapResult){
+                var wrappedWithGot = Array.from((new Uint8Array(wrapResult)).values());
+                return wrappedWithGot.every((x,i) => x === wrappedWithExpected[i]);
+            });
+        } else {
+            var expectedDerivedBits;
+            return subtle.deriveBits(deriveParams, expected, 128)
+            .then(function(result){
+                expectedDerivedBits = Array.from((new Uint8Array(result)).values());
+                return subtle.deriveBits(deriveParams, got, 128);
+            }).then(function(result){
+                var gotDerivedBits = Array.from((new Uint8Array(result)).values());
+                return gotDerivedBits.every((x,i) => x === expectedDerivedBits[i]);
+            });
+        }
+    }
+
+    // Raw AES encryption
+    function aes( k, p ) {
+        return subtle.encrypt({name: "AES-CBC", iv: new Uint8Array(16) }, k, p).then(function(ciphertext){return ciphertext.slice(0,16);});
+    }
+
+    // AES Key Wrap
+    function aeskw(key, data) {
+        if (data.byteLength % 8 !== 0) {
+            throw new Error("AES Key Wrap data must be a multiple of 8 bytes in length");
+        }
+
+        var A = Uint8Array.from([0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0, 0, 0, 0, 0, 0, 0, 0]),
+            Av = new DataView(A.buffer),
+            R = [],
+            n = data.byteLength / 8;
+
+        for(var i = 0; i<data.byteLength; i+=8) {
+            R.push(new Uint8Array(data.slice(i,i+8)));
+        }
+
+        function aeskw_step(j, i, final, B) {
+            A.set(new Uint8Array(B.slice(0,8)));
+            Av.setUint32(4,Av.getUint32(4) ^ (n*j+i+1));
+            R[i] = new Uint8Array(B.slice(8,16));
+            if (final) {
+                R.unshift(A.slice(0,8));
+                var result = new Uint8Array(R.length * 8);
+                R.forEach(function(Ri,i){ result.set(Ri, i*8); });
+                return result;
+            } else {
+                A.set(R[(i+1)%n],8);
+                return aes(key,A);
+            }
+        }
+
+        var p = new Promise(function(resolve){
+            A.set(R[0],8);
+            resolve(aes(key,A));
+        });
+
+        for(var j=0;j<6;++j) {
+            for(var i=0;i<n;++i) {
+                p = p.then(aeskw_step.bind(undefined, j, i,j===5 && i===(n-1)));
+            }
+        }
+
+        return p;
+    }
+
+    function str2ab(str)        { return Uint8Array.from( str.split(''), function(s){return s.charCodeAt(0)} ); }
+    function ab2str(ab)         { return String.fromCharCode.apply(null, new Uint8Array(ab)); }
+
+
+}
diff --git a/src/third_party/web_platform_tests/intersection-observer/META.yml b/src/third_party/web_platform_tests/intersection-observer/META.yml
new file mode 100644
index 0000000..31dddab
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/META.yml
@@ -0,0 +1,3 @@
+spec: https://w3c.github.io/IntersectionObserver/
+suggested_reviewers:
+  - szager-chromium
diff --git a/src/third_party/web_platform_tests/intersection-observer/bounding-box.html b/src/third_party/web_platform_tests/intersection-observer/bounding-box.html
new file mode 100644
index 0000000..367243d
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/bounding-box.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#root {
+  overflow: visible;
+  height: 200px;
+  width: 160px;
+  border: 8px solid black;
+}
+#target {
+  margin: 10px;
+  width: 100px;
+  height: 100px;
+  padding: 10px;
+  background-color: green;
+}
+</style>
+
+<div id="root">
+  <div id="target" style="transform: translateY(300px)"></div>
+</div>
+
+<script>
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  var root = document.getElementById("root");
+  assert_true(!!root, "root exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {root: root});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "Test that the target's border bounding box is used to calculate intersection.");
+
+function step0() {
+  var targetBounds = clientBounds(target);
+  target.style.transform = "translateY(195px)";
+  runTestCycle(step1, "target.style.transform = 'translateY(195px)'");
+  checkLastEntry(entries, 0, targetBounds.concat(0, 0, 0, 0, 8, 184, 8, 224, false));
+}
+
+function step1() {
+  var targetBounds = clientBounds(target);
+  target.style.transform = "translateY(300px)";
+  runTestCycle(step2, "target.style.transform = 'translateY(300px)'");
+  checkLastEntry(entries, 1, targetBounds.concat(26, 146, 221, 224, 8, 184, 8, 224, true));
+}
+
+function step2() {
+  var targetBounds = clientBounds(target);
+  target.style.transform = "";
+  target.style.zoom = "2";
+  runTestCycle(step3, "target.style.zoom = 2");
+  checkLastEntry(entries, 2, targetBounds.concat(0, 0, 0, 0, 8, 184, 8, 224, false));
+}
+
+function step3() {
+  var targetBounds = clientBounds(target);
+  var intersectionWidth = (
+      176  // root width including border
+      -8   // root left border
+      -20  // target left margin * target zoom
+  ) / 2;   // convert to target's zoom factor.
+  var intersectionHeight = (216 - 8 - 20) / 2;
+  var intersectionRect = [targetBounds[0], targetBounds[0] + intersectionWidth,
+                          targetBounds[2], targetBounds[2] + intersectionHeight];
+  checkLastEntry(entries, 3, targetBounds.concat(intersectionRect).concat(8, 184, 8, 224, true));
+}
+
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/client-rect.html b/src/third_party/web_platform_tests/intersection-observer/client-rect.html
new file mode 100644
index 0000000..e85171c
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/client-rect.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+iframe {
+  width: 180px;
+  height: 100px;
+}
+</style>
+
+<iframe id="iframe" srcdoc="<div id='target' style='margin:0.5px;width:1000px;height:1000px;'></div>"></iframe>
+
+<script>
+var target;
+var entries = [];
+var observer;
+var iframe = document.getElementById("iframe");
+
+iframe.onload = function() {
+  runTestCycle(function() {
+    target = iframe.contentDocument.getElementById("target");
+    assert_true(!!target, "Target element exists.");
+    observer = new IntersectionObserver(function(changes) {
+      entries = entries.concat(changes);
+    });
+    observer.observe(target);
+    entries = entries.concat(observer.takeRecords());
+    assert_equals(entries.length, 0, "No initial notifications.");
+    runTestCycle(test0, "First rAF should generate notification.");
+  }, "IntersectionObserverEntry.boundingClientRect should match target.boundingClientRect()");
+};
+
+function test0() {
+  assert_equals(entries.length, 1, "One notification.");
+  var bcr = target.getBoundingClientRect();
+  checkLastEntry(entries, 0, [bcr.left, bcr.right, bcr.top, bcr.bottom]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/containing-block.html b/src/third_party/web_platform_tests/intersection-observer/containing-block.html
new file mode 100644
index 0000000..f7ce6fa
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/containing-block.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#root {
+  width: 170px;
+  height: 200px;
+  overflow-y: scroll;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+  position: absolute;
+}
+</style>
+
+<div id="root" style="position: absolute">
+  <div id="target" style="left: 50px; top: 250px"></div>
+</div>
+
+<script>
+var entries = [];
+var root, target;
+
+runTestCycle(function() {
+  root = document.getElementById("root");
+  assert_true(!!root, "root element exists.");
+  target = document.getElementById("target");
+  assert_true(!!target, "target element exists.");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes);
+  }, { root: root });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  target.style.top = "10px";
+  runTestCycle(test1, "In containing block and intersecting.");
+}, "IntersectionObserver should only report intersections if root is a containing block ancestor of target.");
+
+function test1() {
+  runTestCycle(test2, "In containing block and not intersecting.");
+  var rootBounds = contentBounds(root);
+  checkLastEntry(entries, 0, [58, 158, 18, 118, 58, 158, 18, 118].concat(rootBounds));
+  target.style.top = "250px";
+}
+
+function test2() {
+  runTestCycle(test3, "Not in containing block and intersecting.");
+  var rootBounds = contentBounds(root);
+  checkLastEntry(entries, 1, [58, 158, 258, 358, 0, 0, 0, 0].concat(rootBounds));
+  root.style.position = "static";
+  target.style.top = "10px";
+}
+
+function test3() {
+  runTestCycle(test4, "Not in containing block and not intersecting.");
+  checkLastEntry(entries, 1);
+  target.style.top = "250px";
+}
+
+function test4() {
+  checkLastEntry(entries, 1);
+  target.style.top = "0";
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/cross-origin-iframe.sub.html b/src/third_party/web_platform_tests/intersection-observer/cross-origin-iframe.sub.html
new file mode 100644
index 0000000..d444237
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/cross-origin-iframe.sub.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+iframe {
+  width: 160px;
+  height: 100px;
+  overflow-y: scroll;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+</style>
+
+<div class="spacer"></div>
+<iframe src="http://{{hosts[alt][]}}:{{ports[http][0]}}/intersection-observer/resources/cross-origin-subframe.html" sandbox="allow-scripts"></iframe>
+<div class="spacer"></div>
+
+<script>
+async_test(function(t) {
+  var iframe = document.querySelector("iframe");
+
+  function handleMessage(event) {
+    if (event.data.hasOwnProperty('scrollTo')) {
+      document.scrollingElement.scrollTop = event.data.scrollTo;
+      waitForNotification(t, function() { iframe.contentWindow.postMessage("", "*"); },
+        "document.scrollingElement.scrollTop = " + event.data.scrollTo);
+    } else if (event.data.hasOwnProperty('actual')) {
+      checkJsonEntries(event.data.actual, event.data.expected, event.data.description);
+    } else if (event.data.hasOwnProperty('DONE')) {
+      document.scrollingElement.scrollTop = 0;
+      t.done();
+    } else {
+      var description = event.data.description;
+      waitForNotification(t, function() { iframe.contentWindow.postMessage("", "*"); }, description);
+    }
+  }
+
+  window.addEventListener("message", t.step_func(handleMessage));
+
+  iframe.onload = t.step_func(function() {
+    waitForNotification(t, function() { iframe.contentWindow.postMessage("", "*") }, "setup");
+  });
+}, "Intersection observer test with no explicit root and target in a cross-origin iframe.");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/disconnect.html b/src/third_party/web_platform_tests/intersection-observer/disconnect.html
new file mode 100644
index 0000000..9c02daf
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/disconnect.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+
+<div class="spacer"></div>
+<div id="target"></div>
+<div class="spacer"></div>
+
+<script>
+var entries = [];
+var observer;
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "IntersectionObserver should not deliver pending notifications after disconnect().");
+
+function step0() {
+  runTestCycle(step1, "observer.disconnect()");
+  document.scrollingElement.scrollTop = 300;
+  observer.disconnect();
+  assert_equals(entries.length, 1, "Initial notification.");
+}
+
+function step1() {
+  assert_equals(entries.length, 1, "No new notifications.");
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/display-none.html b/src/third_party/web_platform_tests/intersection-observer/display-none.html
new file mode 100644
index 0000000..cae3509
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/display-none.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  background-color: green;
+  width: 100px;
+  height: 100px;
+}
+</style>
+
+<div id="target"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+
+runTestCycle(function() {
+  var target = document.getElementById("target");
+  var root = document.getElementById("root");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "Intersecting notification after first rAF.");
+}, "IntersectionObserver should send a not-intersecting notification for a target that gets display:none.");
+
+function step0() {
+  runTestCycle(step1, "Not-intersecting notification after setting display:none on target.");
+  checkLastEntry(entries, 0, [8, 108, 8, 108, 8, 108, 8, 108, 0, vw, 0, vh, true]);
+  target.style.display = "none";
+}
+
+function step1() {
+  runTestCycle(step2, "Intersecting notification after removing display:none on target.");
+  checkLastEntry(entries, 1, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, false]);
+  target.style.display = "";
+}
+
+function step2() {
+  checkLastEntry(entries, 2, [8, 108, 8, 108, 8, 108, 8, 108, 0, vw, 0, vh, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/document-scrolling-element-root.html b/src/third_party/web_platform_tests/intersection-observer/document-scrolling-element-root.html
new file mode 100644
index 0000000..443ff2e
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/document-scrolling-element-root.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+iframe {
+  height: 250px;
+  width: 150px;
+  border: 0;
+}
+</style>
+<iframe id="target-iframe" src="resources/iframe-no-root-subframe.html"></iframe>
+
+<script>
+var iframe = document.getElementById("target-iframe");
+var target;
+var root;
+var entries = [];
+
+iframe.onload = function() {
+  runTestCycle(function() {
+    assert_true(!!iframe, "iframe exists");
+
+    target = iframe.contentDocument.getElementById("target");
+    assert_true(!!target, "Target element exists.");
+    var observer = new IntersectionObserver(function(changes) {
+      entries = entries.concat(changes)
+    }, { root: iframe.contentDocument });
+    observer.observe(target);
+    entries = entries.concat(observer.takeRecords());
+    assert_equals(entries.length, 0, "No initial notifications.");
+    runTestCycle(step0, "First rAF.");
+  }, "Observer with explicit root which is the document.");
+};
+
+function step0() {
+  let vw = iframe.contentDocument.documentElement.clientWidth;
+  let vh = iframe.contentDocument.documentElement.clientHeight;
+  // The target element is partially clipped by the iframe's root scroller, so
+  // height of the intersection rect is (250 - 208) == 42.
+  checkLastEntry(entries, 0, [8, 108, 208, 308, 8, 108, 208, 250, 0, vw, 0, vh, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/edge-inclusive-intersection.html b/src/third_party/web_platform_tests/intersection-observer/edge-inclusive-intersection.html
new file mode 100644
index 0000000..b73c407
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/edge-inclusive-intersection.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#root {
+  width: 200px;
+  height: 200px;
+  overflow: visible;
+}
+#target {
+  background-color: green;
+}
+</style>
+
+<div id="root">
+  <div id="target" style="width: 100px; height: 100px; transform: translateY(250px)"></div>
+</div>
+
+<script>
+var entries = [];
+
+runTestCycle(function() {
+  var root = document.getElementById('root');
+  assert_true(!!root, "root element exists.");
+  var target = document.getElementById('target');
+  assert_true(!!target, "target element exists.");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes);
+  }, { root: root });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "IntersectionObserver should detect and report edge-adjacent and zero-area intersections.");
+
+function step0() {
+  runTestCycle(step1, "Set transform=translateY(200px) on target.");
+  checkLastEntry(entries, 0, [8, 108, 258, 358, 0, 0, 0, 0, 8, 208, 8, 208, false]);
+  target.style.transform = "translateY(200px)";
+}
+
+function step1() {
+  runTestCycle(step2, "Set transform=translateY(201px) on target.");
+  checkLastEntry(entries, 1, [8, 108, 208, 308, 8, 108, 208, 208, 8, 208, 8, 208, true]);
+  target.style.transform = "translateY(201px)";
+}
+
+function step2() {
+  runTestCycle(step3, "Set transform=translateY(185px) on target.");
+  checkLastEntry(entries, 2);
+  target.style.height = "0px";
+  target.style.width = "300px";
+  target.style.transform = "translateY(185px)";
+}
+
+function step3() {
+  checkLastEntry(entries, 3, [8, 308, 193, 193, 8, 208, 193, 193, 8, 208, 8, 208, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/empty-root-margin.html b/src/third_party/web_platform_tests/intersection-observer/empty-root-margin.html
new file mode 100644
index 0000000..9eaf856
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/empty-root-margin.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<style>
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+
+<div id="target"></div>
+
+<script>
+var target = document.getElementById("target");
+async_test((t) => {
+    var observer = new IntersectionObserver(t.step_func_done((entries) => {
+        var rootBounds = entries[0].rootBounds;
+        assert_equals(rootBounds.left, 0);
+        assert_equals(rootBounds.right, document.documentElement.clientWidth);
+        assert_equals(rootBounds.top, 0);
+        assert_equals(rootBounds.bottom, document.documentElement.clientHeight);
+        observer.disconnect();
+    }), { rootMargin: "" });
+    observer.observe(document.getElementById("target"));
+}, "An empty rootMargin string is interpreted as a margin of size zero");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/idlharness.window.js b/src/third_party/web_platform_tests/intersection-observer/idlharness.window.js
new file mode 100644
index 0000000..2059e1c
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/idlharness.window.js
@@ -0,0 +1,22 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+'use strict';
+
+// https://w3c.github.io/IntersectionObserver/
+
+idl_test(
+  ['intersection-observer'],
+  ['dom'],
+  idl_array => {
+    idl_array.add_objects({
+      IntersectionObserver: ['observer'],
+    });
+    var options = {
+      root: document.body,
+      rootMargin: '0px',
+      threshold: 1.0
+    }
+    self.observer = new IntersectionObserver(() => {}, options);
+  }
+);
diff --git a/src/third_party/web_platform_tests/intersection-observer/iframe-no-root-with-wrapping-scroller.html b/src/third_party/web_platform_tests/intersection-observer/iframe-no-root-with-wrapping-scroller.html
new file mode 100644
index 0000000..28e6d09
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/iframe-no-root-with-wrapping-scroller.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+iframe {
+  height: 100px;
+  width: 150px;
+}
+</style>
+
+<div class="spacer"></div>
+<div style="overflow: hidden">
+  <iframe id="target-iframe" src="resources/iframe-no-root-subframe.html"></iframe>
+</div>
+<div class="spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var iframe = document.getElementById("target-iframe");
+var target;
+var entries = [];
+
+iframe.onload = function() {
+  runTestCycle(function() {
+    assert_true(!!iframe, "iframe exists");
+
+    target = iframe.contentDocument.getElementById("target");
+    assert_true(!!target, "Target element exists.");
+    var observer = new IntersectionObserver(function(changes) {
+      entries = entries.concat(changes)
+    });
+    observer.observe(target);
+    entries = entries.concat(observer.takeRecords());
+    assert_equals(entries.length, 0, "No initial notifications.");
+    runTestCycle(step0, "First rAF.");
+  }, "Observer with the implicit root; target in a same-origin iframe.");
+};
+
+function step0() {
+  document.scrollingElement.scrollTop = 200;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = 200");
+  checkLastEntry(entries, 0, [8, 108, 208, 308, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+
+function step1() {
+  iframe.contentDocument.scrollingElement.scrollTop = 250;
+  runTestCycle(step2, "iframe.contentDocument.scrollingElement.scrollTop = 250");
+  assert_equals(entries.length, 1, "entries.length == 1");
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 100;
+  runTestCycle(step3, "document.scrollingElement.scrollTop = 100");
+  checkLastEntry(entries, 1, [8, 108, -42, 58, 8, 108, 0, 58, 0, vw, 0, vh, true]);
+}
+
+function step3() {
+  checkLastEntry(entries, 2, [8, 108, -42, 58, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+  document.scrollingElement.scrollTop = 0;
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/iframe-no-root.html b/src/third_party/web_platform_tests/intersection-observer/iframe-no-root.html
new file mode 100644
index 0000000..8532246
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/iframe-no-root.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+iframe {
+  height: 100px;
+  width: 150px;
+}
+</style>
+
+<div class="spacer"></div>
+<iframe id="target-iframe" src="resources/iframe-no-root-subframe.html"></iframe>
+<div class="spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var iframe = document.getElementById("target-iframe");
+var target;
+var entries = [];
+
+iframe.onload = function() {
+  runTestCycle(function() {
+    assert_true(!!iframe, "iframe exists");
+
+    target = iframe.contentDocument.getElementById("target");
+    assert_true(!!target, "Target element exists.");
+    var observer = new IntersectionObserver(function(changes) {
+      entries = entries.concat(changes)
+    });
+    observer.observe(target);
+    entries = entries.concat(observer.takeRecords());
+    assert_equals(entries.length, 0, "No initial notifications.");
+    runTestCycle(step0, "First rAF.");
+  }, "Observer with the implicit root; target in a same-origin iframe.");
+};
+
+function step0() {
+  document.scrollingElement.scrollTop = 200;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = 200");
+  checkLastEntry(entries, 0, [8, 108, 208, 308, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+
+function step1() {
+  iframe.contentDocument.scrollingElement.scrollTop = 250;
+  runTestCycle(step2, "iframe.contentDocument.scrollingElement.scrollTop = 250");
+  assert_equals(entries.length, 1, "entries.length == 1");
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 100;
+  runTestCycle(step3, "document.scrollingElement.scrollTop = 100");
+  checkLastEntry(entries, 1, [8, 108, -42, 58, 8, 108, 0, 58, 0, vw, 0, vh, true]);
+}
+
+function step3() {
+  checkLastEntry(entries, 2, [8, 108, -42, 58, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+  document.scrollingElement.scrollTop = 0;
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/initial-observation-with-threshold.html b/src/third_party/web_platform_tests/intersection-observer/initial-observation-with-threshold.html
new file mode 100644
index 0000000..b9218b0
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/initial-observation-with-threshold.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+#root {
+  display: inline-block;
+  overflow-y: scroll;
+  height: 240px;
+  border: 3px solid black;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  margin: 200px 0 0 0;
+  background-color: green;
+}
+</style>
+
+<div id="root">
+  <div id="target"></div>
+</div>
+
+<script>
+var entries = [];
+var root, target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  root = document.getElementById("root");
+  assert_true(!!root, "root exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, { root: root, threshold: [0.5] });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF");
+}, "First observation with a threshold.");
+
+function step0() {
+  root.scrollTop = 20;
+  runTestCycle(step1, "root.scrollTop = 20");
+  checkLastEntry(entries, 0, [ 11, 111, 211, 311, 11, 111, 211, 251, 11, 111, 11, 251, false]);
+}
+
+function step1() {
+  checkLastEntry(entries, 1, [ 11, 111, 191, 291, 11, 111, 191, 251, 11, 111, 11, 251, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/inline-client-rect.html b/src/third_party/web_platform_tests/intersection-observer/inline-client-rect.html
new file mode 100644
index 0000000..c096230
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/inline-client-rect.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 120px;
+  left: 0;
+}
+#scroller {
+  width: 250px;
+  overflow: auto;
+}
+#overflow {
+  width: 1000px;
+}
+.content {
+  width: 100px;
+  height: 20px;
+  padding: 40px 0;
+  text-align: center;
+  background-color: grey;
+  display: inline-block;
+}
+</style>
+
+<div id="scroller">
+  <div id="overflow">
+    <span><div class="content">1</div></span>
+    <span><div class="content">2</div></span>
+    <span><div class="content">3</div></span>
+    <span id="target"><div class="content">4</div></span>
+    <span><div class="content">5</div></span>
+  </div>
+</div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var scroller, target, spaceWidth, targetOffsetLeft, targetOffsetTop;
+
+runTestCycle(function() {
+  scroller = document.getElementById("scroller");
+  assert_true(!!scroller, "scroller exists");
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF");
+}, "Inline target");
+
+function step0() {
+  // Measure space width between two adjacent inlines.
+  let nextEl = target.nextElementSibling;
+  spaceWidth = nextEl.offsetLeft - target.offsetLeft - target.offsetWidth;
+  // 8px body margin + 3 preceding siblings @ (100px width + spaceWidth) each
+  targetOffsetLeft = 8 + 300 + (spaceWidth * 3);
+  // 8px body margin + 40px top padding
+  targetOffsetTop = 48;
+  let left = targetOffsetLeft;
+  let right = left + 100;
+  let top = targetOffsetTop;
+  let bottom = top + target.offsetHeight;
+
+  scroller.scrollLeft = 90;
+  runTestCycle(step1, "scroller.scrollLeft = 90");
+
+  checkLastEntry(entries, 0, [left, right, top, bottom,
+                              0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+
+function step1() {
+  // -90px for scroll offset
+  let left = targetOffsetLeft - 90;
+  let right = left + 100;
+  let top = targetOffsetTop;
+  let bottom = top + target.offsetHeight;
+  // 8px body margin + 250px client width of scroller
+  let scrollerRight = 258;
+  checkLastEntry(entries, 1, [left, right, top, bottom,
+                              left, scrollerRight, top, bottom,
+                              0, vw, 0, vh, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/inline-with-block-child-client-rect.html b/src/third_party/web_platform_tests/intersection-observer/inline-with-block-child-client-rect.html
new file mode 100644
index 0000000..81a8fd1
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/inline-with-block-child-client-rect.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 120px;
+  left: 0;
+}
+#target {
+  display: inline;
+}
+</style>
+
+<div id="target">
+  <div>
+    <img width=100 height=100 />
+  </div>
+</div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF");
+}, "Inline target containing a block child");
+
+function step0() {
+  assert_equals(entries.length, 1);
+  checkRect(entries[0].boundingClientRect, clientBounds(target),
+            "entry.boundingClientRect == target.getBoundingClientRect()");
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/isIntersecting-change-events.html b/src/third_party/web_platform_tests/intersection-observer/isIntersecting-change-events.html
new file mode 100644
index 0000000..99bc65b
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/isIntersecting-change-events.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#root {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 150px;
+  height: 200px;
+  overflow-y: scroll;
+}
+#target1, #target2, #target3, #target4 {
+  width: 100px;
+  height: 100px;
+}
+#target1 {
+  background-color: green;
+}
+#target2 {
+  background-color: red;
+}
+#target3 {
+  background-color: blue;
+}
+#target4 {
+  background-color: yellow;
+}
+</style>
+
+<div id="root">
+  <div id="target1"></div>
+  <div id="target2"></div>
+  <div id="target3"></div>
+</div>
+
+<script>
+var entries = [];
+var observer;
+
+runTestCycle(function() {
+  var root = document.getElementById('root');
+  var target1 = document.getElementById('target1');
+  var target2 = document.getElementById('target2');
+  var target3 = document.getElementById('target3');
+  assert_true(!!root, "root element exists.");
+  assert_true(!!target1, "target1 element exists.");
+  assert_true(!!target2, "target2 element exists.");
+  assert_true(!!target3, "target3 element exists.");
+  observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes);
+  }, { root: root });
+  observer.observe(target1);
+  observer.observe(target2);
+  observer.observe(target3);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "Rects in initial notifications should report initial positions.");
+}, "isIntersecting changes should trigger notifications.");
+
+function step0() {
+  assert_equals(entries.length, 3, "Has 3 initial notifications.");
+  checkRect(entries[0].boundingClientRect, [0, 100, 0, 100], "Check 1st entry rect");
+  assert_equals(entries[0].target.id, 'target1', "Check 1st entry target id.");
+  checkIsIntersecting(entries, 0, true);
+  checkRect(entries[1].boundingClientRect, [0, 100, 100, 200], "Check 2nd entry rect");
+  assert_equals(entries[1].target.id, 'target2', "Check 2nd entry target id.");
+  checkIsIntersecting(entries, 1, true);
+  checkRect(entries[2].boundingClientRect, [0, 100, 200, 300], "Check 3rd entry rect");
+  assert_equals(entries[2].target.id, 'target3', "Check 3rd entry target id.");
+  checkIsIntersecting(entries, 2, true);
+  runTestCycle(step1, "Set scrollTop=100 and check for no new notifications.");
+  root.scrollTop = 100;
+}
+
+function step1() {
+  assert_equals(entries.length, 3, "Has 3 total notifications because isIntersecting did not change.");
+  runTestCycle(step2, "Add 4th target.");
+  root.scrollTop = 0;
+  var target4 = document.createElement('div');
+  target4.setAttribute('id', 'target4');
+  root.appendChild(target4);
+  observer.observe(target4);
+}
+
+function step2() {
+  assert_equals(entries.length, 4, "Has 4 total notifications because 4th element was added.");
+  checkRect(entries[3].boundingClientRect, [0, 100, 300, 400], "Check 4th entry rect");
+  assert_equals(entries[3].target.id, 'target4', "Check 4th entry target id.");
+  checkIsIntersecting(entries, 3, false);
+  assert_equals(entries[3].intersectionRatio, 0, 'target4 initially has intersectionRatio of 0.');
+  runTestCycle(step3, "Set scrollTop=100 and check for one new notification.");
+  root.scrollTop = 100;
+}
+
+function step3() {
+  assert_equals(entries.length, 5, "Has 5 total notifications.");
+  checkRect(entries[4].boundingClientRect, [0, 100, 200, 300], "Check 5th entry rect");
+  assert_equals(entries[4].target.id, 'target4', "Check 5th entry target id.");
+  checkIsIntersecting(entries, 4, true);
+  assert_equals(entries[4].intersectionRatio, 0, 'target4 still has intersectionRatio of 0.');
+  root.scrollTop = 0; // reset to make it easier to refresh and run the test
+}
+
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/isIntersecting-threshold.html b/src/third_party/web_platform_tests/intersection-observer/isIntersecting-threshold.html
new file mode 100644
index 0000000..106b65e
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/isIntersecting-threshold.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+<style>
+#scroller { width: 100px; height: 100px; overflow: scroll; }
+#scroller > div { height: 800px; }
+#target { margin-top: 25px; height: 50px; background-color: blue; }
+</style>
+<div id="scroller">
+  <div>
+    <div id="target"></div>
+  </div>
+</div>
+
+<script>
+let entries = [];
+
+window.onload = function() {
+  runTestCycle(step2, "At initial scroll position");
+
+  scroller.scrollTop = 0;
+  let observer = new IntersectionObserver(
+    es => entries = entries.concat(es),
+    { threshold: 1 }
+  );
+  observer.observe(target);
+};
+
+function step2() {
+  runTestCycle(step3, "Scrolled to half way through target element");
+
+  assert_equals(entries.length, 1);
+  assert_equals(entries[0].intersectionRatio, 1);
+  assert_equals(entries[0].isIntersecting, true);
+  scroller.scrollTop = 50;
+}
+
+function step3() {
+  runTestCycle(step4, "Scrolled to target element completely off screen");
+
+  assert_equals(entries.length, 2);
+  assert_true(entries[1].intersectionRatio >= 0.5 &&
+              entries[1].intersectionRatio < 1);
+  assert_equals(entries[1].isIntersecting, true);
+  scroller.scrollTop = 100;
+}
+
+function step4() {
+  assert_equals(entries.length, 2);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/multiple-targets.html b/src/third_party/web_platform_tests/intersection-observer/multiple-targets.html
new file mode 100644
index 0000000..22353e3
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/multiple-targets.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+.target {
+  width: 100px;
+  height: 100px;
+  margin: 10px;
+  background-color: green;
+}
+</style>
+
+<div class="spacer"></div>
+<div id="target1" class="target"></div>
+<div id="target2" class="target"></div>
+<div id="target3" class="target"></div>
+
+<script>
+var entries = [];
+var target1, target2, target3;
+
+runTestCycle(function() {
+  target1 = document.getElementById("target1");
+  assert_true(!!target1, "target1 exists.");
+  target2 = document.getElementById("target2");
+  assert_true(!!target2, "target2 exists.");
+  target3 = document.getElementById("target3");
+  assert_true(!!target3, "target3 exists.");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target1);
+  observer.observe(target2);
+  observer.observe(target3);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "One observer with multiple targets.");
+
+function step0() {
+  document.scrollingElement.scrollTop = 150;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = 150");
+  assert_equals(entries.length, 3, "Three initial notifications.");
+  assert_equals(entries[0].target, target1, "entries[0].target === target1");
+  assert_equals(entries[1].target, target2, "entries[1].target === target2");
+  assert_equals(entries[2].target, target3, "entries[2].target === target3");
+}
+
+function step1() {
+  document.scrollingElement.scrollTop = 10000;
+  runTestCycle(step2, "document.scrollingElement.scrollTop = 10000");
+  assert_equals(entries.length, 4, "Four notifications.");
+  assert_equals(entries[3].target, target1, "entries[3].target === target1");
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 0;
+  runTestCycle(step3, "document.scrollingElement.scrollTop = 0");
+  assert_equals(entries.length, 6, "Six notifications.");
+  assert_equals(entries[4].target, target2, "entries[4].target === target2");
+  assert_equals(entries[5].target, target3, "entries[5].target === target3");
+}
+
+function step3() {
+  assert_equals(entries.length, 9, "Nine notifications.");
+  assert_equals(entries[6].target, target1, "entries[6].target === target1");
+  assert_equals(entries[7].target, target2, "entries[7].target === target2");
+  assert_equals(entries[8].target, target3, "entries[8].target === target3");
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/multiple-thresholds.html b/src/third_party/web_platform_tests/intersection-observer/multiple-thresholds.html
new file mode 100644
index 0000000..3599e1f
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/multiple-thresholds.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+
+<div class="spacer"></div>
+<div id="target"></div>
+<div class="spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, { threshold: [0, 0.25, 0.5, 0.75, 1] });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "Observer with multiple thresholds.");
+
+function step0() {
+  document.scrollingElement.scrollTop = 120;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = 120");
+  checkLastEntry(entries, 0, [8, 108, vh + 108, vh + 208, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+
+function step1() {
+  document.scrollingElement.scrollTop = 160;
+  runTestCycle(step2, "document.scrollingElement.scrollTop = 160");
+  checkLastEntry(entries, 1, [8, 108, vh - 12, vh + 88, 8, 108, vh - 12, vh, 0, vw, 0, vh, true]);
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 200;
+  runTestCycle(step3, "document.scrollingElement.scrollTop = 200");
+  checkLastEntry(entries, 2, [8, 108, vh - 52, vh + 48, 8, 108, vh - 52, vh, 0, vw, 0, vh, true]);
+}
+
+function step3() {
+  document.scrollingElement.scrollTop = 240;
+  runTestCycle(step4, "document.scrollingElement.scrollTop = 240");
+  checkLastEntry(entries, 3, [8, 108, vh - 92, vh + 8, 8, 108, vh - 92, vh, 0, vw, 0, vh, true]);
+}
+
+function step4() {
+  document.scrollingElement.scrollTop = vh + 140;
+  runTestCycle(step5, "document.scrollingElement.scrollTop = window.innerHeight + 140");
+  checkLastEntry(entries, 4, [8, 108, vh - 132, vh - 32, 8, 108, vh - 132, vh - 32, 0, vw, 0, vh, true]);
+}
+
+function step5() {
+  document.scrollingElement.scrollTop = vh + 160;
+  runTestCycle(step6, "document.scrollingElement.scrollTop = window.innerHeight + 160");
+  checkLastEntry(entries, 5, [8, 108, -32, 68, 8, 108, 0, 68, 0, vw, 0, vh, true]);
+}
+
+function step6() {
+  document.scrollingElement.scrollTop = vh + 200;
+  runTestCycle(step7, "document.scrollingElement.scrollTop = window.innerHeight + 200");
+  checkLastEntry(entries, 6, [8, 108, -52, 48, 8, 108, 0, 48, 0, vw, 0, vh, true]);
+}
+
+function step7() {
+  checkLastEntry(entries, 7, [8, 108, -92, 8, 8, 108, 0, 8, 0, vw, 0, vh, true]);
+  document.scrollingElement.scrollTop = vh + 220;
+  runTestCycle(step8, "document.scrollingElement.scrollTop = window.innerHeight + 220");
+}
+
+function step8() {
+  checkLastEntry(entries, 8, [8, 108, -112, -12, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+  document.scrollingElement.scrollTop = 0;
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/nested-cross-origin-iframe.sub.html b/src/third_party/web_platform_tests/intersection-observer/nested-cross-origin-iframe.sub.html
new file mode 100644
index 0000000..090d236
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/nested-cross-origin-iframe.sub.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/css/cssom-view/support/scroll-behavior.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+<style>
+.spacer {
+  height: calc(100vh + 100px);
+}
+</style>
+<div class="spacer"></div>
+<iframe id="iframe"></iframe>
+<script>
+
+promise_test(async t => {
+  iframe.src =      // non secure port.
+    get_host_info().HTTP_NOTSAMESITE_ORIGIN + "/intersection-observer/resources/nested-cross-origin-child-iframe.sub.html";
+
+  await new Promise(resolve => {
+    window.addEventListener("message", event => {
+      if (event.data == "ready") {
+        resolve();
+      }
+    }, { once: true });
+  });
+
+  let isIntersecting = false;
+  window.addEventListener("message", function listener(event) {
+    if (event.origin == get_host_info().HTTPS_NOTSAMESITE_ORIGIN) {
+      isIntersecting = event.data;
+      window.removeEventListener("message", listener);
+    }
+  });
+
+  await new Promise(resolve => waitForNotification(t, resolve));
+  await new Promise(resolve => waitForNotification(t, resolve));
+
+  assert_false(isIntersecting,
+               "The target element is not intersecting in all ancestor viewports");
+
+  // Scroll the iframe in this document into view, but still the target element
+  // in the grand child document is out of the child iframe's viewport.
+  iframe.scrollIntoView({ behavior: "instant" });
+
+  await waitForScrollEnd(document.scrollingElement);
+
+  assert_false(isIntersecting,
+               "The target element is not intersecting in all ancestor viewports");
+
+  // Now make the target element visible in the child iframe's viewport.
+  frames[0].postMessage("scroll", "*");
+
+  await new Promise(resolve => {
+    window.addEventListener("message", function listener(event) {
+      // It's possible that the message from the IntersectionObserver in the
+      // grand child document (HTTPS_NORSAMESITE_ORIGIN) is delivered ealier
+      // than scrollEnd message from the child document
+      // (HTTP_NOTSAMESITE_ORIGIN), so we need to differentiate them.
+      if (event.origin == get_host_info().HTTP_NOTSAMESITE_ORIGIN &&
+          event.data == "scrollEnd" ) {
+        window.removeEventListener("message", listener);
+        resolve();
+      }
+    });
+  });
+
+  await new Promise(resolve => waitForNotification(t, resolve));
+
+  assert_true(isIntersecting,
+              "The target element is now intersecting in all ancestor viewports");
+}, "IntersectionObserver with `implicit root` in a nested cross-origin iframe works");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/observer-attributes.html b/src/third_party/web_platform_tests/intersection-observer/observer-attributes.html
new file mode 100644
index 0000000..4e2e529
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/observer-attributes.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="root"></div>
+
+<script>
+test(function() {
+  var observer = new IntersectionObserver(function(e) {}, {});
+  test(function() { assert_equals(observer.root, null) },
+       "observer.root");
+  test(function() { assert_array_equals(observer.thresholds, [0]) },
+       "observer.thresholds");
+  test(function() { assert_equals(observer.rootMargin, "0px 0px 0px 0px") },
+       "observer.rootMargin");
+
+  var rootDiv = document.getElementById("root");
+  observer = new IntersectionObserver(function(e) {}, {
+    root: rootDiv,
+    threshold: [0, 0.25, 0.5, 1.0],
+    rootMargin: "10% 20px"
+  });
+  test(function() { assert_equals(observer.root, rootDiv) },
+       "set observer.root");
+  test(function() { assert_array_equals(observer.thresholds, [0, 0.25, 0.5, 1.0]) },
+       "set observer.thresholds");
+  test(function() { assert_equals(observer.rootMargin, "10% 20px 10% 20px") },
+       "set observer.rootMargin");
+}, "Observer attribute getters.");
+
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/observer-exceptions.html b/src/third_party/web_platform_tests/intersection-observer/observer-exceptions.html
new file mode 100644
index 0000000..126790f
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/observer-exceptions.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+test(function () {
+  assert_throws_js(RangeError, function() {
+    new IntersectionObserver(e => {}, {threshold: [1.1]})
+  })
+}, "IntersectionObserver constructor with { threshold: [1.1] }");
+
+test(function () {
+  assert_throws_js(TypeError, function() {
+    new IntersectionObserver(e => {}, {threshold: ["foo"]})
+  })
+}, 'IntersectionObserver constructor with { threshold: ["foo"] }');
+
+test(function () {
+  assert_throws_dom("SYNTAX_ERR", function() {
+    new IntersectionObserver(e => {}, {rootMargin: "1"})
+  })
+}, 'IntersectionObserver constructor with { rootMargin: "1" }');
+
+test(function () {
+  assert_throws_dom("SYNTAX_ERR", function() {
+    new IntersectionObserver(e => {}, {rootMargin: "2em"})
+  })
+}, 'IntersectionObserver constructor with { rootMargin: "2em" }');
+
+test(function () {
+  assert_throws_dom("SYNTAX_ERR", function() {
+    new IntersectionObserver(e => {}, {rootMargin: "auto"})
+  })
+}, 'IntersectionObserver constructor with { rootMargin: "auto" }');
+
+test(function () {
+  assert_throws_dom("SYNTAX_ERR", function() {
+    new IntersectionObserver(e => {}, {rootMargin: "calc(1px + 2px)"})
+  })
+}, 'IntersectionObserver constructor with { rootMargin: "calc(1px + 2px)" }');
+
+test(function () {
+  assert_throws_dom("SYNTAX_ERR", function() {
+    new IntersectionObserver(e => {}, {rootMargin: "1px !important"})
+  })
+}, 'IntersectionObserver constructor with { rootMargin: "1px !important" }');
+
+test(function () {
+  assert_throws_dom("SYNTAX_ERR", function() {
+    new IntersectionObserver(e => {}, {rootMargin: "1px 1px 1px 1px 1px"})
+  })
+}, 'IntersectionObserver constructor with { rootMargin: "1px 1px 1px 1px 1px" }');
+
+test(function () {
+  assert_throws_js(TypeError, function() {
+    let observer = new IntersectionObserver(c => {}, {});
+    observer.observe("foo");
+  })
+}, 'IntersectionObserver.observe("foo")');
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/observer-in-iframe.html b/src/third_party/web_platform_tests/intersection-observer/observer-in-iframe.html
new file mode 100644
index 0000000..e918bf1
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/observer-in-iframe.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+</style>
+<iframe id="target-iframe" src="resources/observer-in-iframe-subframe.html" width="150px" height="150px"></iframe>
diff --git a/src/third_party/web_platform_tests/intersection-observer/observer-without-js-reference.html b/src/third_party/web_platform_tests/intersection-observer/observer-without-js-reference.html
new file mode 100644
index 0000000..53100c50
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/observer-without-js-reference.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+<div class="spacer"></div>
+<div id="target"></div>
+<div class="spacer"></div>
+
+<script>
+var entries = [];
+
+runTestCycle(function() {
+  var target = document.getElementById("target");
+  assert_true(!!target, "Target exists");
+  function createObserver() {
+    new IntersectionObserver(function(changes) {
+      entries = entries.concat(changes)
+    }).observe(target);
+  }
+  createObserver();
+  runTestCycle(step0, "First rAF");
+}, "IntersectionObserver that is unreachable in js should still generate notifications.");
+
+function step0() {
+  document.scrollingElement.scrollTop = 300;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = 300");
+  assert_equals(entries.length, 1, "One notification.");
+}
+
+function step1() {
+  document.scrollingElement.scrollTop = 0;
+  assert_equals(entries.length, 2, "Two notifications.");
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/remove-element.html b/src/third_party/web_platform_tests/intersection-observer/remove-element.html
new file mode 100644
index 0000000..a093b22
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/remove-element.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#root {
+  display: inline-block;
+  overflow-y: scroll;
+  height: 200px;
+  border: 3px solid black;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+.spacer {
+  height: 300px;
+}
+</style>
+
+<div id="root">
+  <div id="leading-space" class="spacer"></div>
+  <div id="target"></div>
+  <div id="trailing-space" class="spacer"</div>
+</div>
+
+<script>
+var entries = [];
+var root, target, trailingSpace;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "Target exists");
+  trailingSpace = document.getElementById("trailing-space");
+  assert_true(!!trailingSpace, "TrailingSpace exists");
+  root = document.getElementById("root");
+  assert_true(!!root, "Root exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {root: root});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF");
+}, "Verify that not-intersecting notifications are sent when a target is removed from the DOM tree.");
+
+function step0() {
+  root.scrollTop = 150;
+  runTestCycle(step1, "root.scrollTop = 150");
+  checkLastEntry(entries, 0, [11, 111, 311, 411, 0, 0, 0, 0, 11, 111, 11, 211, false]);
+}
+
+function step1() {
+  root.removeChild(target);
+  runTestCycle(step2, "root.removeChild(target).");
+  checkLastEntry(entries, 1, [11, 111, 161, 261, 11, 111, 161, 211, 11, 111, 11, 211, true]);
+}
+
+function step2() {
+  root.scrollTop = 0;
+  root.insertBefore(target, trailingSpace);
+  runTestCycle(step3, "root.insertBefore(target, trailingSpace).");
+  checkLastEntry(entries, 2, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, false]);
+}
+
+function step3() {
+  root.scrollTop = 150;
+  runTestCycle(step4, "root.scrollTop = 150 after reinserting target.");
+  checkLastEntry(entries, 2);
+}
+
+function step4() {
+  checkLastEntry(entries, 3, [11, 111, 161, 261, 11, 111, 161, 211, 11, 111, 11, 211, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/cross-origin-child-iframe.sub.html b/src/third_party/web_platform_tests/intersection-observer/resources/cross-origin-child-iframe.sub.html
new file mode 100644
index 0000000..8e2c36e
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/cross-origin-child-iframe.sub.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<script src="/common/get-host-info.sub.js"></script>
+<iframe id="iframe"></iframe>
+<script>
+iframe.src =
+  get_host_info().ORIGIN + "/intersection-observer/resources/same-origin-grand-child-iframe.html";
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/cross-origin-subframe.html b/src/third_party/web_platform_tests/intersection-observer/resources/cross-origin-subframe.html
new file mode 100644
index 0000000..4305ed1
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/cross-origin-subframe.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<div style="height: 200px; width: 100px;"></div>
+<div id="target" style="background-color: green; width:100px; height:100px"></div>
+<div id="empty-target" style="width: 100px"></div>
+<div style="height: 200px; width: 100px;"></div>
+
+<script>
+var port;
+var entries = [];
+var target = document.getElementById("target");
+var emptyTarget = document.getElementById("empty-target");
+var scroller = document.scrollingElement;
+var nextStep;
+
+function clientRectToJson(rect) {
+  if (!rect)
+    return "null";
+  return {
+    top: rect.top,
+    right: rect.right,
+    bottom: rect.bottom,
+    left: rect.left
+  };
+}
+
+function entryToJson(entry) {
+  return {
+    boundingClientRect: clientRectToJson(entry.boundingClientRect),
+    intersectionRect: clientRectToJson(entry.intersectionRect),
+    rootBounds: clientRectToJson(entry.rootBounds),
+    isIntersecting: entry.isIntersecting,
+    target: entry.target.id
+  };
+}
+
+// Note that we never use RAF in this code, because this frame might get render-throttled.
+// Instead of RAF-ing, we just post an empty message to the parent window, which will
+// RAF when it is received, and then send us a message to cause the next step to run.
+
+// Use a rootMargin here, and verify it does NOT get applied for the cross-origin case.
+var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+}, { rootMargin: "7px" });
+observer.observe(target);
+observer.observe(emptyTarget);
+
+function step0() {
+  entries = entries.concat(observer.takeRecords());
+  nextStep = step1;
+  var expected = [{
+    boundingClientRect: [8, 108, 208, 308],
+    intersectionRect: [0, 0, 0, 0],
+    rootBounds: "null",
+    isIntersecting: false,
+    target: target.id
+  }, {
+    boundingClientRect: [8, 108, 308, 308],
+    intersectionRect: [0, 0, 0, 0],
+    rootBounds: "null",
+    isIntersecting: false,
+    target: emptyTarget.id
+  }];
+  port.postMessage({
+    actual: entries.map(entryToJson),
+    expected: expected,
+    description: "First rAF"
+  }, "*");
+  entries = [];
+  port.postMessage({scrollTo: 200}, "*");
+}
+
+function step1() {
+  entries = entries.concat(observer.takeRecords());
+  port.postMessage({
+    actual: entries.map(entryToJson),
+    expected: [],
+    description: "topDocument.scrollingElement.scrollTop = 200"
+  }, "*");
+  entries = [];
+  scroller.scrollTop = 250;
+  nextStep = step2;
+  port.postMessage({}, "*");
+}
+
+function step2() {
+  entries = entries.concat(observer.takeRecords());
+  var expected = [{
+    boundingClientRect: [8, 108, -42, 58],
+    intersectionRect: [8, 108, 0, 58],
+    rootBounds: "null",
+    isIntersecting: true,
+    target: target.id
+  }, {
+    boundingClientRect: [8, 108, 58, 58],
+    intersectionRect: [8, 108, 58, 58],
+    rootBounds: "null",
+    isIntersecting: true,
+    target: emptyTarget.id
+  }];
+  port.postMessage({
+    actual: entries.map(entryToJson),
+    expected: expected,
+    description: "iframeDocument.scrollingElement.scrollTop = 250"
+  }, "*");
+  entries = [];
+  nextStep = step3;
+  port.postMessage({scrollTo: 100}, "*");
+}
+
+function step3() {
+  entries = entries.concat(observer.takeRecords());
+  var expected = [{
+    boundingClientRect: [8, 108, -42, 58],
+    intersectionRect: [0, 0, 0, 0],
+    rootBounds: "null",
+    isIntersecting: false,
+    target: target.id
+  }, {
+    boundingClientRect: [8, 108, 58, 58],
+    intersectionRect: [0, 0, 0, 0],
+    rootBounds: "null",
+    isIntersecting: false,
+    target: emptyTarget.id
+  }];
+  port.postMessage({
+    actual: entries.map(entryToJson),
+    expected: expected,
+    description: "topDocument.scrollingElement.scrollTop = 100"
+  }, "*");
+  port.postMessage({DONE: 1}, "*");
+}
+
+function handleMessage(event)
+{
+  port = event.source;
+  nextStep();
+}
+
+nextStep = step0;
+window.addEventListener("message", handleMessage);
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/iframe-no-root-subframe.html b/src/third_party/web_platform_tests/intersection-observer/resources/iframe-no-root-subframe.html
new file mode 100644
index 0000000..ee63a06
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/iframe-no-root-subframe.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<div style="height: 200px; width: 100px;"></div>
+<div id="target" style="background-color: green; width:100px; height:100px"></div>
+<div style="height: 200px; width: 100px;"></div>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/intersection-observer-test-utils.js b/src/third_party/web_platform_tests/intersection-observer/resources/intersection-observer-test-utils.js
new file mode 100644
index 0000000..7db26d7
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/intersection-observer-test-utils.js
@@ -0,0 +1,196 @@
+// Here's how waitForNotification works:
+//
+// - myTestFunction0()
+//   - waitForNotification(myTestFunction1)
+//     - requestAnimationFrame()
+//   - Modify DOM in a way that should trigger an IntersectionObserver callback.
+// - BeginFrame
+//   - requestAnimationFrame handler runs
+//     - Second requestAnimationFrame()
+//   - Style, layout, paint
+//   - IntersectionObserver generates new notifications
+//     - Posts a task to deliver notifications
+// - Task to deliver IntersectionObserver notifications runs
+//   - IntersectionObserver callbacks run
+// - Second requestAnimationFrameHandler runs
+//     - step_timeout()
+// - step_timeout handler runs
+//   - myTestFunction1()
+//     - [optional] waitForNotification(myTestFunction2)
+//       - requestAnimationFrame()
+//     - Verify newly-arrived IntersectionObserver notifications
+//     - [optional] Modify DOM to trigger new notifications
+//
+// Ideally, it should be sufficient to use requestAnimationFrame followed
+// by two step_timeouts, with the first step_timeout firing in between the
+// requestAnimationFrame handler and the task to deliver notifications.
+// However, the precise timing of requestAnimationFrame, the generation of
+// a new display frame (when IntersectionObserver notifications are
+// generated), and the delivery of these events varies between engines, making
+// this tricky to test in a non-flaky way.
+//
+// In particular, in WebKit, requestAnimationFrame and the generation of
+// a display frame are two separate tasks, so a step_timeout called within
+// requestAnimationFrame can fire before a display frame is generated.
+//
+// In Gecko, on the other hand, requestAnimationFrame and the generation of
+// a display frame are a single task, and IntersectionObserver notifications
+// are generated during this task. However, the task posted to deliver these
+// notifications can fire after the following requestAnimationFrame.
+//
+// This means that in general, by the time the second requestAnimationFrame
+// handler runs, we know that IntersectionObservations have been generated,
+// and that a task to deliver these notifications has been posted (though
+// possibly not yet delivered). Then, by the time the step_timeout() handler
+// runs, these notifications have been delivered.
+//
+// Since waitForNotification uses a double-rAF, it is now possible that
+// IntersectionObservers may have generated more notifications than what is
+// under test, but have not yet scheduled the new batch of notifications for
+// delivery. As a result, observer.takeRecords should NOT be used in tests:
+//
+// - myTestFunction0()
+//   - waitForNotification(myTestFunction1)
+//     - requestAnimationFrame()
+//   - Modify DOM in a way that should trigger an IntersectionObserver callback.
+// - BeginFrame
+//   - requestAnimationFrame handler runs
+//     - Second requestAnimationFrame()
+//   - Style, layout, paint
+//   - IntersectionObserver generates a batch of notifications
+//     - Posts a task to deliver notifications
+// - Task to deliver IntersectionObserver notifications runs
+//   - IntersectionObserver callbacks run
+// - BeginFrame
+//   - Second requestAnimationFrameHandler runs
+//     - step_timeout()
+//   - IntersectionObserver generates another batch of notifications
+//     - Post task to deliver notifications
+// - step_timeout handler runs
+//   - myTestFunction1()
+//     - At this point, observer.takeRecords will get the second batch of
+//       notifications.
+function waitForNotification(t, f) {
+  requestAnimationFrame(function() {
+    requestAnimationFrame(function() { t.step_timeout(f, 0); });
+  });
+}
+
+// If you need to wait until the IntersectionObserver algorithm has a chance
+// to run, but don't need to wait for delivery of the notifications...
+function waitForFrame(t, f) {
+  requestAnimationFrame(function() {
+    t.step_timeout(f, 0);
+  });
+}
+
+// The timing of when runTestCycle is called is important.  It should be
+// called:
+//
+//   - Before or during the window load event, or
+//   - Inside of a prior runTestCycle callback, *before* any assert_* methods
+//     are called.
+//
+// Following these rules will ensure that the test suite will not abort before
+// all test steps have run.
+//
+// If the 'delay' parameter to the IntersectionObserver constructor is used,
+// tests will need to add the same delay to their runTestCycle invocations, to
+// wait for notifications to be generated and delivered.
+function runTestCycle(f, description, delay) {
+  async_test(function(t) {
+    if (delay) {
+      step_timeout(() => {
+        waitForNotification(t, t.step_func_done(f));
+      }, delay);
+    } else {
+      waitForNotification(t, t.step_func_done(f));
+    }
+  }, description);
+}
+
+// Root bounds for a root with an overflow clip as defined by:
+//   http://wicg.github.io/IntersectionObserver/#intersectionobserver-root-intersection-rectangle
+function contentBounds(root) {
+  var left = root.offsetLeft + root.clientLeft;
+  var right = left + root.clientWidth;
+  var top = root.offsetTop + root.clientTop;
+  var bottom = top + root.clientHeight;
+  return [left, right, top, bottom];
+}
+
+// Root bounds for a root without an overflow clip as defined by:
+//   http://wicg.github.io/IntersectionObserver/#intersectionobserver-root-intersection-rectangle
+function borderBoxBounds(root) {
+  var left = root.offsetLeft;
+  var right = left + root.offsetWidth;
+  var top = root.offsetTop;
+  var bottom = top + root.offsetHeight;
+  return [left, right, top, bottom];
+}
+
+function clientBounds(element) {
+  var rect = element.getBoundingClientRect();
+  return [rect.left, rect.right, rect.top, rect.bottom];
+}
+
+function rectArea(rect) {
+  return (rect.left - rect.right) * (rect.bottom - rect.top);
+}
+
+function checkRect(actual, expected, description, all) {
+  if (!expected.length)
+    return;
+  assert_equals(actual.left | 0, expected[0] | 0, description + '.left');
+  assert_equals(actual.right | 0, expected[1] | 0, description + '.right');
+  assert_equals(actual.top | 0, expected[2] | 0, description + '.top');
+  assert_equals(actual.bottom | 0, expected[3] | 0, description + '.bottom');
+}
+
+function checkLastEntry(entries, i, expected) {
+  assert_equals(entries.length, i + 1, 'entries.length');
+  if (expected) {
+    checkRect(
+        entries[i].boundingClientRect, expected.slice(0, 4),
+        'entries[' + i + '].boundingClientRect', entries[i]);
+    checkRect(
+        entries[i].intersectionRect, expected.slice(4, 8),
+        'entries[' + i + '].intersectionRect', entries[i]);
+    checkRect(
+        entries[i].rootBounds, expected.slice(8, 12),
+        'entries[' + i + '].rootBounds', entries[i]);
+    if (expected.length > 12) {
+      assert_equals(
+          entries[i].isIntersecting, expected[12],
+          'entries[' + i + '].isIntersecting');
+    }
+  }
+}
+
+function checkJsonEntry(actual, expected) {
+  checkRect(
+      actual.boundingClientRect, expected.boundingClientRect,
+      'entry.boundingClientRect');
+  checkRect(
+      actual.intersectionRect, expected.intersectionRect,
+      'entry.intersectionRect');
+  if (actual.rootBounds == 'null')
+    assert_equals(expected.rootBounds, 'null', 'rootBounds is null');
+  else
+    checkRect(actual.rootBounds, expected.rootBounds, 'entry.rootBounds');
+  assert_equals(actual.isIntersecting, expected.isIntersecting);
+  assert_equals(actual.target, expected.target);
+}
+
+function checkJsonEntries(actual, expected, description) {
+  test(function() {
+    assert_equals(actual.length, expected.length);
+    for (var i = 0; i < actual.length; i++)
+      checkJsonEntry(actual[i], expected[i]);
+  }, description);
+}
+
+function checkIsIntersecting(entries, i, expected) {
+  assert_equals(entries[i].isIntersecting, expected,
+    'entries[' + i + '].target.isIntersecting equals ' + expected);
+}
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/nested-cross-origin-child-iframe.sub.html b/src/third_party/web_platform_tests/intersection-observer/resources/nested-cross-origin-child-iframe.sub.html
new file mode 100644
index 0000000..78f3d2c
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/nested-cross-origin-child-iframe.sub.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/css/cssom-view/support/scroll-behavior.js"></script>
+<style>
+.spacer {
+  height: calc(100vh + 100px);
+}
+</style>
+<div class="spacer"></div>
+<iframe id="iframe"></iframe>
+<script>
+iframe.src =      // secure port
+  get_host_info().HTTPS_NOTSAMESITE_ORIGIN + "/intersection-observer/resources/nested-cross-origin-grand-child-iframe.html";
+
+window.addEventListener("message", async event => {
+  if (event.data == "scroll") {
+    iframe.scrollIntoView({ behavior: "instant" });
+    await waitForScrollEnd(document.scrollingElement);
+    window.parent.postMessage("scrollEnd", "*");
+  }
+});
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/nested-cross-origin-grand-child-iframe.html b/src/third_party/web_platform_tests/intersection-observer/resources/nested-cross-origin-grand-child-iframe.html
new file mode 100644
index 0000000..3676760
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/nested-cross-origin-grand-child-iframe.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<div id="target" style="height: 100px; background-color: green;"></div>
+<script>
+const observer = new IntersectionObserver(records => {
+  records.forEach(record => {
+    if (record.isIntersecting) {
+      window.top.postMessage(record.isIntersecting, "*");
+    }
+  });
+}, {});
+observer.observe(target);
+window.addEventListener("load", () => {
+  window.top.postMessage("ready", "*");
+});
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/observer-in-iframe-subframe.html b/src/third_party/web_platform_tests/intersection-observer/resources/observer-in-iframe-subframe.html
new file mode 100644
index 0000000..9d0769a
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/observer-in-iframe-subframe.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./intersection-observer-test-utils.js"></script>
+
+<style>
+#root {
+  width: 200px;
+  height: 200px;
+}
+#scroller {
+  width: 160px;
+  height: 200px;
+  overflow-y: scroll;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+.spacer {
+  height: 300px;
+}
+</style>
+
+<div id="root">
+  <div id="scroller">
+    <div class="spacer"></div>
+    <div id="target"></div>
+    <div class="spacer"></div>
+  </div>
+</div>
+
+<script>
+setup({message_events: [], output_document: window.parent.document});
+
+var entries = [];
+var root, scroller, target;
+
+runTestCycle(function() {
+  root = document.getElementById("root");
+  assert_true(!!root, "Root element exists.");
+  scroller = document.getElementById("scroller");
+  assert_true(!!scroller, "Scroller element exists.");
+  target = document.getElementById("target");
+  assert_true(!!target, "Target element exists.");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {root: root});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.")
+  runTestCycle(step1, "First rAF.");
+}, "IntersectionObserver in iframe with explicit root.");
+
+function step1() {
+  scroller.scrollTop = 250;
+  runTestCycle(step2, "scroller.scrollTop = 250");
+  checkLastEntry(entries, 0, [8, 108, 308, 408, 0, 0, 0, 0, 8, 208, 8, 208, false]);
+}
+
+function step2() {
+  checkLastEntry(entries, 1, [8, 108, 58, 158, 8, 108, 58, 158, 8, 208, 8, 208, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/same-origin-grand-child-iframe.html b/src/third_party/web_platform_tests/intersection-observer/resources/same-origin-grand-child-iframe.html
new file mode 100644
index 0000000..25db5a2
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/same-origin-grand-child-iframe.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<div id="target"></div>
+<script>
+const observer = new IntersectionObserver(records => {
+  window.top.postMessage(records[0].rootBounds, "*");
+}, {});
+observer.observe(target);
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/scaled-target-subframe.html b/src/third_party/web_platform_tests/intersection-observer/resources/scaled-target-subframe.html
new file mode 100644
index 0000000..8f6f930
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/scaled-target-subframe.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<style>
+html, body {
+  margin: 0;
+}
+#target {
+  width: 100px;
+  height: 100px;
+}
+</style>
+
+<div id="target">target</div>
+
+<script>
+var delay = 100;
+var results = [];
+
+function waitForNotification(f) {
+  setTimeout(() => {
+    requestAnimationFrame(function () {
+      requestAnimationFrame(function () {
+        setTimeout(f)
+      })
+    })
+  }, delay)
+}
+
+window.addEventListener("message", event => {
+  waitForNotification(() => {
+    window.parent.postMessage(results.map(e => e.isVisible), "*");
+    results = [];
+  });
+});
+
+onload = () => {
+  var target = document.getElementById("target");
+  var observer = new IntersectionObserver(entries => {
+    results = entries;
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(document.getElementById("target"));
+  window.parent.postMessage("", "*");
+};
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/timestamp-subframe.html b/src/third_party/web_platform_tests/intersection-observer/resources/timestamp-subframe.html
new file mode 100644
index 0000000..143e4f6
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/timestamp-subframe.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<style>
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+.spacer {
+  width: height: 100px
+}
+</style>
+
+<div class="spacer"></div>
+<div id="target"></div>
+<div class="spacer"></div>
+
+<script>
+document.createObserverCallback = function(entries) {
+  return function(newEntries) {
+    for (var i in newEntries) {
+      entries.push(newEntries[i]);
+    }
+  };
+}
+document.createObserver = function(callback) {
+  return new IntersectionObserver(callback, {});
+};
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/v2-subframe.html b/src/third_party/web_platform_tests/intersection-observer/resources/v2-subframe.html
new file mode 100644
index 0000000..295bbf0
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/v2-subframe.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<div id="target">target</div>
+<script>
+var delay = 100;
+var results = [];
+
+function waitForNotification(f) {
+  setTimeout(() => {
+    requestAnimationFrame(function () {
+      requestAnimationFrame(function () {
+        setTimeout(f)
+      })
+    })
+  }, delay)
+}
+
+window.addEventListener("message", event => {
+  waitForNotification(() => {
+    window.parent.postMessage(results.map(e => e.isVisible), "*");
+    results = [];
+  });
+});
+
+onload = () => {
+  var target = document.getElementById("target");
+  var observer = new IntersectionObserver(entries => {
+    results = entries;
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(document.getElementById("target"));
+  window.parent.postMessage("", "*");
+};
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/root-margin-root-element.html b/src/third_party/web_platform_tests/intersection-observer/root-margin-root-element.html
new file mode 100644
index 0000000..6016d45
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/root-margin-root-element.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+#root {
+  display: inline-block;
+  overflow-y: scroll;
+  height: 200px;
+  border: 3px solid black;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+
+<div class="spacer"></div>
+<div id="root">
+  <div style="height: 300px;"></div>
+  <div id="target"></div>
+</div>
+<div class="spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var root, target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  root = document.getElementById("root");
+  assert_true(!!root, "root exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, { root: root, rootMargin: "10px 20% 40% 30px" });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF");
+}, "Root margin with explicit root.");
+
+function step0() {
+  document.scrollingElement.scrollTop = vh;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = window.innerHeight.");
+  checkLastEntry(entries, 0, [ 11, 111, vh + 411, vh + 511, 0, 0, 0, 0, -19, 131, vh + 101, vh + 391, false]);
+}
+
+function step1() {
+  root.scrollTop = 50;
+  runTestCycle(step2, "root.scrollTop = 50, putting target into root margin");
+  assert_equals(entries.length, 1, "No notifications after scrolling frame.");
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 0;
+  runTestCycle(step3, "document.scrollingElement.scrollTop = 0.");
+  checkLastEntry(entries, 1, [11, 111, 361, 461, 11, 111, 361, 391, -19, 131, 101, 391, true]);
+}
+
+function step3() {
+  root.scrollTop = 0;
+  runTestCycle(step4, "root.scrollTop = 0");
+  checkLastEntry(entries, 1);
+}
+
+function step4() {
+  root.scrollTop = 50;
+  runTestCycle(step5, "root.scrollTop = 50 with root scrolled out of view.");
+  checkLastEntry(entries, 2, [ 11, 111, vh + 411, vh + 511, 0, 0, 0, 0, -19, 131, vh + 101, vh + 391, false]);
+}
+
+// This tests that notifications are generated even when the root element is off screen.
+function step5() {
+  checkLastEntry(entries, 3, [11, 111, vh + 361, vh + 461, 11, 111, vh + 361, vh + 391, -19, 131, vh + 101, vh + 391, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/root-margin-rounding.html b/src/third_party/web_platform_tests/intersection-observer/root-margin-rounding.html
new file mode 100644
index 0000000..f5e3323
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/root-margin-rounding.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1553673">
+<style>
+  html { width: 100vw; height: 100vh }
+</style>
+<script>
+const t = async_test("IntersectionObserver root margin cannot end up with negative rect (and thus non-intersecting) due to rounding");
+
+let remainingTests = 100;
+
+// This is just a best-effort test to catch issues.
+for (let i = 0; i < 100; ++i) {
+  let offset = i / 100;
+  let observer;
+  observer = new IntersectionObserver(t.step_func(function(entries) {
+    assert_equals(entries.length, 1);
+    assert_equals(entries[0].target, document.documentElement);
+    assert_true(entries[0].isIntersecting, "should be intersecting at " + offset);
+    if (!--remainingTests)
+      t.done();
+    observer.disconnect();
+  }), { rootMargin: `${-100 * (1 - offset)}% 0px ${-100 * offset}%` });
+  observer.observe(document.documentElement);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/root-margin.html b/src/third_party/web_platform_tests/intersection-observer/root-margin.html
new file mode 100644
index 0000000..898454c
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/root-margin.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  display: inline-block;
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+.vertical-spacer {
+  height: calc(100vh + 100px);
+}
+.horizontal-spacer {
+  display: inline-block;
+  width: 120vw;
+}
+</style>
+
+<div class="vertical-spacer"></div>
+<div style="white-space:nowrap;">
+  <div class="horizontal-spacer"></div>
+  <div id="target"></div>
+  <div class="horizontal-spacer"></div>
+</div>
+<div class="vertical-spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "Target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, { rootMargin: "10px 20% 40% 30px" });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "Root margin tests");
+
+function step0() {
+  var targetBounds = clientBounds(target);
+  document.scrollingElement.scrollLeft = 100;
+  runTestCycle(step1, "document.scrollingElement.scrollLeft = 100");
+  checkLastEntry(entries, 0, targetBounds.concat(0, 0, 0, 0, -30, vw * 1.2, -10, vh * 1.4, false));
+}
+
+function step1() {
+  var targetBounds = clientBounds(target);
+  var sw = window.innerWidth - document.documentElement.clientWidth;
+  var sh = window.innerHeight - document.documentElement.clientHeight;
+  document.scrollingElement.scrollTop = vh + 200;
+  runTestCycle(step2, "document.scrollingElement.scrollTop = document.documentElement.clientHeight + 200");
+  checkLastEntry(entries, 1, targetBounds.concat(
+    targetBounds[0], Math.min(targetBounds[1], vw * 1.2), vh + 108 + sh, Math.min(vh + 208 + sw, vh * 1.4),
+    -30, vw * 1.2, -10, vh * 1.4,
+    true
+  ));
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = vh + 300;
+  runTestCycle(step3, "document.scrollingElement.scrollTop = document.documentElement.clientHeight + 300");
+  checkLastEntry(entries, 1);
+}
+
+function step3() {
+  var targetBounds = clientBounds(target);
+  document.scrollingElement.scrollLeft = 0;
+  document.scrollingElement.scrollTop = 0;
+  checkLastEntry(entries, 2, targetBounds.concat(0, 0, 0, 0, -30, vw * 1.2, -10, vh * 1.4, false));
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/rtl-clipped-root.html b/src/third_party/web_platform_tests/intersection-observer/rtl-clipped-root.html
new file mode 100644
index 0000000..a30c6e3
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/rtl-clipped-root.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html dir="rtl">
+<head>
+  <meta name="viewport" content="width=device-width,initial-scale=1">
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+  <script src="./resources/intersection-observer-test-utils.js"></script>
+
+  <style>
+  pre, #log {
+    position: absolute;
+    top: 120px;
+    left: 0;
+  }
+  #root {
+    width: 350px;
+    height: 100px;
+    border: 1px solid black;
+    display: flex;
+    flex-direction: row;
+    overflow-x: auto;
+  }
+  #target-start, #target-end {
+    width: 100px;
+    height: 100px;
+    flex-shrink: 0;
+    background-color: green;
+    text-align: center;
+  }
+  #target-end {
+    margin-inline-start: 500px;
+  }
+  </style>
+</head>
+
+<div id="root">
+  <div id="target-start">start</div>
+  <div id="target-end">end</div>
+</div>
+
+<script>
+runTestCycle(function() {
+  let io = new IntersectionObserver(entries => {
+    entries.forEach(entry => {
+      if (entry.isIntersecting) {
+        entry.target.classList.add("intersecting");
+      } else {
+        entry.target.classList.remove("intersecting");
+      }
+    });
+  }, { root: document.getElementById("root") });
+  document.querySelectorAll("#root > div").forEach(element => {
+    io.observe(element);
+  });
+  runTestCycle(step0, "First rAF");
+}, "Explicit rtl root with overflow clipping");
+
+function step0() {
+  assert_true(
+    document.getElementById("target-start").classList.contains("intersecting"),
+    "Target at scroll start is intersecting");
+  assert_false(
+    document.getElementById("target-end").classList.contains("intersecting"),
+    "Target at scroll end is not intersecting");
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/same-document-no-root.html b/src/third_party/web_platform_tests/intersection-observer/same-document-no-root.html
new file mode 100644
index 0000000..63e9f86
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/same-document-no-root.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+
+<div class="spacer"></div>
+<div id="target"></div>
+<div class="spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "IntersectionObserver in a single document using the implicit root.");
+
+function step0() {
+  document.scrollingElement.scrollTop = 300;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = 300");
+  checkLastEntry(entries, 0, [8, 108, vh + 108, vh + 208, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+
+function step1() {
+  document.scrollingElement.scrollTop = 100;
+  runTestCycle(step2, "document.scrollingElement.scrollTop = 100");
+  checkLastEntry(entries, 1, [8, 108, vh - 192, vh - 92, 8, 108, vh - 192, vh - 92, 0, vw, 0, vh, true]);
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 0;
+  checkLastEntry(entries, 2, [8, 108, vh + 8, vh + 108, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/same-document-root.html b/src/third_party/web_platform_tests/intersection-observer/same-document-root.html
new file mode 100644
index 0000000..bfb9b72
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/same-document-root.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+#root {
+  display: inline-block;
+  overflow-y: scroll;
+  height: 200px;
+  border: 3px solid black;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+
+<div class="spacer"></div>
+<div id="root">
+  <div style="height: 300px;"></div>
+  <div id="target"></div>
+</div>
+<div class="spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var root, target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  root = document.getElementById("root");
+  assert_true(!!root, "root exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, { root: root });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF");
+}, "IntersectionObserver in a single document with explicit root.");
+
+function step0() {
+  document.scrollingElement.scrollTop = vh;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = window.innerHeight.");
+  checkLastEntry(entries, 0, [ 11, 111, vh + 411, vh + 511, 0, 0, 0, 0, 11, 111, vh + 111, vh + 311, false]);
+}
+
+function step1() {
+  root.scrollTop = 150;
+  runTestCycle(step2, "root.scrollTop = 150 with root scrolled into view.");
+  assert_equals(entries.length, 1, "No notifications after scrolling frame.");
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 0;
+  runTestCycle(step3, "document.scrollingElement.scrollTop = 0.");
+  checkLastEntry(entries, 1, [11, 111, 261, 361, 11, 111, 261, 311, 11, 111, 111, 311, true]);
+}
+
+function step3() {
+  root.scrollTop = 0;
+  runTestCycle(step4, "root.scrollTop = 0");
+  checkLastEntry(entries, 1);
+}
+
+function step4() {
+  root.scrollTop = 150;
+  runTestCycle(step5, "root.scrollTop = 150 with root scrolled out of view.");
+  checkLastEntry(entries, 2, [11, 111, vh + 411, vh + 511, 0, 0, 0, 0, 11, 111, vh + 111, vh + 311, false]);
+}
+
+// This tests that notifications are generated even when the root element is off screen.
+function step5() {
+  checkLastEntry(entries, 3, [11, 111, vh + 261, vh + 361, 11, 111, vh + 261, vh + 311, 11, 111, vh + 111, vh + 311, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/same-document-zero-size-target.html b/src/third_party/web_platform_tests/intersection-observer/same-document-zero-size-target.html
new file mode 100644
index 0000000..20bd11d
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/same-document-zero-size-target.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+#target {
+  width: 0px;
+  height: 0px;
+  background-color: green;
+}
+</style>
+
+<div class="spacer"></div>
+<div id="target"></div>
+<div class="spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "Target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF");
+}, "Observing a zero-area target.");
+
+function step0() {
+  document.scrollingElement.scrollTop = 300;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = 300");
+  checkLastEntry(entries, 0, [8, 8, vh + 108, vh + 108, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+
+function step1() {
+  document.scrollingElement.scrollTop = 100;
+  runTestCycle(step2, "document.scrollingElement.scrollTop = 100");
+  checkLastEntry(entries, 1, [8, 8, vh - 192, vh - 192, 8, 8, vh - 192, vh - 192, 0, vw, 0, vh, true]);
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 0;
+  checkLastEntry(entries, 2, [8, 8, vh + 8, vh + 8, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/same-origin-grand-child-iframe.sub.html b/src/third_party/web_platform_tests/intersection-observer/same-origin-grand-child-iframe.sub.html
new file mode 100644
index 0000000..57c0347
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/same-origin-grand-child-iframe.sub.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+<iframe id="iframe"></iframe>
+<script>
+promise_test(async t => {
+  iframe.src =
+    get_host_info().HTTP_NOTSAMESITE_ORIGIN + "/intersection-observer/resources/cross-origin-child-iframe.sub.html";
+
+  const rootBounds = await new Promise(resolve => {
+    window.addEventListener("message", event => resolve(event.data));
+  }, { once: true } );
+
+  assert_equals(rootBounds.left, 0);
+  assert_equals(rootBounds.top, 0);
+  assert_equals(rootBounds.right, document.documentElement.clientWidth);
+  assert_equals(rootBounds.bottom, document.documentElement.clientHeight);
+}, "rootBounds in a same-origin iframe in the case where there is a cross-origin "
++  "iframe in between the top document and the same origin iframe");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/shadow-content.html b/src/third_party/web_platform_tests/intersection-observer/shadow-content.html
new file mode 100644
index 0000000..ce9473c
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/shadow-content.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+</style>
+
+<div id="host"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  var shadowHost = document.getElementById("host");
+  assert_true(!!shadowHost, "Host exists");
+  var shadowRoot = shadowHost.attachShadow({ mode: "open" });
+  assert_true(!!shadowRoot, "Shadow root exists");
+  shadowRoot.innerHTML = "<div id='target' style='width: 100px; height: 100px; background-color: green;'></div>";
+  target = shadowRoot.getElementById("target");
+  assert_true(!!target, "target exists");
+
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF after creating shadow DOM.");
+}, "Observing a target inside shadow DOM.");
+
+function step0() {
+  checkLastEntry(entries, 0, [8, 108, 8, 108, 8, 108, 8, 108, 0, vw, 0, vh, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/target-in-different-window.html b/src/third_party/web_platform_tests/intersection-observer/target-in-different-window.html
new file mode 100644
index 0000000..645b7ec
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/target-in-different-window.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<script>
+var entries = [];
+var popup, target;
+
+function waitForPopupNotification(f) {
+  popup.requestAnimationFrame(function() {
+    popup.requestAnimationFrame(function() { popup.setTimeout(f); });
+  });
+}
+
+async_test((t) => {
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes);
+  });
+  popup = window.open();
+  t.add_cleanup(() => popup.close());
+  target = popup.document.createElement('div');
+  target.style.width = "100px";
+  target.style.height = "100px";
+  observer.observe(target);
+  waitForPopupNotification(t.step_func(() => {
+    assert_equals(entries.length, 1, "Initial notification for detached target.");
+    assert_equals(entries[0].isIntersecting, false, "not intersecting");
+    popup.document.body.appendChild(target);
+    waitForPopupNotification(t.step_func_done(() => {
+      assert_equals(entries.length, 2, "Notification after insertion into popup.");
+      assert_equals(entries[1].isIntersecting, true, "intersecting");
+    }));
+  }));
+}, "IntersectionObserver with target in a different window.");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/text-target.html b/src/third_party/web_platform_tests/intersection-observer/text-target.html
new file mode 100644
index 0000000..1abe535
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/text-target.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+</style>
+
+<div class="spacer"></div>
+<br id="target">
+<div class="spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var target;
+var tw, th;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  let target_rect = target.getBoundingClientRect();
+  tw = target_rect.width;
+  th = target_rect.height;
+  assert_true(!!target, "target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "IntersectionObserver observing a br element.");
+
+function step0() {
+  document.scrollingElement.scrollTop = 300;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = 300");
+  // The numbers in brackets are target client rect; intersection rect;
+  // and root bounds.
+  checkLastEntry(entries, 0, [8, 8 + tw, vh + 108, vh + 108 + th, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+
+function step1() {
+  document.scrollingElement.scrollTop = 100;
+  runTestCycle(step2, "document.scrollingElement.scrollTop = 100");
+  checkLastEntry(entries, 1, [8, 8 + tw, vh - 192, vh - 192 + th, 8, 8 + tw, vh - 192, vh - 192 + th, 0, vw, 0, vh, true]);
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 0;
+  checkLastEntry(entries, 2, [8, 8 + tw, vh + 8, vh + 8 + th, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/timestamp.html b/src/third_party/web_platform_tests/intersection-observer/timestamp.html
new file mode 100644
index 0000000..3f573bc
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/timestamp.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+
+</style>
+<div id="leading-space" class="spacer"></div>
+<div id="trailing-space" class="spacer"></div>
+
+<script>
+// Pick this number to be comfortably greater than the length of two frames at 60Hz.
+var timeSkew = 40;
+
+var topWindowEntries = [];
+var iframeWindowEntries = [];
+var targetIframe;
+var topWindowTimeBeforeNotification;
+var iframeWindowTimeBeforeNotification;
+
+async_test(function(t) {
+  t.step_timeout(function() {
+    targetIframe = document.createElement("iframe");
+    assert_true(!!targetIframe, "iframe exists");
+    targetIframe.src = "resources/timestamp-subframe.html";
+    var trailingSpace = document.getElementById("trailing-space");
+    assert_true(!!trailingSpace, "trailing-space exists");
+    trailingSpace.parentNode.insertBefore(targetIframe, trailingSpace);
+    targetIframe.onload = function() {
+      var target = targetIframe.contentDocument.getElementById("target");
+      var iframeScroller = targetIframe.contentDocument.scrollingElement;
+
+      // Observer created here, callback created in iframe context.  Timestamps should be
+      // from this window.
+      var observer = new IntersectionObserver(
+          targetIframe.contentDocument.createObserverCallback(topWindowEntries), {});
+      assert_true(!!observer, "Observer exists");
+      observer.observe(target);
+
+      // Callback created here, observer created in iframe.  Timestamps should be
+      // from iframe window.
+      observer = targetIframe.contentDocument.createObserver(function(newEntries) {
+        iframeWindowEntries = iframeWindowEntries.concat(newEntries);
+      });
+      observer.observe(target);
+      runTestCycle(step1, "First rAF after iframe is loaded.");
+      t.done();
+    };
+  }, timeSkew);
+}, "Check that timestamps correspond to the to execution context that created the observer.");
+
+function step1() {
+  document.scrollingElement.scrollTop = 200;
+  targetIframe.contentDocument.scrollingElement.scrollTop = 250;
+  topWindowTimeBeforeNotification = performance.now();
+  iframeWindowTimeBeforeNotification = targetIframe.contentWindow.performance.now();
+  runTestCycle(step2, "Generate notifications.");
+  assert_equals(topWindowEntries.length, 1, "One notification to top window observer.");
+  assert_equals(iframeWindowEntries.length, 1, "One notification to iframe observer.");
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 0;
+  var topWindowTimeAfterNotification = performance.now();
+  var iframeWindowTimeAfterNotification = targetIframe.contentWindow.performance.now();
+
+  assert_approx_equals(
+      topWindowEntries[1].time - topWindowTimeBeforeNotification,
+      iframeWindowEntries[1].time - iframeWindowTimeBeforeNotification,
+      // Since all intersections are computed in a tight loop between 2 frames,
+      // an epsilon of 16ms (the length of one frame at 60Hz) turned out to be
+      // reliable, even at slow frame rates.
+      16,
+      "Notification times are relative to the expected time origins");
+
+  assert_equals(topWindowEntries.length, 2, "Top window observer has two notifications.");
+  assert_between_inclusive(
+      topWindowEntries[1].time,
+      topWindowTimeBeforeNotification,
+      topWindowTimeAfterNotification,
+      "Notification to top window observer is within the expected range.");
+
+  assert_equals(iframeWindowEntries.length, 2, "Iframe observer has two notifications.");
+  assert_between_inclusive(
+      iframeWindowEntries[1].time,
+      iframeWindowTimeBeforeNotification,
+      iframeWindowTimeAfterNotification,
+      "Notification to iframe observer is within the expected range.");
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/unclipped-root.html b/src/third_party/web_platform_tests/intersection-observer/unclipped-root.html
new file mode 100644
index 0000000..a59105e
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/unclipped-root.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#root {
+  overflow: visible;
+  height: 200px;
+  width: 160px;
+  border: 7px solid black;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+
+<div id="root">
+  <div id="target" style="transform: translateY(300px)"></div>
+</div>
+
+<script>
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  var root = document.getElementById("root");
+  assert_true(!!root, "root exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {root: root});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "Test that border bounding box is used to calculate intersection with a non-scrolling root.");
+
+function step0() {
+  target.style.transform = "translateY(195px)";
+  runTestCycle(step1, "target.style.transform = 'translateY(195px)'");
+  checkLastEntry(entries, 0, [15, 115, 315, 415, 0, 0, 0, 0, 8, 182, 8, 222, false]);
+}
+
+function step1() {
+  target.style.transform = "";
+  checkLastEntry(entries, 1, [15, 115, 210, 310, 15, 115, 210, 222, 8, 182, 8, 222, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/animated-occlusion.html b/src/third_party/web_platform_tests/intersection-observer/v2/animated-occlusion.html
new file mode 100644
index 0000000..fa69733
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/animated-occlusion.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+@keyframes rotate {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(45deg);
+  }
+}
+#occluder {
+  will-change: transform;
+  width: 100px;
+  height: 100px;
+  background-color: blue;
+}
+</style>
+
+<div id="target"></div>
+<div id="occluder"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("occluder");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 in a single document using the implicit root, with an animated occluding element.", delay);
+
+function step0() {
+  occluder.style.animation = "rotate .1s linear";
+  step_timeout(() => {
+    runTestCycle(step1, "occluder.style.animation = 'rotate .1s linear'", delay);
+  }, 50);
+  checkLastEntry(entries, 0, [0, 100, 0, 100, 0, 100, 0, 100, 0, vw, 0, vh, true, true]);
+}
+
+function step1() {
+  checkLastEntry(entries, 1, [0, 100, 0, 100, 0, 100, 0, 100, 0, vw, 0, vh, true, false]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/blur-filter.html b/src/third_party/web_platform_tests/intersection-observer/v2/blur-filter.html
new file mode 100644
index 0000000..8cf6306
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/blur-filter.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+#occluder {
+  margin-top: 10px;
+  width: 100px;
+  height: 100px;
+  background-color: blue;
+  filter: blur(50px);
+}
+</style>
+
+<div id="target"></div>
+<div id="occluder"></div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("occluder");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 in a single document using the implicit root, with an occluding element.", delay);
+
+function step0() {
+  // Occluding elements with opacity=0 should not affect target visibility.
+  occluder.style.opacity = "0";
+  runTestCycle(step2, "occluder.style.opacity = 0", delay);
+
+  // First notification should report occlusion due to blur filter.
+  checkLastEntry(entries, 0, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, false]);
+}
+
+function step2() {
+  checkLastEntry(entries, 1, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/box-shadow.html b/src/third_party/web_platform_tests/intersection-observer/v2/box-shadow.html
new file mode 100644
index 0000000..765fa8b
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/box-shadow.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+iframe {
+  width: 100px;
+  height: 100px;
+  border: 0;
+}
+#box-shadow {
+  display: inline-block;
+  box-shadow: -50px -50px 0 50px rgba(255, 0, 0, 0.7);
+}
+</style>
+
+<iframe id=target srcdoc="<!DOCTYPE html><div>Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum.</div>"></iframe><div id=box-shadow></div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("box-shadow");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  let observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 observing an iframe element.", delay);
+
+function step0() {
+  occluder.style.boxShadow = "none";
+  runTestCycle(step1, 'occluder.style.boxShadow = "none"', delay);
+  assert_equals(entries.length, 1, "Initial notification.");
+  assert_equals(entries[0].isVisible, false, "Initially occluded.");
+}
+
+function step1() {
+  occluder.style.boxShadow = "";
+  runTestCycle(step2, 'occluder.style.boxShadow = ""', delay);
+  assert_equals(entries.length, 2, "Notification after removing box shadow.");
+  assert_equals(entries[1].isVisible, true, "Visible when box shadow removed.");
+}
+
+function step2() {
+  assert_equals(entries.length, 3, "Notification after re-adding box shadow.");
+  assert_equals(entries[2].isVisible, false, "Occluded when box shadow re-added.");
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/cross-origin-effects.sub.html b/src/third_party/web_platform_tests/intersection-observer/v2/cross-origin-effects.sub.html
new file mode 100644
index 0000000..5f328be
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/cross-origin-effects.sub.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+</style>
+
+<div id="container">
+  <iframe src="http://{{domains[www1]}}:{{ports[http][0]}}/intersection-observer/resources/v2-subframe.html"></iframe>
+</div>
+
+<script>
+async_test(function(t) {
+  let container = document.getElementById("container");
+  let iframe = document.querySelector("iframe");
+
+  function step0(event) {
+    assert_equals(event.data,"");
+  }
+
+  function step1(event) {
+    container.style.opacity = "0.99";
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([true]));
+  }
+
+  function step2(event) {
+    container.style.opacity = "";
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([false]));
+  }
+
+  function step3(event) {
+    container.style.transform = "skew(30deg)";
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([true]));
+  }
+
+  function step4(event) {
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([false]));
+  }
+
+  let steps = [step0, step1, step2, step3, step4];
+
+  window.addEventListener("message", event => {
+    if (steps.length) {
+      t.step(steps.shift(), t, event);
+      waitForFrame(t, () => {
+        iframe.contentWindow.postMessage("", "*")
+      });
+    } else {
+      t.done();
+    }
+  });
+
+}, "Intersection observer V2 test with visual effects on iframe.");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/cross-origin-occlusion.sub.html b/src/third_party/web_platform_tests/intersection-observer/v2/cross-origin-occlusion.sub.html
new file mode 100644
index 0000000..4c2f286
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/cross-origin-occlusion.sub.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+iframe {
+  width: 300px;
+  height: 150px;
+  border: none;
+}
+#occluder {
+  will-change: transform;
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+
+<iframe src="http://{{domains[www1]}}:{{ports[http][0]}}/intersection-observer/resources/v2-subframe.html"></iframe>
+<div id="occluder"></div>
+
+<script>
+async_test(function(t) {
+  let iframe = document.querySelector("iframe");
+  let occluder = document.getElementById("occluder");
+
+  function step0(event) {
+    assert_equals(event.data,"");
+  }
+
+  function step1(event) {
+    occluder.style.marginTop = "-150px";
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([true]));
+  }
+
+  function step2(event) {
+    occluder.style.marginTop = "";
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([false]));
+  }
+
+  function step3(event) {
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([true]));
+  }
+
+  let steps = [step0, step1, step2, step3];
+
+  window.addEventListener("message", event => {
+    if (steps.length) {
+      t.step(steps.shift(), t, event);
+      waitForFrame(t, () => {
+        iframe.contentWindow.postMessage("", "*");
+      });
+    } else {
+      t.done();
+    }
+  });
+
+}, "Intersection observer V2 test with occlusion of target in iframe.");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/delay-test.html b/src/third_party/web_platform_tests/intersection-observer/v2/delay-test.html
new file mode 100644
index 0000000..e3906ea
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/delay-test.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+#occluder {
+  width: 100px;
+  height: 100px;
+  background-color: blue;
+}
+</style>
+
+<div id="target"></div>
+<div id="occluder"></div>
+
+<script>
+async_test(t => {
+  let entries = [];
+  let delay = 100;
+  let target = document.getElementById("target");
+  let occluder = document.getElementById("occluder");
+
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  let observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  // The first notification should be sent without delay.
+  waitForNotification(t, t.step_func(step0));
+
+  function waitForDelay(timerExpiredBeforeLastFrame, nextStep) {
+    requestAnimationFrame(t.step_func(() => {
+      if (timerExpiredBeforeLastFrame) {
+        // New notifications should have been generated during the previous
+        // frame and delivered by now.
+        assert_equals(entries.length, 2);
+        assert_greater_than(entries[1].time - entries[0].time, delay);
+        assert_false(entries[1].isVisible);
+        nextStep();
+      } else {
+        // Observer may not have updated yet. Wait for next frame.
+        let timerExpired = performance.now() - entries[0].time >= delay;
+        waitForDelay(timerExpired, nextStep);
+      }
+    }));
+  }
+
+  function step0() {
+    assert_equals(entries.length, 1);
+    assert_true(entries[0].isVisible);
+    // This should trigger a notification on the next run.
+    occluder.style.marginTop = "-10px";
+    // Enter a rAF loop until the delay timer expires.
+    waitForDelay(false, step1);
+  }
+
+  function step1() {
+    occluder.style.marginTop = "10px";
+    // This style invalidation should cause a frame to run before the observer
+    // can generate a notification (due to delay parameter). Make sure the
+    // notification will still be generated even if we don't force more frames
+    // with a rAF loop.
+    t.step_timeout(() => {
+      assert_equals(entries.length, 3);
+      assert_true(entries[0].isVisible);
+      t.done();
+    }, 2 * delay);
+  }
+
+}, "'delay' parameter throttles frequency of notifications.");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/drop-shadow-filter-vertical-rl.html b/src/third_party/web_platform_tests/intersection-observer/v2/drop-shadow-filter-vertical-rl.html
new file mode 100644
index 0000000..fc5b145
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/drop-shadow-filter-vertical-rl.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 150px;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+  float: left;
+}
+#occluder {
+  float: left;
+  margin-left: 10px;
+  width: 100px;
+  height: 100px;
+  background-color: blue;
+  filter: drop-shadow(-50px 0);
+  writing-mode: vertical-rl;
+}
+</style>
+
+<div id="target"></div>
+<div id="occluder"></div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("occluder");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 in a single document using the implicit root, with an occluding element.", delay);
+
+function step0() {
+  // Occluding elements with opacity=0 should not affect target visibility.
+  occluder.style.opacity = "0";
+  runTestCycle(step2, "occluder.style.opacity = 0", delay);
+
+  // First notification should report occlusion due to drop shadow filter.
+  checkLastEntry(entries, 0, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, false]);
+}
+
+function step2() {
+  checkLastEntry(entries, 1, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/iframe-target.html b/src/third_party/web_platform_tests/intersection-observer/v2/iframe-target.html
new file mode 100644
index 0000000..53fbff8
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/iframe-target.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+iframe {
+  width: 150px;
+  height: 100px;
+  border: 0;
+}
+</style>
+
+<iframe srcdoc="<!DOCTYPE html><div>Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum.</div>"></iframe>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.querySelector("iframe");
+  assert_true(!!target, "target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 observing an iframe element.", delay);
+
+function step0() {
+  checkLastEntry(entries, 0, [0, 150, 0, 100, 0, 150, 0, 100, 0, 800, 0, 600, true, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/inline-occlusion.html b/src/third_party/web_platform_tests/intersection-observer/v2/inline-occlusion.html
new file mode 100644
index 0000000..e4b097e
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/inline-occlusion.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.testdiv {
+  font-size: 24px;
+}
+</style>
+
+<div class="testdiv">This is the <span id="target">target</span>.</div>
+<div class="testdiv" id="occluder">This is the occluder.</div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("occluder");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 in a single document using the implicit root, with an occluding element.", delay);
+
+function step0() {
+  occluder.style.marginTop = "-10px";
+  runTestCycle(step1, "occluder.style.marginTop = '-10px'", delay);
+  assert_equals(entries.length, 1);
+  assert_true(entries[0].isVisible);
+}
+
+function step1() {
+  // Occluding elements with opacity=0 should not affect target visibility.
+  occluder.style.opacity = "0";
+  runTestCycle(step2, "occluder.style.opacity = 0", delay);
+  assert_equals(entries.length, 2);
+  assert_false(entries[1].isVisible);
+}
+
+function step2() {
+  assert_equals(entries.length, 3);
+  assert_true(entries[2].isVisible);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/position-relative.html b/src/third_party/web_platform_tests/intersection-observer/v2/position-relative.html
new file mode 100644
index 0000000..4cdc429
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/position-relative.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.relpos {
+  position: relative;
+}
+</style>
+
+<div id="target" class="relpos">
+  <div class="relpos">
+    <img border="0" width="100" height="100" src=""/>
+  </div>
+</div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 observing a position:relative div containing a position:relative child");
+
+function step0() {
+  assert_equals(entries.length, 1, "First notification.");
+  assert_true(entries[0].isVisible, "Target is visible.");
+}
+</script>
+
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/scaled-target.html b/src/third_party/web_platform_tests/intersection-observer/v2/scaled-target.html
new file mode 100644
index 0000000..f48f079
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/scaled-target.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#iframe {
+  width: 100px;
+  height: 100px;
+  border: 0;
+  margin-bottom: 10px;
+}
+#occluder {
+  width: 100px;
+  height: 100px;
+  background-color: blue;
+  position: relative;
+}
+</style>
+
+<iframe id="iframe" src="../resources/scaled-target-subframe.html"></iframe>
+<div id="occluder"></div>
+
+<script>
+async_test(function(t) {
+  let iframe = document.getElementById("iframe");
+
+  function step0(event) {
+    assert_equals(event.data, "");
+  }
+
+  function step1(event) {
+    iframe.style.transform = "scale(2)";
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([true]));
+  }
+
+  function step2(event) {
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([false]));
+  }
+
+  let steps = [step0, step1, step2];
+
+  window.addEventListener("message", event => {
+    if (steps.length) {
+      t.step(steps.shift(), t, event);
+      waitForFrame(t, () => {
+        iframe.contentWindow.postMessage("", "*")
+      });
+    } else {
+      t.done();
+    }
+  });
+
+}, "IntersectionObserver V2 test with scale applied to target.");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/simple-effects.html b/src/third_party/web_platform_tests/intersection-observer/v2/simple-effects.html
new file mode 100644
index 0000000..baf3220
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/simple-effects.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+#effects {
+  opacity: 1;
+  filter: none;
+}
+</style>
+
+<div id="effects">
+  <div id="target"></div>
+</div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var effects;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  effects = document.getElementById("effects");
+  assert_true(!!target, "target exists");
+  assert_true(!!effects, "effects exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 in a single document using the implicit root, with a non-zero opacity ancestor.", delay);
+
+function step0() {
+  effects.style.opacity = "0.99";
+  runTestCycle(step1, "effects.style.opacity = 0.99", delay);
+  checkLastEntry(entries, 0, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, true]);
+}
+
+function step1() {
+  effects.style.opacity = "1";
+  runTestCycle(step2, "effects.style.opacity = 1", delay);
+  checkLastEntry(entries, 1, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, false]);
+}
+
+function step2() {
+  effects.style.filter = "grayscale(50%)";
+  runTestCycle(step3, "effects.style.filter = grayscale(50%)", delay);
+  checkLastEntry(entries, 2, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, true]);
+}
+
+function step3() {
+  checkLastEntry(entries, 3, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, false]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/simple-occlusion-svg-foreign-object.html b/src/third_party/web_platform_tests/intersection-observer/v2/simple-occlusion-svg-foreign-object.html
new file mode 100644
index 0000000..588ec2a
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/simple-occlusion-svg-foreign-object.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+#occluder {
+  width: 100px;
+  height: 100px;
+  background-color: blue;
+}
+</style>
+
+<div id="target"></div>
+<svg id="svg" style="display: block">
+  <foreignObject>
+    <div id="occluder"></div>
+  </foreignObject>
+</svg>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("occluder");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 in a single document using the implicit root, with an occluding element.", delay);
+
+function step0() {
+  svg.style.marginTop = "-10px";
+  runTestCycle(step1, "svg.style.marginTop = '-10px'", delay);
+  checkLastEntry(entries, 0, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, true]);
+}
+
+function step1() {
+  // Occluding elements with opacity=0 should not affect target visibility.
+  svg.style.opacity = "0";
+  runTestCycle(step2, "occluder.style.opacity = 0", delay);
+  checkLastEntry(entries, 1, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, false]);
+}
+
+function step2() {
+  checkLastEntry(entries, 2, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/simple-occlusion.html b/src/third_party/web_platform_tests/intersection-observer/v2/simple-occlusion.html
new file mode 100644
index 0000000..f3ce518
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/simple-occlusion.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+#occluder {
+  width: 100px;
+  height: 100px;
+  background-color: blue;
+}
+</style>
+
+<div id="target"></div>
+<div id="occluder"></div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("occluder");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 in a single document using the implicit root, with an occluding element.", delay);
+
+function step0() {
+  occluder.style.marginTop = "-10px";
+  runTestCycle(step1, "occluder.style.marginTop = '-10px'", delay);
+  checkLastEntry(entries, 0, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, true]);
+}
+
+function step1() {
+  // Occluding elements with opacity=0 should not affect target visibility.
+  occluder.style.opacity = "0";
+  runTestCycle(step2, "occluder.style.opacity = 0", delay);
+  checkLastEntry(entries, 1, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, false]);
+}
+
+function step2() {
+  checkLastEntry(entries, 2, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/text-editor-occlusion.html b/src/third_party/web_platform_tests/intersection-observer/v2/text-editor-occlusion.html
new file mode 100644
index 0000000..2edb7bb
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/text-editor-occlusion.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.testdiv {
+  font-size: 24px;
+}
+</style>
+
+<div class="testdiv">Target: <input id="target" type="text"></input></div>
+<div class="testdiv" id="occluder">This is the occluder.</div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("occluder");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 in a single document using the implicit root, with an occluding element.", delay);
+
+function step0() {
+  occluder.style.marginTop = "-10px";
+  runTestCycle(step1, "occluder.style.marginTop = '-10px'", delay);
+  assert_equals(entries.length, 1);
+  assert_true(entries[0].isVisible);
+}
+
+function step1() {
+  // Occluding elements with opacity=0 should not affect target visibility.
+  occluder.style.opacity = "0";
+  runTestCycle(step2, "occluder.style.opacity = 0", delay);
+  assert_equals(entries.length, 2);
+  assert_false(entries[1].isVisible);
+}
+
+function step2() {
+  assert_equals(entries.length, 3);
+  assert_true(entries[2].isVisible);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/text-shadow.html b/src/third_party/web_platform_tests/intersection-observer/v2/text-shadow.html
new file mode 100644
index 0000000..cdfc1a2
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/text-shadow.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+iframe {
+  width: 100px;
+  height: 100px;
+  border: 0;
+}
+#text-shadow {
+  display: inline-block;
+  font-size: 144px;
+  font-weight: 1000;
+  color: rgba(0, 0, 0, 0);
+  text-shadow: -100px 0 0 rgba(255, 0, 0, .7);
+}
+</style>
+
+<iframe id=target srcdoc="<!DOCTYPE html><div>Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum.</div>"></iframe><div id=text-shadow>O</div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("text-shadow");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  let observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 observing an iframe element.", delay);
+
+function step0() {
+  occluder.style.textShadow = "none";
+  runTestCycle(step1, 'occluder.style.textShadow = "none"', delay);
+  assert_equals(entries.length, 1, "Initial notification.");
+  assert_equals(entries[0].isVisible, false, "Initially occluded.");
+}
+
+function step1() {
+  occluder.style.textShadow = "";
+  runTestCycle(step2, 'occluder.style.textShadow = ""', delay);
+  assert_equals(entries.length, 2, "Notification after removing text shadow.");
+  assert_equals(entries[1].isVisible, true, "Visible when text shadow removed.");
+}
+
+function step2() {
+  assert_equals(entries.length, 3, "Notification after re-adding text shadow.");
+  assert_equals(entries[2].isVisible, false, "Occluded when text shadow re-added.");
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/zero-area-element-hidden.html b/src/third_party/web_platform_tests/intersection-observer/zero-area-element-hidden.html
new file mode 100644
index 0000000..be57ac6
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/zero-area-element-hidden.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  width: 0px;
+  height: 0px;
+  position: fixed;
+  top: -1000px;
+}
+</style>
+
+<div id='target'></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+
+runTestCycle(function() {
+  var target = document.getElementById('target');
+  assert_true(!!target, "target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "A zero-area hidden target should not be intersecting.");
+
+function step0() {
+  checkLastEntry(entries, 0, [8, 8, -1000, -1000, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/zero-area-element-visible.html b/src/third_party/web_platform_tests/intersection-observer/zero-area-element-visible.html
new file mode 100644
index 0000000..5431750
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/zero-area-element-visible.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  width: 0px;
+  height: 0px;
+}
+</style>
+
+<div id='target'></div>
+
+<script>
+var entries = [];
+
+runTestCycle(function() {
+  var target = document.getElementById('target');
+  assert_true(!!target, "target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF should generate a notification.");
+}, "Ensure that a zero-area target intersecting root generates a notification with intersectionRatio == 1");
+
+function step0() {
+  assert_equals(entries.length, 1, "One notification.");
+  assert_equals(entries[0].intersectionRatio, 1, "intersectionRatio == 1");
+}
+</script>