encap_decap_keys.tentative.https.any.js (13789B)
1 // META: title=WebCryptoAPI: ML-KEM encapsulateKey() and decapsulateKey() tests 2 // META: script=ml_kem_vectors.js 3 // META: script=../util/helpers.js 4 // META: timeout=long 5 6 function define_key_tests() { 7 var subtle = self.crypto.subtle; 8 var variants = ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']; 9 10 // Test various 256-bit shared key algorithms 11 var sharedKeyConfigs = [ 12 { 13 algorithm: { name: 'AES-GCM', length: 256 }, 14 usages: ['encrypt', 'decrypt'], 15 description: 'AES-GCM-256', 16 }, 17 { 18 algorithm: { name: 'AES-CBC', length: 256 }, 19 usages: ['encrypt', 'decrypt'], 20 description: 'AES-CBC-256', 21 }, 22 { 23 algorithm: { name: 'AES-CTR', length: 256 }, 24 usages: ['encrypt', 'decrypt'], 25 description: 'AES-CTR-256', 26 }, 27 { 28 algorithm: { name: 'AES-KW', length: 256 }, 29 usages: ['wrapKey', 'unwrapKey'], 30 description: 'AES-KW-256', 31 }, 32 { 33 algorithm: { name: 'HMAC', hash: 'SHA-256' }, 34 usages: ['sign', 'verify'], 35 description: 'HMAC-SHA-256', 36 }, 37 ]; 38 39 variants.forEach(function (algorithmName) { 40 sharedKeyConfigs.forEach(function (config) { 41 // Test encapsulateKey operation 42 promise_test(async function (test) { 43 // Generate a key pair for testing 44 var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ 45 'encapsulateKey', 46 'decapsulateKey', 47 ]); 48 49 // Test encapsulateKey 50 var encapsulatedKey = await subtle.encapsulateKey( 51 { name: algorithmName }, 52 keyPair.publicKey, 53 config.algorithm, 54 true, 55 config.usages 56 ); 57 58 assert_true( 59 encapsulatedKey instanceof Object, 60 'encapsulateKey should return an object' 61 ); 62 assert_true( 63 encapsulatedKey.hasOwnProperty('sharedKey'), 64 'Result should have sharedKey property' 65 ); 66 assert_true( 67 encapsulatedKey.hasOwnProperty('ciphertext'), 68 'Result should have ciphertext property' 69 ); 70 assert_true( 71 encapsulatedKey.sharedKey instanceof CryptoKey, 72 'sharedKey should be a CryptoKey' 73 ); 74 assert_true( 75 encapsulatedKey.ciphertext instanceof ArrayBuffer, 76 'ciphertext should be ArrayBuffer' 77 ); 78 79 // Verify the shared key properties 80 assert_equals( 81 encapsulatedKey.sharedKey.type, 82 'secret', 83 'Shared key should be secret type' 84 ); 85 assert_equals( 86 encapsulatedKey.sharedKey.algorithm.name, 87 config.algorithm.name, 88 'Shared key algorithm should match' 89 ); 90 assert_true( 91 encapsulatedKey.sharedKey.extractable, 92 'Shared key should be extractable as specified' 93 ); 94 assert_array_equals( 95 encapsulatedKey.sharedKey.usages, 96 config.usages, 97 'Shared key should have correct usages' 98 ); 99 100 // Verify algorithm-specific properties 101 if (config.algorithm.length) { 102 assert_equals( 103 encapsulatedKey.sharedKey.algorithm.length, 104 config.algorithm.length, 105 'Key length should be 256' 106 ); 107 } 108 if (config.algorithm.hash) { 109 assert_equals( 110 encapsulatedKey.sharedKey.algorithm.hash.name, 111 config.algorithm.hash, 112 'Hash algorithm should match' 113 ); 114 } 115 116 // Verify ciphertext length based on algorithm variant 117 var expectedCiphertextLength; 118 switch (algorithmName) { 119 case 'ML-KEM-512': 120 expectedCiphertextLength = 768; 121 break; 122 case 'ML-KEM-768': 123 expectedCiphertextLength = 1088; 124 break; 125 case 'ML-KEM-1024': 126 expectedCiphertextLength = 1568; 127 break; 128 } 129 assert_equals( 130 encapsulatedKey.ciphertext.byteLength, 131 expectedCiphertextLength, 132 'Ciphertext should be ' + 133 expectedCiphertextLength + 134 ' bytes for ' + 135 algorithmName 136 ); 137 }, algorithmName + ' encapsulateKey with ' + config.description); 138 139 // Test decapsulateKey operation 140 promise_test(async function (test) { 141 // Generate a key pair for testing 142 var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ 143 'encapsulateKey', 144 'decapsulateKey', 145 ]); 146 147 // First encapsulate to get ciphertext 148 var encapsulatedKey = await subtle.encapsulateKey( 149 { name: algorithmName }, 150 keyPair.publicKey, 151 config.algorithm, 152 true, 153 config.usages 154 ); 155 156 // Then decapsulate using the private key 157 var decapsulatedKey = await subtle.decapsulateKey( 158 { name: algorithmName }, 159 keyPair.privateKey, 160 encapsulatedKey.ciphertext, 161 config.algorithm, 162 true, 163 config.usages 164 ); 165 166 assert_true( 167 decapsulatedKey instanceof CryptoKey, 168 'decapsulateKey should return a CryptoKey' 169 ); 170 assert_equals( 171 decapsulatedKey.type, 172 'secret', 173 'Decapsulated key should be secret type' 174 ); 175 assert_equals( 176 decapsulatedKey.algorithm.name, 177 config.algorithm.name, 178 'Decapsulated key algorithm should match' 179 ); 180 assert_true( 181 decapsulatedKey.extractable, 182 'Decapsulated key should be extractable as specified' 183 ); 184 assert_array_equals( 185 decapsulatedKey.usages, 186 config.usages, 187 'Decapsulated key should have correct usages' 188 ); 189 190 // Extract both keys and verify they are identical 191 var originalKeyMaterial = await subtle.exportKey( 192 'raw', 193 encapsulatedKey.sharedKey 194 ); 195 var decapsulatedKeyMaterial = await subtle.exportKey( 196 'raw', 197 decapsulatedKey 198 ); 199 200 assert_true( 201 equalBuffers(originalKeyMaterial, decapsulatedKeyMaterial), 202 'Decapsulated key material should match original' 203 ); 204 205 // Verify the key material is 32 bytes (256 bits) 206 assert_equals( 207 originalKeyMaterial.byteLength, 208 32, 209 'Shared key material should be 32 bytes' 210 ); 211 }, algorithmName + ' decapsulateKey with ' + config.description); 212 213 // Test round-trip compatibility 214 promise_test(async function (test) { 215 var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ 216 'encapsulateKey', 217 'decapsulateKey', 218 ]); 219 220 var encapsulatedKey = await subtle.encapsulateKey( 221 { name: algorithmName }, 222 keyPair.publicKey, 223 config.algorithm, 224 true, 225 config.usages 226 ); 227 228 var decapsulatedKey = await subtle.decapsulateKey( 229 { name: algorithmName }, 230 keyPair.privateKey, 231 encapsulatedKey.ciphertext, 232 config.algorithm, 233 true, 234 config.usages 235 ); 236 237 // Verify keys have the same material 238 var originalKeyMaterial = await subtle.exportKey( 239 'raw', 240 encapsulatedKey.sharedKey 241 ); 242 var decapsulatedKeyMaterial = await subtle.exportKey( 243 'raw', 244 decapsulatedKey 245 ); 246 247 assert_true( 248 equalBuffers(originalKeyMaterial, decapsulatedKeyMaterial), 249 'Encapsulated and decapsulated keys should have the same material' 250 ); 251 252 // Test that the derived keys can actually be used for their intended purpose 253 if ( 254 config.algorithm.name.startsWith('AES') && 255 config.usages.includes('encrypt') 256 ) { 257 await testAESOperation( 258 encapsulatedKey.sharedKey, 259 decapsulatedKey, 260 config.algorithm 261 ); 262 } else if (config.algorithm.name === 'HMAC') { 263 await testHMACOperation(encapsulatedKey.sharedKey, decapsulatedKey); 264 } 265 }, algorithmName + 266 ' encapsulateKey/decapsulateKey round-trip with ' + 267 config.description); 268 }); 269 270 // Test vector-based decapsulation for each shared key config 271 sharedKeyConfigs.forEach(function (config) { 272 promise_test(async function (test) { 273 var vectors = ml_kem_vectors[algorithmName]; 274 275 // Import the private key from the vector's privateSeed 276 var privateKey = await subtle.importKey( 277 'raw-seed', 278 vectors.privateSeed, 279 { name: algorithmName }, 280 false, 281 ['decapsulateKey'] 282 ); 283 284 // Decapsulate the sample ciphertext from the vectors to get a shared key 285 var decapsulatedKey = await subtle.decapsulateKey( 286 { name: algorithmName }, 287 privateKey, 288 vectors.sampleCiphertext, 289 config.algorithm, 290 true, 291 config.usages 292 ); 293 294 assert_true( 295 decapsulatedKey instanceof CryptoKey, 296 'decapsulateKey should return a CryptoKey' 297 ); 298 assert_equals( 299 decapsulatedKey.type, 300 'secret', 301 'Decapsulated key should be secret type' 302 ); 303 assert_equals( 304 decapsulatedKey.algorithm.name, 305 config.algorithm.name, 306 'Decapsulated key algorithm should match' 307 ); 308 assert_true( 309 decapsulatedKey.extractable, 310 'Decapsulated key should be extractable as specified' 311 ); 312 assert_array_equals( 313 decapsulatedKey.usages, 314 config.usages, 315 'Decapsulated key should have correct usages' 316 ); 317 318 // Extract the key material and verify it matches the expected shared secret 319 var keyMaterial = await subtle.exportKey('raw', decapsulatedKey); 320 assert_equals( 321 keyMaterial.byteLength, 322 32, 323 'Shared key material should be 32 bytes' 324 ); 325 assert_true( 326 equalBuffers(keyMaterial, vectors.expectedSharedSecret), 327 "Decapsulated key material should match vector's expectedSharedSecret" 328 ); 329 330 // Verify algorithm-specific properties 331 if (config.algorithm.length) { 332 assert_equals( 333 decapsulatedKey.algorithm.length, 334 config.algorithm.length, 335 'Key length should be 256' 336 ); 337 } 338 if (config.algorithm.hash) { 339 assert_equals( 340 decapsulatedKey.algorithm.hash.name, 341 config.algorithm.hash, 342 'Hash algorithm should match' 343 ); 344 } 345 }, algorithmName + 346 ' vector-based sampleCiphertext decapsulation with ' + 347 config.description); 348 }); 349 }); 350 } 351 352 async function testAESOperation(key1, key2, algorithm) { 353 var plaintext = new Uint8Array([ 354 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 355 ]); 356 357 if (algorithm.name === 'AES-GCM') { 358 var iv = crypto.getRandomValues(new Uint8Array(12)); 359 var params = { name: 'AES-GCM', iv: iv }; 360 361 var ciphertext1 = await crypto.subtle.encrypt(params, key1, plaintext); 362 var ciphertext2 = await crypto.subtle.encrypt(params, key2, plaintext); 363 364 assert_true( 365 equalBuffers(ciphertext1, ciphertext2), 366 'AES-GCM encryption should produce identical results' 367 ); 368 369 var decrypted1 = await crypto.subtle.decrypt(params, key1, ciphertext1); 370 var decrypted2 = await crypto.subtle.decrypt(params, key2, ciphertext2); 371 372 assert_true( 373 equalBuffers(decrypted1, plaintext), 374 'AES-GCM decryption should work with key1' 375 ); 376 assert_true( 377 equalBuffers(decrypted2, plaintext), 378 'AES-GCM decryption should work with key2' 379 ); 380 } else if (algorithm.name === 'AES-CBC') { 381 var iv = crypto.getRandomValues(new Uint8Array(16)); 382 var params = { name: 'AES-CBC', iv: iv }; 383 384 var ciphertext1 = await crypto.subtle.encrypt(params, key1, plaintext); 385 var ciphertext2 = await crypto.subtle.encrypt(params, key2, plaintext); 386 387 assert_true( 388 equalBuffers(ciphertext1, ciphertext2), 389 'AES-CBC encryption should produce identical results' 390 ); 391 } else if (algorithm.name === 'AES-CTR') { 392 var counter = crypto.getRandomValues(new Uint8Array(16)); 393 var params = { name: 'AES-CTR', counter: counter, length: 128 }; 394 395 var ciphertext1 = await crypto.subtle.encrypt(params, key1, plaintext); 396 var ciphertext2 = await crypto.subtle.encrypt(params, key2, plaintext); 397 398 assert_true( 399 equalBuffers(ciphertext1, ciphertext2), 400 'AES-CTR encryption should produce identical results' 401 ); 402 } 403 } 404 405 async function testHMACOperation(key1, key2) { 406 var data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); 407 408 var signature1 = await crypto.subtle.sign({ name: 'HMAC' }, key1, data); 409 var signature2 = await crypto.subtle.sign({ name: 'HMAC' }, key2, data); 410 411 assert_true( 412 equalBuffers(signature1, signature2), 413 'HMAC signatures should be identical' 414 ); 415 416 var verified1 = await crypto.subtle.verify( 417 { name: 'HMAC' }, 418 key1, 419 signature1, 420 data 421 ); 422 var verified2 = await crypto.subtle.verify( 423 { name: 'HMAC' }, 424 key2, 425 signature2, 426 data 427 ); 428 429 assert_true(verified1, 'HMAC verification should succeed with key1'); 430 assert_true(verified2, 'HMAC verification should succeed with key2'); 431 } 432 433 // Helper function to compare two ArrayBuffers 434 function equalBuffers(a, b) { 435 if (a.byteLength !== b.byteLength) { 436 return false; 437 } 438 var aBytes = new Uint8Array(a); 439 var bBytes = new Uint8Array(b); 440 for (var i = 0; i < a.byteLength; i++) { 441 if (aBytes[i] !== bBytes[i]) { 442 return false; 443 } 444 } 445 return true; 446 } 447 448 define_key_tests();