@@ -307,6 +307,44 @@ function buffers(vector) {
307307 } ;
308308}
309309
310+ async function testAlgorithmConfigPrototypePollution ( ) {
311+ const properties = [
312+ 'encapsulationKeyLength' ,
313+ 'ciphertextLength' ,
314+ 'groupOrder' ,
315+ 'namedCurve' ,
316+ ] ;
317+ const descriptors = new Map ( ) ;
318+
319+ for ( const property of properties ) {
320+ descriptors . set (
321+ property ,
322+ Object . getOwnPropertyDescriptor ( Object . prototype , property ) ) ;
323+ Object . defineProperty ( Object . prototype , property , {
324+ __proto__ : null ,
325+ configurable : true ,
326+ get : common . mustNotCall ( `Object.prototype.${ property } getter` ) ,
327+ set : common . mustNotCall ( `Object.prototype.${ property } setter` ) ,
328+ } ) ;
329+ }
330+
331+ try {
332+ await subtle . generateKey (
333+ 'MLKEM768-X25519' ,
334+ true ,
335+ [ 'encapsulateBits' , 'decapsulateBits' ] ) ;
336+ } finally {
337+ for ( const property of properties ) {
338+ const descriptor = descriptors . get ( property ) ;
339+ if ( descriptor === undefined ) {
340+ delete Object . prototype [ property ] ;
341+ } else {
342+ Object . defineProperty ( Object . prototype , property , descriptor ) ;
343+ }
344+ }
345+ }
346+ }
347+
310348async function testGeneratedRoundTrip ( vector ) {
311349 const algorithm = { name : vector . name } ;
312350 const { ciphertext } = buffers ( vector ) ;
@@ -415,6 +453,28 @@ async function testVectorRoundTrip(vector) {
415453 ciphertext ) ;
416454 assert ( Buffer . from ( vectorSharedSecret ) . equals ( sharedSecret ) ) ;
417455
456+ const implicitRejectionCiphertext = Buffer . from ( ciphertext ) ;
457+ const pqCiphertextLength =
458+ ciphertext . byteLength - lengths [ vector . name ] . groupElement ;
459+ implicitRejectionCiphertext . fill ( 0 , 0 , pqCiphertextLength ) ;
460+ // Preserve the valid traditional group element so this only exercises
461+ // ML-KEM implicit rejection.
462+ assert ( implicitRejectionCiphertext . subarray ( pqCiphertextLength )
463+ . equals ( ciphertext . subarray ( pqCiphertextLength ) ) ) ;
464+
465+ const implicitRejectionSharedSecret = Buffer . from (
466+ await subtle . decapsulateBits (
467+ algorithm ,
468+ privateKey ,
469+ implicitRejectionCiphertext ) ) ;
470+ assert . strictEqual ( implicitRejectionSharedSecret . byteLength , 32 ) ;
471+ assert ( ! implicitRejectionSharedSecret . equals ( sharedSecret ) ) ;
472+ assert ( implicitRejectionSharedSecret . equals ( Buffer . from (
473+ await subtle . decapsulateBits (
474+ algorithm ,
475+ privateKey ,
476+ implicitRejectionCiphertext ) ) ) ) ;
477+
418478 const publicKeyOnly = await subtle . importKey (
419479 'raw-public' ,
420480 publicKey ,
@@ -500,6 +560,27 @@ async function testFailures(vector) {
500560 const publicKeyOnly = await subtle . getPublicKey ( privateKey , [ 'encapsulateBits' ] ) ;
501561 const jwk = await subtle . exportKey ( 'jwk' , privateKey ) ;
502562
563+ // JWK key usage validation precedes `key_ops` validation.
564+ await assert . rejects (
565+ subtle . importKey (
566+ 'jwk' ,
567+ { ...jwk , key_ops : [ 'encapsulateBits' , 'encapsulateBits' ] } ,
568+ algorithm ,
569+ true ,
570+ [ 'encapsulateBits' ] ) ,
571+ {
572+ name : 'SyntaxError' ,
573+ message : `Unsupported key usage for ${ vector . name } key` ,
574+ } ) ;
575+ await assert . rejects (
576+ subtle . importKey (
577+ 'jwk' ,
578+ { ...jwk , key_ops : [ 'decapsulateBits' , 'decapsulateBits' ] } ,
579+ algorithm ,
580+ true ,
581+ [ 'decapsulateBits' ] ) ,
582+ { name : 'DataError' , message : 'Duplicate key operation' } ) ;
583+
503584 await assert . rejects (
504585 subtle . importKey ( 'raw-public' , Buffer . alloc ( publicKey . byteLength - 1 ) , algorithm , true , [ 'encapsulateBits' ] ) ,
505586 { name : 'DataError' } ) ;
@@ -598,6 +679,7 @@ function testSupports(vector) {
598679}
599680
600681( async ( ) => {
682+ await testAlgorithmConfigPrototypePollution ( ) ;
601683 for ( const vector of vectors ) {
602684 testSupports ( vector ) ;
603685 await testGeneratedRoundTrip ( vector ) ;
0 commit comments