Skip to content

crypto: support ML-KEM in Web Cryptography #59569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions deps/ncrypto/ncrypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2162,21 +2162,34 @@ DataPointer EVPKeyPointer::rawPublicKey() const {
#if OPENSSL_WITH_PQC
DataPointer EVPKeyPointer::rawSeed() const {
if (!pkey_) return {};

// Determine seed length and parameter name based on key type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Determine seed length and parameter name based on key type
// Determine seed length and parameter name based on key type.

size_t seed_len;
const char* param_name;

switch (id()) {
case EVP_PKEY_ML_DSA_44:
case EVP_PKEY_ML_DSA_65:
case EVP_PKEY_ML_DSA_87:
seed_len = 32; // ML-DSA uses 32-byte seeds
param_name = OSSL_PKEY_PARAM_ML_DSA_SEED;
break;
case EVP_PKEY_ML_KEM_512:
case EVP_PKEY_ML_KEM_768:
case EVP_PKEY_ML_KEM_1024:
seed_len = 64; // ML-KEM uses 64-byte seeds
param_name = OSSL_PKEY_PARAM_ML_KEM_SEED;
break;
default:
unreachable();
}

size_t seed_len = 32;
if (auto data = DataPointer::Alloc(seed_len)) {
const Buffer<unsigned char> buf = data;
size_t len = data.size();

if (EVP_PKEY_get_octet_string_param(
get(), OSSL_PKEY_PARAM_ML_DSA_SEED, buf.data, len, &seed_len) != 1)
get(), param_name, buf.data, len, &seed_len) != 1)
return {};
return data;
}
Expand Down
354 changes: 292 additions & 62 deletions doc/api/webcrypto.md

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion lib/internal/crypto/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,14 @@ const {
result = require('internal/crypto/ml_dsa')
.mlDsaImportKey('KeyObject', this, algorithm, extractable, keyUsages);
break;
case 'ML-KEM-512':
// Fall through
case 'ML-KEM-768':
// Fall through
case 'ML-KEM-1024':
result = require('internal/crypto/ml_kem')
.mlKemImportKey('KeyObject', this, algorithm, extractable, keyUsages);
break;
default:
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
}
Expand Down Expand Up @@ -568,7 +576,7 @@ function getKeyObjectHandleFromJwk(key, ctx) {
const handle = new KeyObjectHandle();

const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate;
if (!handle.initMlDsaRaw(key.alg, keyData, keyType)) {
if (!handle.initPqcRaw(key.alg, keyData, keyType)) {
throw new ERR_CRYPTO_INVALID_JWK();
}

Expand Down
11 changes: 4 additions & 7 deletions lib/internal/crypto/ml_dsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function verifyAcceptableMlDsaKeyUse(name, isPublic, usages) {
function createMlDsaRawKey(name, keyData, isPublic) {
const handle = new KeyObjectHandle();
const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate;
if (!handle.initMlDsaRaw(name, keyData, keyType)) {
if (!handle.initPqcRaw(name, keyData, keyType)) {
throw lazyDOMException('Invalid keyData', 'DataError');
}

Expand Down Expand Up @@ -119,19 +119,16 @@ function mlDsaExportKey(key, format) {
switch (format) {
case kWebCryptoKeyFormatRaw: {
if (key[kKeyType] === 'private') {
const { priv } = key[kKeyObject][kHandle].exportJwk({}, false);
return Buffer.alloc(32, priv, 'base64url').buffer;
return key[kKeyObject][kHandle].rawSeed().buffer;
}

const { pub } = key[kKeyObject][kHandle].exportJwk({}, false);
return Buffer.alloc(Buffer.byteLength(pub, 'base64url'), pub, 'base64url').buffer;
return key[kKeyObject][kHandle].rawPublicKey().buffer;
}
case kWebCryptoKeyFormatSPKI: {
return key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI).buffer;
}
case kWebCryptoKeyFormatPKCS8: {
const { priv } = key[kKeyObject][kHandle].exportJwk({}, false);
const seed = Buffer.alloc(32, priv, 'base64url');
const seed = key[kKeyObject][kHandle].rawSeed();
const buffer = new Uint8Array(54);
buffer.set([
0x30, 0x34, 0x02, 0x01, 0x00, 0x30, 0x0B, 0x06,
Expand Down
287 changes: 287 additions & 0 deletions lib/internal/crypto/ml_kem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
'use strict';

const {
PromiseWithResolvers,
SafeSet,
Uint8Array,
} = primordials;

const {
kCryptoJobAsync,
KEMDecapsulateJob,
KEMEncapsulateJob,
KeyObjectHandle,
kKeyFormatDER,
kKeyTypePrivate,
kKeyTypePublic,
kWebCryptoKeyFormatPKCS8,
kWebCryptoKeyFormatRaw,
kWebCryptoKeyFormatSPKI,
} = internalBinding('crypto');

const {
getUsagesUnion,
hasAnyNotIn,
kHandle,
kKeyObject,
} = require('internal/crypto/util');

const {
lazyDOMException,
promisify,
} = require('internal/util');

const {
generateKeyPair: _generateKeyPair,
} = require('internal/crypto/keygen');

const {
InternalCryptoKey,
PrivateKeyObject,
PublicKeyObject,
createPrivateKey,
createPublicKey,
kAlgorithm,
kKeyType,
} = require('internal/crypto/keys');

const generateKeyPair = promisify(_generateKeyPair);

async function mlKemGenerateKey(algorithm, extractable, keyUsages) {
const { name } = algorithm;

const usageSet = new SafeSet(keyUsages);
if (hasAnyNotIn(usageSet, ['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits'])) {
throw lazyDOMException(
`Unsupported key usage for an ${name} key`,
'SyntaxError');
}

const keyPair = await generateKeyPair(name.toLowerCase()).catch((err) => {
throw lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: err });
});

const publicUsages = getUsagesUnion(usageSet, 'encapsulateBits', 'encapsulateKey');
const privateUsages = getUsagesUnion(usageSet, 'decapsulateBits', 'decapsulateKey');

const keyAlgorithm = { name };

const publicKey =
new InternalCryptoKey(
keyPair.publicKey,
keyAlgorithm,
publicUsages,
true);

const privateKey =
new InternalCryptoKey(
keyPair.privateKey,
keyAlgorithm,
privateUsages,
extractable);

return { __proto__: null, privateKey, publicKey };
}

function mlKemExportKey(key, format) {
try {
switch (format) {
case kWebCryptoKeyFormatRaw: {
if (key[kKeyType] === 'private') {
return key[kKeyObject][kHandle].rawSeed().buffer;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to change the internal APIs to return ArrayBuffer objects if the implicit assumption in their use is that the TypedArray is a view of the entire ArrayBuffer. (Not to be changed in this PR though.)

}

return key[kKeyObject][kHandle].rawPublicKey().buffer;
}
case kWebCryptoKeyFormatSPKI: {
return key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI).buffer;
}
case kWebCryptoKeyFormatPKCS8: {
const seed = key[kKeyObject][kHandle].rawSeed();
const buffer = new Uint8Array(86);
buffer.set([
0x30, 0x54, 0x02, 0x01, 0x00, 0x30, 0x0B, 0x06,
0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04,
0x04, 0x00, 0x04, 0x42, 0x80, 0x40,
], 0);
switch (key[kAlgorithm].name) {
case 'ML-KEM-512':
buffer.set([0x01], 17);
break;
case 'ML-KEM-768':
buffer.set([0x02], 17);
break;
case 'ML-KEM-1024':
buffer.set([0x03], 17);
break;
}
buffer.set(seed, 22);
return buffer.buffer;
}
default:
return undefined;
}
} catch (err) {
throw lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: err });
}
}

function verifyAcceptableMlKemKeyUse(name, isPublic, usages) {
const checkSet = isPublic ? ['encapsulateKey', 'encapsulateBits'] : ['decapsulateKey', 'decapsulateBits'];
if (hasAnyNotIn(usages, checkSet)) {
throw lazyDOMException(
`Unsupported key usage for a ${name} key`,
'SyntaxError');
}
}

function createMlKemRawKey(name, keyData, isPublic) {
const handle = new KeyObjectHandle();
const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate;
if (!handle.initPqcRaw(name, keyData, keyType)) {
throw lazyDOMException('Invalid keyData', 'DataError');
}

return isPublic ? new PublicKeyObject(handle) : new PrivateKeyObject(handle);
}

function mlKemImportKey(
format,
keyData,
algorithm,
extractable,
keyUsages) {

const { name } = algorithm;
let keyObject;
const usagesSet = new SafeSet(keyUsages);
switch (format) {
case 'KeyObject': {
verifyAcceptableMlKemKeyUse(name, keyData.type === 'public', usagesSet);
keyObject = keyData;
break;
}
case 'spki': {
verifyAcceptableMlKemKeyUse(name, true, usagesSet);
try {
keyObject = createPublicKey({
key: keyData,
format: 'der',
type: 'spki',
});
} catch (err) {
throw lazyDOMException(
'Invalid keyData', { name: 'DataError', cause: err });
}
break;
}
case 'pkcs8': {
verifyAcceptableMlKemKeyUse(name, false, usagesSet);
try {
keyObject = createPrivateKey({
key: keyData,
format: 'der',
type: 'pkcs8',
});
} catch (err) {
throw lazyDOMException(
'Invalid keyData', { name: 'DataError', cause: err });
}
break;
}
case 'raw-public':
case 'raw-seed': {
const isPublic = format === 'raw-public';
verifyAcceptableMlKemKeyUse(name, isPublic, usagesSet);

try {
keyObject = createMlKemRawKey(name, keyData, isPublic);
} catch (err) {
throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err });
}
break;
}
default:
return undefined;
}

if (keyObject.asymmetricKeyType !== name.toLowerCase()) {
throw lazyDOMException('Invalid key type', 'DataError');
}

return new InternalCryptoKey(
keyObject,
{ name },
keyUsages,
extractable);
}

function mlKemEncapsulate(encapsulationKey) {
if (encapsulationKey[kKeyType] !== 'public') {
throw lazyDOMException(`Key must be a public key`, 'InvalidAccessError');
}

const { promise, resolve, reject } = PromiseWithResolvers();

const job = new KEMEncapsulateJob(
kCryptoJobAsync,
encapsulationKey[kKeyObject][kHandle],
undefined,
undefined,
undefined);

job.ondone = (error, result) => {
if (error) {
reject(lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: error }));
} else {
const { 0: sharedKey, 1: ciphertext } = result;
resolve({ sharedKey: sharedKey.buffer, ciphertext: ciphertext.buffer });
}
};
job.run();

return promise;
}

function mlKemDecapsulate(decapsulationKey, ciphertext) {
if (decapsulationKey[kKeyType] !== 'private') {
throw lazyDOMException(`Key must be a private key`, 'InvalidAccessError');
}

const { promise, resolve, reject } = PromiseWithResolvers();

const job = new KEMDecapsulateJob(
kCryptoJobAsync,
decapsulationKey[kKeyObject][kHandle],
undefined,
undefined,
undefined,
ciphertext);

job.ondone = (error, result) => {
if (error) {
reject(lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: error }));
} else {
resolve(result.buffer);
}
};
job.run();

return promise;
}

module.exports = {
mlKemExportKey,
mlKemImportKey,
mlKemEncapsulate,
mlKemDecapsulate,
mlKemGenerateKey,
};
Loading
Loading