diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index 9c5e59ebb3b..b80bbf0f3bd 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -274,6 +274,14 @@ async function cfrgImportKey( 'DataError'); } + if (keyData.alg !== undefined && (name === 'Ed25519' || name === 'Ed448')) { + if (keyData.alg !== name && keyData.alg !== 'EdDSA') { + throw lazyDOMException( + 'JWK "alg" does not match the requested algorithm', + 'DataError'); + } + } + if (!isPublic && typeof keyData.x !== 'string') { throw lazyDOMException('Invalid JWK', 'DataError'); } diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 8cd27717532..004c5a59acd 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -475,6 +475,7 @@ async function exportKeyJWK(key) { // Fall through case 'Ed448': jwk.crv ||= key.algorithm.name; + jwk.alg = key.algorithm.name; return jwk; case 'AES-CTR': // Fall through diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index 1632cc71364..58673011a73 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -31,7 +31,7 @@ Last update: - user-timing: https://github.com/web-platform-tests/wpt/tree/5ae85bf826/user-timing - wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/cde25e7e3c/wasm/jsapi - wasm/webapi: https://github.com/web-platform-tests/wpt/tree/fd1b23eeaa/wasm/webapi -- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/b81831169b/WebCryptoAPI +- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/edd42c005c/WebCryptoAPI - webidl/ecmascript-binding/es-exceptions: https://github.com/web-platform-tests/wpt/tree/a370aad338/webidl/ecmascript-binding/es-exceptions - webmessaging/broadcastchannel: https://github.com/web-platform-tests/wpt/tree/e97fac4791/webmessaging/broadcastchannel diff --git a/test/fixtures/wpt/WebCryptoAPI/crypto_key_cached_slots.https.any.js b/test/fixtures/wpt/WebCryptoAPI/crypto_key_cached_slots.https.any.js new file mode 100644 index 00000000000..f573fac1c96 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/crypto_key_cached_slots.https.any.js @@ -0,0 +1,44 @@ +// META: title=WebCryptoAPI: CryptoKey cached ECMAScript objects + +// https://w3c.github.io/webcrypto/#dom-cryptokey-algorithm +// https://github.com/servo/servo/issues/33908 + +promise_test(function() { + return self.crypto.subtle.generateKey( + { + name: "AES-CTR", + length: 256, + }, + true, + ["encrypt"], + ).then( + function(key) { + let a = key.algorithm; + let b = key.algorithm; + assert_true(a === b); + }, + function(err) { + assert_unreached("generateKey threw an unexpected error: " + err.toString()); + } + ); +}, "CryptoKey.algorithm getter returns cached object"); + +promise_test(function() { + return self.crypto.subtle.generateKey( + { + name: "AES-CTR", + length: 256, + }, + true, + ["encrypt"], + ).then( + function(key) { + let a = key.usages; + let b = key.usages; + assert_true(a === b); + }, + function(err) { + assert_unreached("generateKey threw an unexpected error: " + err.toString()); + } + ); +}, "CryptoKey.usages getter returns cached object"); diff --git a/test/fixtures/wpt/WebCryptoAPI/cryptokey_algorithm_returns_cached_object.https.any.js b/test/fixtures/wpt/WebCryptoAPI/cryptokey_algorithm_returns_cached_object.https.any.js deleted file mode 100644 index b2d73fbab78..00000000000 --- a/test/fixtures/wpt/WebCryptoAPI/cryptokey_algorithm_returns_cached_object.https.any.js +++ /dev/null @@ -1,24 +0,0 @@ -// META: title=WebCryptoAPI: CryptoKey.algorithm getter returns cached object - -// https://w3c.github.io/webcrypto/#dom-cryptokey-algorithm -// https://github.com/servo/servo/issues/33908 - -promise_test(function() { - return self.crypto.subtle.generateKey( - { - name: "AES-CTR", - length: 256, - }, - true, - ["encrypt"], - ).then( - function(key) { - let a = key.algorithm; - let b = key.algorithm; - assert_true(a === b); - }, - function(err) { - assert_unreached("generateKey threw an unexpected error: " + err.toString()); - } - ); -}, "CryptoKey.algorithm getter returns cached object"); \ No newline at end of file diff --git a/test/fixtures/wpt/WebCryptoAPI/digest/digest.https.any.js b/test/fixtures/wpt/WebCryptoAPI/digest/digest.https.any.js index 379d9311f30..3b0972b1f2b 100644 --- a/test/fixtures/wpt/WebCryptoAPI/digest/digest.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/digest/digest.https.any.js @@ -118,6 +118,20 @@ }); }); + // Call digest() with empty algorithm object + Object.keys(sourceData).forEach(function(size) { + promise_test(function(test) { + var promise = subtle.digest({}, sourceData[size]) + .then(function(result) { + assert_unreached("digest() with missing algorithm name should have thrown a TypeError"); + }, function(err) { + assert_equals(err.name, "TypeError", "Missing algorithm name should cause TypeError") + }); + + return promise; + }, "empty algorithm object with " + size); + }); + done(); diff --git a/test/fixtures/wpt/WebCryptoAPI/generateKey/failures.js b/test/fixtures/wpt/WebCryptoAPI/generateKey/failures.js index e0f0279a69b..deaac636a99 100644 --- a/test/fixtures/wpt/WebCryptoAPI/generateKey/failures.js +++ b/test/fixtures/wpt/WebCryptoAPI/generateKey/failures.js @@ -166,6 +166,14 @@ function run_test(algorithmNames) { }); }); + // Empty algorithm should fail with TypeError + 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({}, extractable, usages, "TypeError", "Empty algorithm"); + }); + }); + // Algorithms normalize okay, but usages bad (though not empty). // It shouldn't matter what other extractable is. Should fail diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/ec_importKey_failures_fixtures.js b/test/fixtures/wpt/WebCryptoAPI/import_export/ec_importKey_failures_fixtures.js index 796db364c2e..a2d25e816cb 100644 --- a/test/fixtures/wpt/WebCryptoAPI/import_export/ec_importKey_failures_fixtures.js +++ b/test/fixtures/wpt/WebCryptoAPI/import_export/ec_importKey_failures_fixtures.js @@ -19,6 +19,14 @@ function getMismatchedJWKKeyData(algorithm) { return []; } +function getMismatchedKtyField(algorithm) { + return mismatchedKtyField[algorithm.name]; +} + +function getMismatchedCrvField(algorithm) { + return mismatchedCrvField[algorithm.name]; +} + var validKeyData = { "P-521": [ { @@ -201,3 +209,17 @@ var missingJWKFieldKeyData = { } ] }; + +// The 'kty' field doesn't match the key algorithm. +var mismatchedKtyField = { + "P-521": "OKP", + "P-256": "OKP", + "P-384": "OKP", +} + +// The 'kty' field doesn't match the key algorithm. +var mismatchedCrvField = { + "P-521": "P-256", + "P-256": "P-384", + "P-384": "P-521", +} diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/importKey_failures.js b/test/fixtures/wpt/WebCryptoAPI/import_export/importKey_failures.js index bba48401e61..453461a8771 100644 --- a/test/fixtures/wpt/WebCryptoAPI/import_export/importKey_failures.js +++ b/test/fixtures/wpt/WebCryptoAPI/import_export/importKey_failures.js @@ -192,4 +192,79 @@ function run_test(algorithmNames) { }); }); }); + + // Missing mandatory "name" field on algorithm + testVectors.forEach(function(vector) { + var name = vector.name; + // We just need *some* valid keydata, so pick the first available algorithm. + var algorithm = allAlgorithmSpecifiersFor(name)[0]; + getValidKeyData(algorithm).forEach(function(test) { + validUsages(vector, test.format, test.data).forEach(function(usages) { + [true, false].forEach(function(extractable) { + testError(test.format, {}, test.data, name, usages, extractable, "TypeError", "Missing algorithm name"); + }); + }); + }); + }); + + // The 'kty' field is not correct. + testVectors.forEach(function(vector) { + var name = vector.name; + allAlgorithmSpecifiersFor(name).forEach(function(algorithm) { + getValidKeyData(algorithm).forEach(function(test) { + if (test.format === "jwk") { + var data = {crv: test.data.crv, kty: test.data.kty, d: test.data.d, x: test.data.x, d: test.data.d}; + data.kty = getMismatchedKtyField(algorithm); + var usages = validUsages(vector, 'jwk', test.data); + testError('jwk', algorithm, data, name, usages, true, "DataError", "Invalid 'kty' field"); + } + }); + }); + }); + + // The 'ext' field is not correct. + testVectors.forEach(function(vector) { + var name = vector.name; + allAlgorithmSpecifiersFor(name).forEach(function(algorithm) { + getValidKeyData(algorithm).forEach(function(test) { + if (test.format === "jwk") { + var data = {crv: test.data.crv, kty: test.data.kty, d: test.data.d, x: test.data.x, d: test.data.d}; + data.ext = false; + var usages = validUsages(vector, 'jwk', test.data); + testError('jwk', algorithm, data, name, usages, true, "DataError", "Import from a non-extractable"); + } + }); + }); + }); + + // The 'use' field is incorrect. + testVectors.forEach(function(vector) { + var name = vector.name; + allAlgorithmSpecifiersFor(name).forEach(function(algorithm) { + getValidKeyData(algorithm).forEach(function(test) { + if (test.format === "jwk") { + var data = {crv: test.data.crv, kty: test.data.kty, d: test.data.d, x: test.data.x, d: test.data.d}; + data.use = "invalid"; + var usages = validUsages(vector, 'jwk', test.data); + if (usages.length !== 0) + testError('jwk', algorithm, data, name, usages, true, "DataError", "Invalid 'use' field"); + } + }); + }); + }); + + // The 'crv' field is incorrect. + testVectors.forEach(function(vector) { + var name = vector.name; + allAlgorithmSpecifiersFor(name).forEach(function(algorithm) { + getValidKeyData(algorithm).forEach(function(test) { + if (test.format === "jwk") { + var data = {crv: test.data.crv, kty: test.data.kty, d: test.data.d, x: test.data.x, d: test.data.d}; + data.crv = getMismatchedCrvField(algorithm) + var usages = validUsages(vector, 'jwk', test.data); + testError('jwk', algorithm, data, name, usages, true, "DataError", "Invalid 'crv' field"); + } + }); + }); + }); } diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey.js b/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey.js index 0e6a016fe20..8627f85e466 100644 --- a/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey.js +++ b/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey.js @@ -11,7 +11,7 @@ function runTests(algorithmName) { ['spki', 'jwk', 'raw'].forEach(function(format) { if (format === "jwk") { // Not all fields used for public keys testFormat(format, algorithm, jwkData, algorithmName, usages, extractable); - // Test for https://github.com/WICG/webcrypto-secure-curves/pull/24 + // Test for https://github.com/w3c/webcrypto/pull/401 if (extractable) { testJwkAlgBehaviours(algorithm, jwkData.jwk, algorithmName, usages); } @@ -27,7 +27,7 @@ function runTests(algorithmName) { ['pkcs8', 'jwk'].forEach(function(format) { testFormat(format, algorithm, data, algorithmName, usages, extractable); - // Test for https://github.com/WICG/webcrypto-secure-curves/pull/24 + // Test for https://github.com/w3c/webcrypto/pull/401 if (format === "jwk" && extractable) { testJwkAlgBehaviours(algorithm, data.jwk, algorithmName, usages); } @@ -67,27 +67,34 @@ function testFormat(format, algorithm, keyData, keySize, usages, extractable) { }); } -// Test importKey/exportKey "alg" behaviours, alg is ignored upon import and alg is missing for Ed25519 and Ed448 JWK export -// https://github.com/WICG/webcrypto-secure-curves/pull/24 +// Test importKey/exportKey "alg" behaviours (https://github.com/w3c/webcrypto/pull/401) +// - alg is ignored for ECDH import +// - TODO: alg is checked to be the algorithm.name or EdDSA for Ed25519 and Ed448 import +// - alg is missing for ECDH export +// - alg is the algorithm name for Ed25519 and Ed448 export function testJwkAlgBehaviours(algorithm, keyData, crv, usages) { [algorithm, algorithm.name].forEach((alg) => { - promise_test(function(test) { - return subtle.importKey('jwk', { ...keyData, alg: 'this is ignored' }, alg, true, usages). - then(function(key) { - assert_equals(key.constructor, CryptoKey, "Imported a CryptoKey object"); + (crv.startsWith('Ed') ? [algorithm.name, 'EdDSA'] : ['this is ignored']).forEach((jwkAlg) => { + promise_test(function(test) { + return subtle.importKey('jwk', { ...keyData, alg: jwkAlg }, alg, true, usages). + then(function(key) { + assert_equals(key.constructor, CryptoKey, "Imported a CryptoKey object"); - return subtle.exportKey('jwk', key). - then(function(result) { - assert_equals(Object.keys(result).length, keyData.d ? 6 : 5, "Correct number of JWK members"); - assert_equals(result.alg, undefined, 'No JWK "alg" member is present'); - assert_true(equalJwk(keyData, result), "Round trip works"); - }, function(err) { + return subtle.exportKey('jwk', key). + then(function(result) { + let expectedKeys = crv.startsWith('Ed') ? 6 : 5 + if (keyData.d) expectedKeys++ + assert_equals(Object.keys(result).length, expectedKeys, "Correct number of JWK members"); + assert_equals(result.alg, crv.startsWith('Ed') ? algorithm.name : undefined, 'Expected JWK "alg" member'); + 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()); - }); - }, function(err) { - assert_unreached("Threw an unexpected error: " + err.toString()); - }); - }, "Good parameters with ignored JWK alg: " + crv.toString() + " " + parameterString('jwk', keyData, alg, true, usages)); + }); + }, 'Good parameters with JWK alg' + (crv.startsWith('Ed') ? ` ${jwkAlg}: ` : ': ') + crv.toString() + " " + parameterString('jwk', keyData, alg, true, usages, jwkAlg)); + }); }); } diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_failures_fixtures.js b/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_failures_fixtures.js index 88f552291cd..9bedddc5c59 100644 --- a/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_failures_fixtures.js +++ b/test/fixtures/wpt/WebCryptoAPI/import_export/okp_importKey_failures_fixtures.js @@ -17,6 +17,14 @@ function getMismatchedJWKKeyData(algorithm) { return mismatchedJWKKeyData[algorithm.name]; } +function getMismatchedKtyField(algorithm) { + return mismatchedKtyField[algorithm.name]; +} + +function getMismatchedCrvField(algorithm) { + return mismatchedCrvField[algorithm.name]; +} + var validKeyData = { "Ed25519": [ { @@ -412,3 +420,19 @@ var mismatchedJWKKeyData = { }, ], } + +// The 'kty' field doesn't match the key algorithm. +var mismatchedKtyField = { + "Ed25519": "EC", + "X25519": "EC", + "Ed448": "EC", + "X448": "EC", +} + +// The 'kty' field doesn't match the key algorithm. +var mismatchedCrvField = { + "Ed25519": "X25519", + "X25519": "Ed448", + "Ed448": "X25519", + "X448": "Ed25519", +} diff --git a/test/fixtures/wpt/WebCryptoAPI/wrapKey_unwrapKey/wrapKey_unwrapKey.https.any.js b/test/fixtures/wpt/WebCryptoAPI/wrapKey_unwrapKey/wrapKey_unwrapKey.https.any.js index edb67d9e30f..9dd837b3bf6 100644 --- a/test/fixtures/wpt/WebCryptoAPI/wrapKey_unwrapKey/wrapKey_unwrapKey.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/wrapKey_unwrapKey/wrapKey_unwrapKey.https.any.js @@ -1,238 +1,256 @@ // META: title=WebCryptoAPI: wrapKey() and unwrapKey() // META: timeout=long // META: script=../util/helpers.js +// META: script=wrapKey_unwrapKey_vectors.js // Tests for wrapKey and unwrapKey round tripping var subtle = self.crypto.subtle; - var wrappers = []; // Things we wrap (and upwrap) keys with - var keys = []; // Things to wrap and unwrap + var wrappers = {}; // Things we wrap (and upwrap) keys with + var keys = {}; // Things to wrap and unwrap - // Generate all the keys needed, then iterate over all combinations + // 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 wrappingKeysParameters = [ + { + name: "RSA-OAEP", + importParameters: {name: "RSA-OAEP", hash: "SHA-256"}, + wrapParameters: {name: "RSA-OAEP", label: new Uint8Array(8)} + }, + { + name: "AES-CTR", + importParameters: {name: "AES-CTR", length: 128}, + wrapParameters: {name: "AES-CTR", counter: new Uint8Array(16), length: 64} + }, + { + name: "AES-CBC", + importParameters: {name: "AES-CBC", length: 128}, + wrapParameters: {name: "AES-CBC", iv: new Uint8Array(16)} + }, + { + name: "AES-GCM", + importParameters: {name: "AES-GCM", length: 128}, + wrapParameters: {name: "AES-GCM", iv: new Uint8Array(16), additionalData: new Uint8Array(16), tagLength: 128} + }, + { + name: "AES-KW", + importParameters: {name: "AES-KW", length: 128}, + wrapParameters: {name: "AES-KW"} + } + ]; + + var keysToWrapParameters = [ + {algorithm: {name: "RSASSA-PKCS1-v1_5", hash: "SHA-256"}, privateUsages: ["sign"], publicUsages: ["verify"]}, + {algorithm: {name: "RSA-PSS", hash: "SHA-256"}, privateUsages: ["sign"], publicUsages: ["verify"]}, + {algorithm: {name: "RSA-OAEP", 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: "Ed25519" }, privateUsages: ["sign"], publicUsages: ["verify"]}, + {algorithm: {name: "Ed448" }, privateUsages: ["sign"], publicUsages: ["verify"]}, + {algorithm: {name: "X25519" }, privateUsages: ["deriveBits"], publicUsages: []}, + {algorithm: {name: "X448" }, 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"]} + ]; + + // Import all the keys needed, then iterate over all combinations // to test wrapping and unwrapping. promise_test(function() { - return Promise.all([generateWrappingKeys(), generateKeysToWrap()]) + return Promise.all([importWrappingKeys(), importKeysToWrap()]) .then(function(results) { - var promises = []; - wrappers.forEach(function(wrapper) { - keys.forEach(function(key) { - promises.push(testWrapping(wrapper, key)); - }) + wrappingKeysParameters.filter((param) => Object.keys(wrappers).includes(param.name)).forEach(function(wrapperParam) { + var wrapper = wrappers[wrapperParam.name]; + keysToWrapParameters.filter((param) => Object.keys(keys).includes(param.algorithm.name)).forEach(function(toWrapParam) { + var keyData = keys[toWrapParam.algorithm.name]; + ["raw", "spki", "pkcs8"].filter((fmt) => Object.keys(keyData).includes(fmt)).forEach(function(keyDataFormat) { + var toWrap = keyData[keyDataFormat]; + [keyDataFormat, "jwk"].forEach(function(format) { + if (wrappingIsPossible(toWrap.originalExport[format], wrapper.parameters.name)) { + testWrapping(wrapper, toWrap, format); + if (canCompareNonExtractableKeys(toWrap.key)) { + testWrappingNonExtractable(wrapper, toWrap, format); + if (format === "jwk") { + testWrappingNonExtractableAsExtractable(wrapper, toWrap); + } + } + } + }); + }); + }); }); - return Promise.allSettled(promises); + return Promise.resolve("setup done"); + }, function(err) { + return Promise.reject("setup failed: " + err.name + ': "' + err.message + '"'); }); }, "setup"); - 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: 128} - }, - { - name: "AES-KW", - generateParameters: {name: "AES-KW", length: 128}, - wrapParameters: {name: "AES-KW"} - } - ]; - + function importWrappingKeys() { // Using allSettled to skip unsupported test cases. - return Promise.allSettled(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; - }) - })); + var promises = []; + wrappingKeysParameters.forEach(function(params) { + if (params.name === "RSA-OAEP") { // we have a key pair, not just a key + var algorithm = {name: "RSA-OAEP", hash: "SHA-256"}; + wrappers[params.name] = {wrappingKey: undefined, unwrappingKey: undefined, parameters: params}; + promises.push(subtle.importKey("spki", wrappingKeyData["RSA"].spki, algorithm, true, ["wrapKey"]) + .then(function(key) { + wrappers["RSA-OAEP"].wrappingKey = key; + })); + promises.push(subtle.importKey("pkcs8", wrappingKeyData["RSA"].pkcs8, algorithm, true, ["unwrapKey"]) + .then(function(key) { + wrappers["RSA-OAEP"].unwrappingKey = key; + })); + } else { + var algorithm = {name: params.name}; + promises.push(subtle.importKey("raw", wrappingKeyData["SYMMETRIC"].raw, algorithm, true, ["wrapKey", "unwrapKey"]) + .then(function(key) { + wrappers[params.name] = {wrappingKey: key, unwrappingKey: key, parameters: params}; + })); + } + }); + // Using allSettled to skip unsupported test cases. + return Promise.allSettled(promises); } + async function importAndExport(format, keyData, algorithm, keyUsages, keyType) { + var importedKey; + try { + importedKey = await subtle.importKey(format, keyData, algorithm, true, keyUsages); + keys[algorithm.name][format] = { name: algorithm.name + " " + keyType, algorithm: algorithm, usages: keyUsages, key: importedKey, originalExport: {} }; + } catch (err) { + delete keys[algorithm.name][format]; + throw("Error importing " + algorithm.name + " " + keyType + " key in '" + format + "' -" + err.name + ': "' + err.message + '"'); + }; + try { + var exportedKey = await subtle.exportKey(format, importedKey); + keys[algorithm.name][format].originalExport[format] = exportedKey; + } catch (err) { + delete keys[algorithm.name][format]; + throw("Error exporting " + algorithm.name + " '" + format + "' key -" + err.name + ': "' + err.message + '"'); + }; + try { + var jwkExportedKey = await subtle.exportKey("jwk", importedKey); + keys[algorithm.name][format].originalExport["jwk"] = jwkExportedKey; + } catch (err) { + delete keys[algorithm.name][format]; + throw("Error exporting " + algorithm.name + " '" + format + "' key to 'jwk' -" + err.name + ': "' + err.message + '"'); + }; + } - 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: "Ed25519" }, privateUsages: ["sign"], publicUsages: ["verify"]}, - {algorithm: {name: "Ed448" }, privateUsages: ["sign"], publicUsages: ["verify"]}, - {algorithm: {name: "X25519" }, privateUsages: ["deriveBits"], publicUsages: []}, - {algorithm: {name: "X448" }, 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"]} - ]; - - // Using allSettled to skip unsupported test cases. - return Promise.allSettled(parameters.map(function(params) { - var usages; - if ("usages" in params) { - usages = params.usages; + function importKeysToWrap() { + var promises = []; + keysToWrapParameters.forEach(function(params) { + if ("publicUsages" in params) { + keys[params.algorithm.name] = {}; + var keyData = toWrapKeyDataFromAlg(params.algorithm.name); + promises.push(importAndExport("spki", keyData.spki, params.algorithm, params.publicUsages, "public key ")); + promises.push(importAndExport("pkcs8", keyData.pkcs8, params.algorithm, params.privateUsages, "private key ")); } else { - usages = params.publicUsages.concat(params.privateUsages); + keys[params.algorithm.name] = {}; + promises.push(importAndExport("raw", toWrapKeyData["SYMMETRIC"].raw, params.algorithm, params.usages, "")); } - - 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; - }); - })); + }); + // Using allSettled to skip unsupported test cases. + return Promise.allSettled(promises); } // 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; - const isPossible = wrappingIsPossible(originalExport, wrapper.parameters.name); - promise_test(function(test) { - if (!isPossible) { - return Promise.resolve().then(() => { - assert_false(false, "Wrapping is not possible"); - }) - } - 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_goodCryptoKey(unwrappedResult, toWrap.algorithm, true, toWrap.usages, toWrap.key.type); - 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){ - if (!isPossible) { - return Promise.resolve().then(() => { - assert_false(false, "Wrapping is not possible"); - }) - } - 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_goodCryptoKey(unwrappedResult, toWrap.algorithm, false, toWrap.usages, toWrap.key.type); - 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){ - if (!isPossible) { - return Promise.resolve().then(() => { - assert_false(false, "Wrapping is not possible"); - }) - } - 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); - } + function testWrapping(wrapper, toWrap, fmt) { + promise_test(async() => { + try { + var wrappedResult = await subtle.wrapKey(fmt, toWrap.key, wrapper.wrappingKey, wrapper.parameters.wrapParameters); + var unwrappedResult = await subtle.unwrapKey(fmt, wrappedResult, wrapper.unwrappingKey, wrapper.parameters.wrapParameters, toWrap.algorithm, true, toWrap.usages); + assert_goodCryptoKey(unwrappedResult, toWrap.algorithm, true, toWrap.usages, toWrap.key.type); + var roundTripExport = await subtle.exportKey(fmt, unwrappedResult); + assert_true(equalExport(toWrap.originalExport[fmt], roundTripExport), "Post-wrap export matches original export"); + } catch (err) { + if (err instanceof AssertionError) { + throw 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); + } + + function testWrappingNonExtractable(wrapper, toWrap, fmt) { + promise_test(async() => { + try { + var wrappedResult = await subtle.wrapKey(fmt, toWrap.key, wrapper.wrappingKey, wrapper.parameters.wrapParameters); + var unwrappedResult = await subtle.unwrapKey(fmt, wrappedResult, wrapper.unwrappingKey, wrapper.parameters.wrapParameters, toWrap.algorithm, false, toWrap.usages); + assert_goodCryptoKey(unwrappedResult, toWrap.algorithm, false, toWrap.usages, toWrap.key.type); + var result = await equalKeys(toWrap.key, unwrappedResult); + assert_true(result, "Unwrapped key matches original"); + } catch (err) { + if (err instanceof AssertionError) { + throw 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); + } + + function testWrappingNonExtractableAsExtractable(wrapper, toWrap) { + promise_test(async() => { + var wrappedKey; + try { + var wrappedResult = await wrapAsNonExtractableJwk(toWrap.key,wrapper); + wrappedKey = wrappedResult; + var unwrappedResult = await subtle.unwrapKey("jwk", wrappedKey, wrapper.unwrappingKey, wrapper.parameters.wrapParameters, toWrap.algorithm, false, toWrap.usages); + assert_false(unwrappedResult.extractable, "Unwrapped key is non-extractable"); + var result = await equalKeys(toWrap.key,unwrappedResult); + assert_true(result, "Unwrapped key matches original"); + } catch (err) { + if (err instanceof AssertionError) { + throw err; + } + assert_unreached("Round trip for non-extractable key threw an error - " + err.name + ': "' + err.message + '"'); + }; + try { + var unwrappedResult = await subtle.unwrapKey("jwk", wrappedKey, wrapper.unwrappingKey, wrapper.parameters.wrapParameters, toWrap.algorithm, true, toWrap.usages); + assert_unreached("Unwrapping a non-extractable JWK as extractable should fail"); + } catch (err) { + if (err instanceof AssertionError) { + throw 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){ + async 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)); - } - }); + var jwkWrappingKey = await subtle.exportKey("jwk",wrappingKey); + // Update the key generation parameters to work as key import parameters + var params = Object.create(wrapper.parameters.importParameters); + 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"]; + var importedWrappingKey = await subtle.importKey("jwk", jwkWrappingKey, params, true, ["encrypt"]); + encryptKey = importedWrappingKey; + var exportedKey = await subtle.exportKey("jwk",key); + exportedKey.ext = false; + var jwk = JSON.stringify(exportedKey) + var result; + if (wrappingKey.algorithm.name === "AES-KW") { + result = await aeskw(encryptKey, str2ab(jwk.slice(0,-1) + " ".repeat(jwk.length%8 ? 8-jwk.length%8 : 0) + "}")); + } else { + result = await subtle.encrypt(wrapper.parameters.wrapParameters,encryptKey,str2ab(jwk)); + } + return result; } @@ -365,9 +383,9 @@ } // Compare two keys by using them (works for non-extractable keys) - function equalKeys(expected, got){ + async function equalKeys(expected, got){ if ( expected.algorithm.name !== got.algorithm.name ) { - return Promise.resolve(false); + return false; } var cryptParams, signParams, wrapParams, deriveParams; @@ -419,75 +437,60 @@ } 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); - }); + var jwkExpectedKey = await subtle.exportKey("jwk", expected); + if (expected.algorithm.name === "RSA-OAEP") { + ["d","p","q","dp","dq","qi","oth"].forEach(function(field){ delete jwkExpectedKey[field]; }); + } + jwkExpectedKey.key_ops = ["encrypt"]; + var expectedEncryptKey = await subtle.importKey("jwk", jwkExpectedKey, expected.algorithm, true, ["encrypt"]); + var encryptedData = await subtle.encrypt(cryptParams, expectedEncryptKey, new Uint8Array(32)); + var decryptedData = await subtle.decrypt(cryptParams, got, encryptedData); + 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" || expected.algorithm.name.startsWith("Ed")) { - 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)); - }); + var jwkExpectedKey = await subtle.exportKey("jwk",expected); + 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" || expected.algorithm.name.startsWith("Ed")) { + delete jwkExpectedKey["d"]; + } + jwkExpectedKey.key_ops = ["verify"]; + var expectedVerifyKey = await subtle.importKey("jwk", jwkExpectedKey, expected.algorithm, true, ["verify"]); + verifyKey = expectedVerifyKey; + var signature = await subtle.sign(signParams, got, new Uint8Array(32)); + var result = await subtle.verify(signParams, verifyKey, signature, new Uint8Array(32)); + return result; } 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]); - }); + var key = await subtle.importKey("raw",new Uint8Array(16), "AES-CBC", true, ["encrypt"]) + aKeyToWrap = key; + var wrapResult = await subtle.wrapKey("raw", aKeyToWrap, expected, wrapParams); + wrappedWithExpected = Array.from((new Uint8Array(wrapResult)).values()); + wrapResult = await subtle.wrapKey("raw", aKeyToWrap, got, wrapParams); + var wrappedWithGot = Array.from((new Uint8Array(wrapResult)).values()); + return wrappedWithGot.every((x,i) => x === wrappedWithExpected[i]); } else if (deriveParams) { var expectedDerivedBits; - return subtle.generateKey(expected.algorithm, true, ['deriveBits']).then(({ publicKey }) => { - deriveParams.public = publicKey; - 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]); - }); + var key = await subtle.generateKey(expected.algorithm, true, ['deriveBits']); + deriveParams.public = key.publicKey; + var result = await subtle.deriveBits(deriveParams, expected, 128); + expectedDerivedBits = Array.from((new Uint8Array(result)).values()); + result = await subtle.deriveBits(deriveParams, got, 128); + 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);}); + async function aes(k, p) { + const ciphertext = await subtle.encrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, k, p); + return ciphertext.slice(0, 16); } // AES Key Wrap - function aeskw(key, data) { + async 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"); } @@ -501,7 +504,7 @@ R.push(new Uint8Array(data.slice(i,i+8))); } - function aeskw_step(j, i, final, B) { + async 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)); @@ -516,18 +519,16 @@ } } - var p = new Promise(function(resolve){ - A.set(R[0],8); - resolve(aes(key,A)); - }); + A.set(R[0], 8); + let B = await aes(key, A); for(var j=0;j<6;++j) { for(var i=0;i