Tests: Re-import WebCryptoAPI tests

Some test have changed name and some utilities have now expanded to
accommodate new algorithms.
This commit is contained in:
Tete17
2025-11-25 15:50:23 +01:00
committed by Jelle Raaijmakers
parent 8a79792a58
commit aa44d254a4
Notes: github-actions[bot] 2025-12-10 20:30:16 +00:00
37 changed files with 1325 additions and 280 deletions

View File

@@ -13,4 +13,4 @@ self.GLOBAL = {
<script src="cfrg_curves_bits_fixtures.js"></script>
<script src="cfrg_curves_bits.js"></script>
<div id=log></div>
<script src="../../WebCryptoAPI/derive_bits_keys/cfrg_curves_bits_curve448.https.any.js"></script>
<script src="../../WebCryptoAPI/derive_bits_keys/cfrg_curves_bits_curve448.tentative.https.any.js"></script>

View File

@@ -284,7 +284,7 @@ function run_test() {
resolve(vector);
});
} else {
return subtle.importKey("raw", vector.keyBuffer, {name: vector.algorithm.name}, false, usages)
return subtle.importKey(vector.algorithm.name.toUpperCase() === "AES-OCB" ? "raw-secret" : "raw", vector.keyBuffer, {name: vector.algorithm.name}, false, usages)
.then(function(key) {
vector.key = key;
return vector;

View File

@@ -25,6 +25,8 @@ function run_test(algorithmNames) {
{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-OCB", resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
{name: "ChaCha20-Poly1305", 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"]},
@@ -34,8 +36,16 @@ function run_test(algorithmNames) {
{name: "ECDH", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]},
{name: "Ed25519", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
{name: "Ed448", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
{name: "ML-DSA-44", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
{name: "ML-DSA-65", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
{name: "ML-DSA-87", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
{name: "ML-KEM-512", resultType: "CryptoKeyPair", usages: ["decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"], mandatoryUsages: ["decapsulateBits", "decapsulateKey"]},
{name: "ML-KEM-768", resultType: "CryptoKeyPair", usages: ["decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"], mandatoryUsages: ["decapsulateBits", "decapsulateKey"]},
{name: "ML-KEM-1024", resultType: "CryptoKeyPair", usages: ["decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"], mandatoryUsages: ["decapsulateBits", "decapsulateKey"]},
{name: "X25519", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]},
{name: "X448", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]},
{name: "KMAC128", resultType: CryptoKey, usages: ["sign", "verify"], mandatoryUsages: []},
{name: "KMAC256", resultType: CryptoKey, usages: ["sign", "verify"], mandatoryUsages: []},
];
var testVectors = [];

View File

@@ -14,4 +14,4 @@ self.GLOBAL = {
<script src="../util/helpers.js"></script>
<script src="failures.js"></script>
<div id=log></div>
<script src="../../WebCryptoAPI/generateKey/failures_X448.https.any.js"></script>
<script src="../../WebCryptoAPI/generateKey/failures_Ed448.tentative.https.any.js"></script>

View File

@@ -14,4 +14,4 @@ self.GLOBAL = {
<script src="../util/helpers.js"></script>
<script src="failures.js"></script>
<div id=log></div>
<script src="../../WebCryptoAPI/generateKey/failures_Ed448.https.any.js"></script>
<script src="../../WebCryptoAPI/generateKey/failures_X448.tentative.https.any.js"></script>

View File

@@ -21,6 +21,8 @@ function run_test(algorithmNames, slowTest) {
{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-OCB", resultType: CryptoKey, usages: ["encrypt", "decrypt", "wrapKey", "unwrapKey"], mandatoryUsages: []},
{name: "ChaCha20-Poly1305", 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"]},
@@ -30,8 +32,16 @@ function run_test(algorithmNames, slowTest) {
{name: "ECDH", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]},
{name: "Ed25519", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
{name: "Ed448", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
{name: "ML-DSA-44", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
{name: "ML-DSA-65", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
{name: "ML-DSA-87", resultType: "CryptoKeyPair", usages: ["sign", "verify"], mandatoryUsages: ["sign"]},
{name: "ML-KEM-512", resultType: "CryptoKeyPair", usages: ["decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"], mandatoryUsages: ["decapsulateBits", "decapsulateKey"]},
{name: "ML-KEM-768", resultType: "CryptoKeyPair", usages: ["decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"], mandatoryUsages: ["decapsulateBits", "decapsulateKey"]},
{name: "ML-KEM-1024", resultType: "CryptoKeyPair", usages: ["decapsulateBits", "decapsulateKey", "encapsulateBits", "encapsulateKey"], mandatoryUsages: ["decapsulateBits", "decapsulateKey"]},
{name: "X25519", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]},
{name: "X448", resultType: "CryptoKeyPair", usages: ["deriveKey", "deriveBits"], mandatoryUsages: ["deriveKey", "deriveBits"]},
{name: "KMAC128", resultType: CryptoKey, usages: ["sign", "verify"], mandatoryUsages: []},
{name: "KMAC256", resultType: CryptoKey, usages: ["sign", "verify"], mandatoryUsages: []},
];
var testVectors = [];
@@ -74,20 +84,60 @@ function run_test(algorithmNames, slowTest) {
assert_unreached("generateKey threw an unexpected error: " + err.toString());
})
.then(async function (result) {
if (resultType === "CryptoKeyPair") {
await Promise.all([
subtle.exportKey('jwk', result.publicKey),
// TODO: remove this block to enable ML-KEM JWK when its definition is done in IETF JOSE WG
if (result.publicKey?.algorithm.name.startsWith('ML-KEM')) {
const promises = [
subtle.exportKey('spki', result.publicKey),
result.publicKey.algorithm.name.startsWith('RSA') ? undefined : subtle.exportKey('raw', result.publicKey),
...(extractable ? [
subtle.exportKey('jwk', result.privateKey),
subtle.exportKey('pkcs8', result.privateKey),
] : [])
]);
extractable ? subtle.exportKey('pkcs8', result.privateKey) : undefined,
subtle.exportKey('raw-public', result.publicKey),
];
if (extractable)
promises.push(subtle.exportKey('raw-seed', result.privateKey));
} else if (resultType === "CryptoKeyPair") {
const promises = [
subtle.exportKey('jwk', result.publicKey),
extractable ? subtle.exportKey('jwk', result.privateKey) : undefined,
subtle.exportKey('spki', result.publicKey),
extractable ? subtle.exportKey('pkcs8', result.privateKey) : undefined,
];
switch (result.publicKey.algorithm.name.substring(0, 2)) {
case 'ML':
promises.push(subtle.exportKey('raw-public', result.publicKey));
if (extractable)
promises.push(subtle.exportKey('raw-seed', result.privateKey));
break;
case 'SL':
promises.push(subtle.exportKey('raw-public', result.publicKey));
if (extractable)
promises.push(subtle.exportKey('raw-private', result.privateKey));
break;
case 'EC':
case 'Ed':
case 'X2':
case 'X4':
promises.push(subtle.exportKey('raw', result.publicKey));
break;
case 'RS':
break;
default:
throw new Error('not implemented');
}
const [jwkPub, jwkPriv] = await Promise.all(promises);
if (extractable) {
// Test that the JWK public key is a superset of the JWK private key.
for (const [prop, value] of Object.entries(jwkPub)) {
if (prop !== 'key_ops') {
assert_equals(value, jwkPriv[prop], `Property ${prop} is equal in public and private JWK`);
}
}
}
} else {
if (extractable) {
await Promise.all([
subtle.exportKey('raw', result),
subtle.exportKey(/cha|ocb|kmac/i.test(result.algorithm.name) ? 'raw-secret' : 'raw', result),
subtle.exportKey('jwk', result),
]);
}

View File

@@ -15,4 +15,4 @@ self.GLOBAL = {
<script src="../../common/subset-tests.js"></script>
<script src="successes.js"></script>
<div id=log></div>
<script src="../../WebCryptoAPI/generateKey/successes_Ed448.https.any.js"></script>
<script src="../../WebCryptoAPI/generateKey/successes_Ed448.tentative.https.any.js"></script>

View File

@@ -15,4 +15,4 @@ self.GLOBAL = {
<script src="../../common/subset-tests.js"></script>
<script src="successes.js"></script>
<div id=log></div>
<script src="../../WebCryptoAPI/generateKey/successes_X448.https.any.js"></script>
<script src="../../WebCryptoAPI/generateKey/successes_X448.tentative.https.any.js"></script>

View File

@@ -20,11 +20,11 @@ function getMismatchedJWKKeyData(algorithm) {
}
function getMismatchedKtyField(algorithm) {
return mismatchedKtyField[algorithm.name];
return mismatchedKtyField[algorithm.namedCurve];
}
function getMismatchedCrvField(algorithm) {
return mismatchedCrvField[algorithm.name];
return mismatchedCrvField[algorithm.namedCurve];
}
var validKeyData = {

View File

@@ -19,6 +19,12 @@ function run_test(algorithmNames) {
var allTestVectors = [ // Parameters that should work for importKey / exportKey
{name: "Ed25519", privateUsages: ["sign"], publicUsages: ["verify"]},
{name: "ML-DSA-44", privateUsages: ["sign"], publicUsages: ["verify"]},
{name: "ML-DSA-65", privateUsages: ["sign"], publicUsages: ["verify"]},
{name: "ML-DSA-87", privateUsages: ["sign"], publicUsages: ["verify"]},
{name: "ML-KEM-512", privateUsages: ["decapsulateKey", "decapsulateBits"], publicUsages: ["encapsulateKey", "encapsulateBits"]},
{name: "ML-KEM-768", privateUsages: ["decapsulateKey", "decapsulateBits"], publicUsages: ["encapsulateKey", "encapsulateBits"]},
{name: "ML-KEM-1024", privateUsages: ["decapsulateKey", "decapsulateBits"], publicUsages: ["encapsulateKey", "encapsulateBits"]},
{name: "Ed448", privateUsages: ["sign"], publicUsages: ["verify"]},
{name: "ECDSA", privateUsages: ["sign"], publicUsages: ["verify"]},
{name: "X25519", privateUsages: ["deriveKey", "deriveBits"], publicUsages: []},
@@ -43,7 +49,7 @@ function run_test(algorithmNames) {
var jwk_label = "";
if (format === "jwk")
jwk_label = data.d === undefined ? " (public) " : "(private)";
jwk_label = isPublicKey(data) ? " (public) " : "(private)";
var result = "(" +
objectToString(format) + jwk_label + ", " +
@@ -101,18 +107,22 @@ function run_test(algorithmNames) {
}
function validUsages(usages, format, data) {
if (format === 'spki' || format === 'raw') return usages.publicUsages
if (format === 'pkcs8') return usages.privateUsages
if (format === 'spki' || format === 'raw' || format === 'raw-public') return usages.publicUsages
if (format === 'pkcs8' || format === 'raw-private' || format === 'raw-seed') return usages.privateUsages
if (format === 'jwk') {
if (data === undefined)
return [];
return data.d === undefined ? usages.publicUsages : usages.privateUsages;
return isPublicKey(data) ? usages.publicUsages : usages.privateUsages;
}
return [];
}
function isPublicKey(data) {
return data.d === undefined && data.priv === undefined;
}
function isPrivateKey(data) {
return data.d !== undefined;
return !isPublicKey(data);
}
// Now test for properly handling errors
@@ -243,7 +253,7 @@ function run_test(algorithmNames) {
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};
var data = {crv: test.data.crv, kty: test.data.kty, d: test.data.d, x: test.data.x, y: test.data.y};
data.use = "invalid";
var usages = validUsages(vector, 'jwk', test.data);
if (usages.length !== 0)

View File

@@ -15,4 +15,4 @@ self.GLOBAL = {
<script src="okp_importKey_fixtures.js"></script>
<script src="okp_importKey.js"></script>
<div id=log></div>
<script src="../../WebCryptoAPI/import_export/okp_importKey_X448.https.any.js"></script>
<script src="../../WebCryptoAPI/import_export/okp_importKey_X448.tentative.https.any.js"></script>

View File

@@ -15,4 +15,4 @@ self.GLOBAL = {
<script src="okp_importKey_failures_fixtures.js"></script>
<script src="importKey_failures.js"></script>
<div id=log></div>
<script src="../../WebCryptoAPI/import_export/okp_importKey_failures_X448.https.any.js"></script>
<script src="../../WebCryptoAPI/import_export/okp_importKey_failures_X448.tentative.https.any.js"></script>

View File

@@ -2,27 +2,27 @@
// helper functions that generate all possible test parameters for
// different situations.
function getValidKeyData(algorithm) {
return validKeyData[algorithm.name];
return validKeyData[algorithm.name || algorithm];
}
function getBadKeyLengthData(algorithm) {
return badKeyLengthData[algorithm.name];
return badKeyLengthData[algorithm.name || algorithm];
}
function getMissingJWKFieldKeyData(algorithm) {
return missingJWKFieldKeyData[algorithm.name];
return missingJWKFieldKeyData[algorithm.name || algorithm];
}
function getMismatchedJWKKeyData(algorithm) {
return mismatchedJWKKeyData[algorithm.name];
return mismatchedJWKKeyData[algorithm.name || algorithm];
}
function getMismatchedKtyField(algorithm) {
return mismatchedKtyField[algorithm.name];
return mismatchedKtyField[algorithm.name || algorithm];
}
function getMismatchedCrvField(algorithm) {
return mismatchedCrvField[algorithm.name];
return mismatchedCrvField[algorithm.name || algorithm];
}
var validKeyData = {
@@ -432,7 +432,7 @@ var mismatchedKtyField = {
// The 'kty' field doesn't match the key algorithm.
var mismatchedCrvField = {
"Ed25519": "X25519",
"X25519": "Ed448",
"Ed448": "X25519",
"X448": "Ed25519",
"X25519": "Ed25519",
"Ed448": "X448",
"X448": "Ed448",
}

View File

@@ -12,5 +12,6 @@ self.GLOBAL = {
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../util/helpers.js"></script>
<script src="symmetric_importKey.js"></script>
<div id=log></div>
<script src="../../WebCryptoAPI/import_export/symmetric_importKey.https.any.js"></script>

View File

@@ -1,222 +1,12 @@
// META: title=WebCryptoAPI: importKey() for symmetric keys
// META: timeout=long
// META: script=../util/helpers.js
// META: script=symmetric_importKey.js
// Test importKey and exportKey for non-PKC algorithms. Only "happy paths" are
// currently tested - those where the operation should succeed.
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) {
// 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);
}
// Generate all combinations of valid usages for testing
allValidUsages(vector.legalUsages).forEach(function(usages) {
testFormat(format, algorithm, data, keyData.length * 8, usages, extractable);
});
testEmptyUsages(format, algorithm, data, keyData.length * 8, extractable);
});
});
});
});
function hasLength(algorithm) {
return algorithm.name === 'HMAC' || algorithm.name.startsWith('AES');
}
// 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");
assert_goodCryptoKey(key, hasLength(key.algorithm) ? { length: keySize, ...algorithm } : algorithm, extractable, usages, 'secret');
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));
}
// Test importKey with a given key format and other parameters but with empty usages.
// Should fail with SyntaxError
function testEmptyUsages(format, algorithm, keyData, keySize, extractable) {
const usages = [];
promise_test(function(test) {
return subtle.importKey(format, keyData, algorithm, extractable, usages).
then(function(key) {
assert_unreached("importKey succeeded but should have failed with SyntaxError");
}, function(err) {
assert_equals(err.name, "SyntaxError", "Should throw correct error, not " + err.name + ": " + err.message);
});
}, "Empty Usages: " + 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, "");
}
// 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(", ") + "}";
}
runTests("AES-CTR");
runTests("AES-CBC");
runTests("AES-GCM");
runTests("AES-KW");
runTests("HMAC");
runTests("HKDF");
runTests("PBKDF2");

View File

@@ -0,0 +1,231 @@
// Test importKey and exportKey for non-PKC algorithms. Only "happy paths" are
// currently tested - those where the operation should succeed.
function runTests(algorithmName) {
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: [true, false], formats: ["raw", "jwk"]},
{name: "HMAC", hash: "SHA-256", legalUsages: ["sign", "verify"], extractable: [true, false], formats: ["raw", "jwk"]},
{name: "HMAC", hash: "SHA-384", legalUsages: ["sign", "verify"], extractable: [true, false], formats: ["raw", "jwk"]},
{name: "HMAC", hash: "SHA-512", legalUsages: ["sign", "verify"], extractable: [true, false], formats: ["raw", "jwk"]},
{name: "HKDF", legalUsages: ["deriveBits", "deriveKey"], extractable: [false], formats: ["raw"]},
{name: "PBKDF2", legalUsages: ["deriveBits", "deriveKey"], extractable: [false], formats: ["raw"]},
{name: "Argon2i", legalUsages: ["deriveBits", "deriveKey"], extractable: [false], formats: ["raw-secret"]},
{name: "Argon2d", legalUsages: ["deriveBits", "deriveKey"], extractable: [false], formats: ["raw-secret"]},
{name: "Argon2id", legalUsages: ["deriveBits", "deriveKey"], extractable: [false], formats: ["raw-secret"]},
{name: "AES-OCB", legalUsages: ["encrypt", "decrypt"], extractable: [true, false], formats: ["raw-secret", "jwk"]},
{name: "ChaCha20-Poly1305", legalUsages: ["encrypt", "decrypt"], extractable: [true, false], formats: ["raw-secret", "jwk"]},
{name: "KMAC128", legalUsages: ["sign", "verify"], extractable: [true, false], formats: ["raw-secret", "jwk"]},
{name: "KMAC256", legalUsages: ["sign", "verify"], extractable: [true, false], formats: ["raw-secret", "jwk"]},
];
// TESTS ARE HERE:
// Test every test vector, along with all available key data
testVectors.filter(({ name }) => name === algorithmName).forEach(function(vector) {
var algorithm = {name: vector.name};
if ("hash" in vector) {
algorithm.hash = vector.hash;
}
rawKeyData.forEach(function(keyData) {
if (vector.name === 'ChaCha20-Poly1305' && keyData.byteLength !== 32) return;
// 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);
}
// Generate all combinations of valid usages for testing
allValidUsages(vector.legalUsages).forEach(function(usages) {
testFormat(format, algorithm, data, keyData.length * 8, usages, extractable);
});
testEmptyUsages(format, algorithm, data, keyData.length * 8, extractable);
});
});
});
});
function hasLength(algorithm) {
return algorithm.name === 'HMAC' || algorithm.name.startsWith('AES') || algorithm.name.startsWith('KMAC');
}
// 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");
assert_goodCryptoKey(key, hasLength(key.algorithm) ? { length: keySize, ...algorithm } : algorithm, extractable, usages, 'secret');
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));
}
// Test importKey with a given key format and other parameters but with empty usages.
// Should fail with SyntaxError
function testEmptyUsages(format, algorithm, keyData, keySize, extractable) {
const usages = [];
promise_test(function(test) {
return subtle.importKey(format, keyData, algorithm, extractable, usages).
then(function(key) {
assert_unreached("importKey succeeded but should have failed with SyntaxError");
}, function(err) {
assert_equals(err.name, "SyntaxError", "Should throw correct error, not " + err.name + ": " + err.message);
});
}, "Empty Usages: " + 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);
} else if (algorithm.name.startsWith("KMAC")) {
result.alg = "K" + algorithm.name.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, "");
}
// 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(", ") + "}";
}
}

View File

@@ -14,4 +14,4 @@ self.GLOBAL = {
<script src="eddsa_vectors.js"></script>
<script src="eddsa.js"></script>
<div id=log></div>
<script src="../../WebCryptoAPI/sign_verify/eddsa_curve448.https.any.js"></script>
<script src="../../WebCryptoAPI/sign_verify/eddsa_curve448.tentative.https.any.js"></script>

View File

@@ -24,7 +24,20 @@ var registeredAlgorithmNames = [
"Ed25519",
"Ed448",
"X25519",
"X448"
"X448",
"ML-DSA-44",
"ML-DSA-65",
"ML-DSA-87",
"ML-KEM-512",
"ML-KEM-768",
"ML-KEM-1024",
"ChaCha20-Poly1305",
"Argon2i",
"Argon2d",
"Argon2id",
"AES-OCB",
"KMAC128",
"KMAC256",
];
@@ -93,6 +106,10 @@ function objectToString(obj) {
// 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) {
if (typeof algorithm === "string") {
algorithm = { name: algorithm };
}
var correctUsages = [];
var registeredAlgorithmName;
@@ -120,6 +137,15 @@ function assert_goodCryptoKey(key, algorithm, extractable, usages, kind) {
default:
assert_unreached("Unrecognized hash");
}
} else if (key.algorithm.name.toUpperCase().startsWith("KMAC") && algorithm.length === undefined) {
switch (key.algorithm.name.toUpperCase()) {
case 'KMAC128':
assert_equals(key.algorithm.length, 128, "Correct length");
break;
case 'KMAC256':
assert_equals(key.algorithm.length, 256, "Correct length");
break;
}
} else {
assert_equals(key.algorithm.length, algorithm.length, "Correct length");
}
@@ -135,13 +161,13 @@ function assert_goodCryptoKey(key, algorithm, extractable, usages, kind) {
// 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) {
["encrypt", "verify", "wrapKey", "encapsulateBits", "encapsulateKey"].forEach(function(usage) {
if (usages.includes(usage)) {
correctUsages.push(usage);
}
});
} else if (key.type === "private") {
["decrypt", "sign", "unwrapKey", "deriveKey", "deriveBits"].forEach(function(usage) {
["decrypt", "sign", "unwrapKey", "deriveKey", "deriveBits", "decapsulateBits", "decapsulateKey"].forEach(function(usage) {
if (usages.includes(usage)) {
correctUsages.push(usage);
}
@@ -202,7 +228,16 @@ function allAlgorithmSpecifiersFor(algorithmName) {
curves.forEach(function(curveName) {
results.push({name: algorithmName, namedCurve: curveName});
});
} else if (algorithmName.toUpperCase().substring(0, 1) === "X" || algorithmName.toUpperCase().substring(0, 2) === "ED") {
} else if (algorithmName.toUpperCase().startsWith("KMAC")) {
[
{length: 128},
{length: 160},
{length: 256},
].forEach(function(hashAlgorithm) {
results.push({name: algorithmName, ...hashAlgorithm});
});
} else {
results.push(algorithmName);
results.push({ name: algorithmName });
}