diff options
Diffstat (limited to 'testing/web-platform/tests/WebCryptoAPI/wrapKey_unwrapKey')
-rw-r--r-- | testing/web-platform/tests/WebCryptoAPI/wrapKey_unwrapKey/wrapKey_unwrapKey.https.any.js | 535 |
1 files changed, 535 insertions, 0 deletions
diff --git a/testing/web-platform/tests/WebCryptoAPI/wrapKey_unwrapKey/wrapKey_unwrapKey.https.any.js b/testing/web-platform/tests/WebCryptoAPI/wrapKey_unwrapKey/wrapKey_unwrapKey.https.any.js new file mode 100644 index 0000000000..edb67d9e30 --- /dev/null +++ b/testing/web-platform/tests/WebCryptoAPI/wrapKey_unwrapKey/wrapKey_unwrapKey.https.any.js @@ -0,0 +1,535 @@ +// META: title=WebCryptoAPI: wrapKey() and unwrapKey() +// META: timeout=long +// META: script=../util/helpers.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 + + // Generate all the keys needed, then iterate over all combinations + // to test wrapping and unwrapping. + promise_test(function() { + return Promise.all([generateWrappingKeys(), generateKeysToWrap()]) + .then(function(results) { + var promises = []; + wrappers.forEach(function(wrapper) { + keys.forEach(function(key) { + promises.push(testWrapping(wrapper, key)); + }) + }); + return Promise.allSettled(promises); + }); + }, "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"} + } + ]; + + // 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; + }) + })); + } + + + 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; + } 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; + }); + })); + } + + // 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); + } + } + }); + })); + } + + // 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 "Ed25519" : + signParams = {name: "Ed25519"}; + break; + case "Ed448" : + signParams = {name: "Ed448"}; + break; + case "X25519" : + deriveParams = {name: "X25519"}; + break; + case "X448" : + deriveParams = {name: "X448"}; + break; + case "HMAC" : + signParams = {name: "HMAC"}; + break; + case "AES-KW" : + wrapParams = {name: "AES-KW"}; + break; + case "ECDH" : + deriveParams = {name: "ECDH"}; + 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" || 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)); + }); + } 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 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]); + }); + } + } + + // 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)); } + |