Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test has a WPT meta file that expects 60 subtest issues.
- This WPT test may be referenced by the following Test IDs:
- /WebCryptoAPI/encap_decap/encap_decap_keys.tentative.https.any.html - WPT Dashboard Interop Dashboard
- /WebCryptoAPI/encap_decap/encap_decap_keys.tentative.https.any.worker.html - WPT Dashboard Interop Dashboard
// META: title=WebCryptoAPI: ML-KEM encapsulateKey() and decapsulateKey() tests
// META: script=ml_kem_vectors.js
// META: script=../util/helpers.js
// META: timeout=long
function define_key_tests() {
var subtle = self.crypto.subtle;
var variants = ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024'];
// Test various 256-bit shared key algorithms
var sharedKeyConfigs = [
{
algorithm: { name: 'AES-GCM', length: 256 },
usages: ['encrypt', 'decrypt'],
description: 'AES-GCM-256',
},
{
algorithm: { name: 'AES-CBC', length: 256 },
usages: ['encrypt', 'decrypt'],
description: 'AES-CBC-256',
},
{
algorithm: { name: 'AES-CTR', length: 256 },
usages: ['encrypt', 'decrypt'],
description: 'AES-CTR-256',
},
{
algorithm: { name: 'AES-KW', length: 256 },
usages: ['wrapKey', 'unwrapKey'],
description: 'AES-KW-256',
},
{
algorithm: { name: 'HMAC', hash: 'SHA-256' },
usages: ['sign', 'verify'],
description: 'HMAC-SHA-256',
},
];
variants.forEach(function (algorithmName) {
sharedKeyConfigs.forEach(function (config) {
// Test encapsulateKey operation
promise_test(async function (test) {
// Generate a key pair for testing
var keyPair = await subtle.generateKey({ name: algorithmName }, false, [
'encapsulateKey',
'decapsulateKey',
]);
// Test encapsulateKey
var encapsulatedKey = await subtle.encapsulateKey(
{ name: algorithmName },
keyPair.publicKey,
config.algorithm,
true,
config.usages
);
assert_true(
encapsulatedKey instanceof Object,
'encapsulateKey should return an object'
);
assert_true(
encapsulatedKey.hasOwnProperty('sharedKey'),
'Result should have sharedKey property'
);
assert_true(
encapsulatedKey.hasOwnProperty('ciphertext'),
'Result should have ciphertext property'
);
assert_true(
encapsulatedKey.sharedKey instanceof CryptoKey,
'sharedKey should be a CryptoKey'
);
assert_true(
encapsulatedKey.ciphertext instanceof ArrayBuffer,
'ciphertext should be ArrayBuffer'
);
// Verify the shared key properties
assert_equals(
encapsulatedKey.sharedKey.type,
'secret',
'Shared key should be secret type'
);
assert_equals(
encapsulatedKey.sharedKey.algorithm.name,
config.algorithm.name,
'Shared key algorithm should match'
);
assert_true(
encapsulatedKey.sharedKey.extractable,
'Shared key should be extractable as specified'
);
assert_array_equals(
encapsulatedKey.sharedKey.usages,
config.usages,
'Shared key should have correct usages'
);
// Verify algorithm-specific properties
if (config.algorithm.length) {
assert_equals(
encapsulatedKey.sharedKey.algorithm.length,
config.algorithm.length,
'Key length should be 256'
);
}
if (config.algorithm.hash) {
assert_equals(
encapsulatedKey.sharedKey.algorithm.hash.name,
config.algorithm.hash,
'Hash algorithm should match'
);
}
// Verify ciphertext length based on algorithm variant
var expectedCiphertextLength;
switch (algorithmName) {
case 'ML-KEM-512':
expectedCiphertextLength = 768;
break;
case 'ML-KEM-768':
expectedCiphertextLength = 1088;
break;
case 'ML-KEM-1024':
expectedCiphertextLength = 1568;
break;
}
assert_equals(
encapsulatedKey.ciphertext.byteLength,
expectedCiphertextLength,
'Ciphertext should be ' +
expectedCiphertextLength +
' bytes for ' +
algorithmName
);
}, algorithmName + ' encapsulateKey with ' + config.description);
// Test decapsulateKey operation
promise_test(async function (test) {
// Generate a key pair for testing
var keyPair = await subtle.generateKey({ name: algorithmName }, false, [
'encapsulateKey',
'decapsulateKey',
]);
// First encapsulate to get ciphertext
var encapsulatedKey = await subtle.encapsulateKey(
{ name: algorithmName },
keyPair.publicKey,
config.algorithm,
true,
config.usages
);
// Then decapsulate using the private key
var decapsulatedKey = await subtle.decapsulateKey(
{ name: algorithmName },
keyPair.privateKey,
encapsulatedKey.ciphertext,
config.algorithm,
true,
config.usages
);
assert_true(
decapsulatedKey instanceof CryptoKey,
'decapsulateKey should return a CryptoKey'
);
assert_equals(
decapsulatedKey.type,
'secret',
'Decapsulated key should be secret type'
);
assert_equals(
decapsulatedKey.algorithm.name,
config.algorithm.name,
'Decapsulated key algorithm should match'
);
assert_true(
decapsulatedKey.extractable,
'Decapsulated key should be extractable as specified'
);
assert_array_equals(
decapsulatedKey.usages,
config.usages,
'Decapsulated key should have correct usages'
);
// Extract both keys and verify they are identical
var originalKeyMaterial = await subtle.exportKey(
'raw',
encapsulatedKey.sharedKey
);
var decapsulatedKeyMaterial = await subtle.exportKey(
'raw',
decapsulatedKey
);
assert_true(
equalBuffers(originalKeyMaterial, decapsulatedKeyMaterial),
'Decapsulated key material should match original'
);
// Verify the key material is 32 bytes (256 bits)
assert_equals(
originalKeyMaterial.byteLength,
32,
'Shared key material should be 32 bytes'
);
}, algorithmName + ' decapsulateKey with ' + config.description);
// Test round-trip compatibility
promise_test(async function (test) {
var keyPair = await subtle.generateKey({ name: algorithmName }, false, [
'encapsulateKey',
'decapsulateKey',
]);
var encapsulatedKey = await subtle.encapsulateKey(
{ name: algorithmName },
keyPair.publicKey,
config.algorithm,
true,
config.usages
);
var decapsulatedKey = await subtle.decapsulateKey(
{ name: algorithmName },
keyPair.privateKey,
encapsulatedKey.ciphertext,
config.algorithm,
true,
config.usages
);
// Verify keys have the same material
var originalKeyMaterial = await subtle.exportKey(
'raw',
encapsulatedKey.sharedKey
);
var decapsulatedKeyMaterial = await subtle.exportKey(
'raw',
decapsulatedKey
);
assert_true(
equalBuffers(originalKeyMaterial, decapsulatedKeyMaterial),
'Encapsulated and decapsulated keys should have the same material'
);
// Test that the derived keys can actually be used for their intended purpose
if (
config.algorithm.name.startsWith('AES') &&
config.usages.includes('encrypt')
) {
await testAESOperation(
encapsulatedKey.sharedKey,
decapsulatedKey,
config.algorithm
);
} else if (config.algorithm.name === 'HMAC') {
await testHMACOperation(encapsulatedKey.sharedKey, decapsulatedKey);
}
}, algorithmName +
' encapsulateKey/decapsulateKey round-trip with ' +
config.description);
});
// Test vector-based decapsulation for each shared key config
sharedKeyConfigs.forEach(function (config) {
promise_test(async function (test) {
var vectors = ml_kem_vectors[algorithmName];
// Import the private key from the vector's privateSeed
var privateKey = await subtle.importKey(
'raw-seed',
vectors.privateSeed,
{ name: algorithmName },
false,
['decapsulateKey']
);
// Decapsulate the sample ciphertext from the vectors to get a shared key
var decapsulatedKey = await subtle.decapsulateKey(
{ name: algorithmName },
privateKey,
vectors.sampleCiphertext,
config.algorithm,
true,
config.usages
);
assert_true(
decapsulatedKey instanceof CryptoKey,
'decapsulateKey should return a CryptoKey'
);
assert_equals(
decapsulatedKey.type,
'secret',
'Decapsulated key should be secret type'
);
assert_equals(
decapsulatedKey.algorithm.name,
config.algorithm.name,
'Decapsulated key algorithm should match'
);
assert_true(
decapsulatedKey.extractable,
'Decapsulated key should be extractable as specified'
);
assert_array_equals(
decapsulatedKey.usages,
config.usages,
'Decapsulated key should have correct usages'
);
// Extract the key material and verify it matches the expected shared secret
var keyMaterial = await subtle.exportKey('raw', decapsulatedKey);
assert_equals(
keyMaterial.byteLength,
32,
'Shared key material should be 32 bytes'
);
assert_true(
equalBuffers(keyMaterial, vectors.expectedSharedSecret),
"Decapsulated key material should match vector's expectedSharedSecret"
);
// Verify algorithm-specific properties
if (config.algorithm.length) {
assert_equals(
decapsulatedKey.algorithm.length,
config.algorithm.length,
'Key length should be 256'
);
}
if (config.algorithm.hash) {
assert_equals(
decapsulatedKey.algorithm.hash.name,
config.algorithm.hash,
'Hash algorithm should match'
);
}
}, algorithmName +
' vector-based sampleCiphertext decapsulation with ' +
config.description);
});
});
}
async function testAESOperation(key1, key2, algorithm) {
var plaintext = new Uint8Array([
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
]);
if (algorithm.name === 'AES-GCM') {
var iv = crypto.getRandomValues(new Uint8Array(12));
var params = { name: 'AES-GCM', iv: iv };
var ciphertext1 = await crypto.subtle.encrypt(params, key1, plaintext);
var ciphertext2 = await crypto.subtle.encrypt(params, key2, plaintext);
assert_true(
equalBuffers(ciphertext1, ciphertext2),
'AES-GCM encryption should produce identical results'
);
var decrypted1 = await crypto.subtle.decrypt(params, key1, ciphertext1);
var decrypted2 = await crypto.subtle.decrypt(params, key2, ciphertext2);
assert_true(
equalBuffers(decrypted1, plaintext),
'AES-GCM decryption should work with key1'
);
assert_true(
equalBuffers(decrypted2, plaintext),
'AES-GCM decryption should work with key2'
);
} else if (algorithm.name === 'AES-CBC') {
var iv = crypto.getRandomValues(new Uint8Array(16));
var params = { name: 'AES-CBC', iv: iv };
var ciphertext1 = await crypto.subtle.encrypt(params, key1, plaintext);
var ciphertext2 = await crypto.subtle.encrypt(params, key2, plaintext);
assert_true(
equalBuffers(ciphertext1, ciphertext2),
'AES-CBC encryption should produce identical results'
);
} else if (algorithm.name === 'AES-CTR') {
var counter = crypto.getRandomValues(new Uint8Array(16));
var params = { name: 'AES-CTR', counter: counter, length: 128 };
var ciphertext1 = await crypto.subtle.encrypt(params, key1, plaintext);
var ciphertext2 = await crypto.subtle.encrypt(params, key2, plaintext);
assert_true(
equalBuffers(ciphertext1, ciphertext2),
'AES-CTR encryption should produce identical results'
);
}
}
async function testHMACOperation(key1, key2) {
var data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
var signature1 = await crypto.subtle.sign({ name: 'HMAC' }, key1, data);
var signature2 = await crypto.subtle.sign({ name: 'HMAC' }, key2, data);
assert_true(
equalBuffers(signature1, signature2),
'HMAC signatures should be identical'
);
var verified1 = await crypto.subtle.verify(
{ name: 'HMAC' },
key1,
signature1,
data
);
var verified2 = await crypto.subtle.verify(
{ name: 'HMAC' },
key2,
signature2,
data
);
assert_true(verified1, 'HMAC verification should succeed with key1');
assert_true(verified2, 'HMAC verification should succeed with key2');
}
// Helper function to compare two ArrayBuffers
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;
}
define_key_tests();