Skip to content

Commit 5db244b

Browse files
committed
fixup! crypto: add Hybrid KEMs to Web Cryptography
1 parent 76f6e0b commit 5db244b

2 files changed

Lines changed: 96 additions & 8 deletions

File tree

‎lib/internal/crypto/kem_hybrids.js‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,14 @@ const kUsages = createKeyUsages(kEncapsulationUsages, kDecapsulationUsages);
117117
* @returns {object}
118118
*/
119119
function withHybridLengths(config) {
120-
config.encapsulationKeyLength =
121-
config.kemPqEncapsulationKeyLength + config.groupElementLength;
122-
config.ciphertextLength =
123-
config.kemPqCiphertextLength + config.groupElementLength;
124-
return config;
120+
return {
121+
__proto__: null,
122+
...config,
123+
encapsulationKeyLength:
124+
config.kemPqEncapsulationKeyLength + config.groupElementLength,
125+
ciphertextLength:
126+
config.kemPqCiphertextLength + config.groupElementLength,
127+
};
125128
}
126129

127130
const kAlgorithms = {
@@ -809,6 +812,11 @@ function getGenerateKeyUsages(name, keyUsages) {
809812
* @returns {InternalCryptoKey}
810813
*/
811814
function importJwkKey(input, name, config, extractable, usagesSet) {
815+
const isPublic = input.priv === undefined;
816+
verifyAcceptableKeyUse(
817+
name,
818+
usagesSet,
819+
isPublic ? kUsages.public : kUsages.private);
812820
validateJwk(input, 'AKP', extractable, usagesSet, 'enc');
813821
if (input.alg !== name) {
814822
throw lazyDOMException(
@@ -818,16 +826,14 @@ function importJwkKey(input, name, config, extractable, usagesSet) {
818826

819827
const rawEncapsulationKey = decodeJwkBase64Url(input.pub);
820828
validateLength(rawEncapsulationKey, config.encapsulationKeyLength);
821-
if (input.priv === undefined) {
822-
verifyAcceptableKeyUse(name, usagesSet, kUsages.public);
829+
if (isPublic) {
823830
return createPublicKeyFromRaw(
824831
rawEncapsulationKey,
825832
config,
826833
extractable,
827834
usagesSet);
828835
}
829836

830-
verifyAcceptableKeyUse(name, usagesSet, kUsages.private);
831837
const { privateKey } = deriveKeyPair(
832838
decodeJwkBase64Url(input.priv),
833839
config,

‎test/parallel/test-webcrypto-kem-hybrids.js‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
310348
async 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

Comments
 (0)