Skip to content

Commit 8625fe6

Browse files
committed
crypto: support ML-KEM in Web Cryptography
1 parent d30090b commit 8625fe6

18 files changed

+1851
-100
lines changed

deps/ncrypto/ncrypto.cc

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2162,21 +2162,34 @@ DataPointer EVPKeyPointer::rawPublicKey() const {
21622162
#if OPENSSL_WITH_PQC
21632163
DataPointer EVPKeyPointer::rawSeed() const {
21642164
if (!pkey_) return {};
2165+
2166+
// Determine seed length and parameter name based on key type
2167+
size_t seed_len;
2168+
const char* param_name;
2169+
21652170
switch (id()) {
21662171
case EVP_PKEY_ML_DSA_44:
21672172
case EVP_PKEY_ML_DSA_65:
21682173
case EVP_PKEY_ML_DSA_87:
2174+
seed_len = 32; // ML-DSA uses 32-byte seeds
2175+
param_name = OSSL_PKEY_PARAM_ML_DSA_SEED;
2176+
break;
2177+
case EVP_PKEY_ML_KEM_512:
2178+
case EVP_PKEY_ML_KEM_768:
2179+
case EVP_PKEY_ML_KEM_1024:
2180+
seed_len = 64; // ML-KEM uses 64-byte seeds
2181+
param_name = OSSL_PKEY_PARAM_ML_KEM_SEED;
21692182
break;
21702183
default:
21712184
unreachable();
21722185
}
21732186

2174-
size_t seed_len = 32;
21752187
if (auto data = DataPointer::Alloc(seed_len)) {
21762188
const Buffer<unsigned char> buf = data;
21772189
size_t len = data.size();
2190+
21782191
if (EVP_PKEY_get_octet_string_param(
2179-
get(), OSSL_PKEY_PARAM_ML_DSA_SEED, buf.data, len, &seed_len) != 1)
2192+
get(), param_name, buf.data, len, &seed_len) != 1)
21802193
return {};
21812194
return data;
21822195
}

doc/api/webcrypto.md

Lines changed: 292 additions & 62 deletions
Large diffs are not rendered by default.

lib/internal/crypto/keys.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,14 @@ const {
308308
result = require('internal/crypto/ml_dsa')
309309
.mlDsaImportKey('KeyObject', this, algorithm, extractable, keyUsages);
310310
break;
311+
case 'ML-KEM-512':
312+
// Fall through
313+
case 'ML-KEM-768':
314+
// Fall through
315+
case 'ML-KEM-1024':
316+
result = require('internal/crypto/ml_kem')
317+
.mlKemImportKey('KeyObject', this, algorithm, extractable, keyUsages);
318+
break;
311319
default:
312320
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
313321
}
@@ -568,7 +576,7 @@ function getKeyObjectHandleFromJwk(key, ctx) {
568576
const handle = new KeyObjectHandle();
569577

570578
const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate;
571-
if (!handle.initMlDsaRaw(key.alg, keyData, keyType)) {
579+
if (!handle.initPqcRaw(key.alg, keyData, keyType)) {
572580
throw new ERR_CRYPTO_INVALID_JWK();
573581
}
574582

lib/internal/crypto/ml_dsa.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function verifyAcceptableMlDsaKeyUse(name, isPublic, usages) {
6969
function createMlDsaRawKey(name, keyData, isPublic) {
7070
const handle = new KeyObjectHandle();
7171
const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate;
72-
if (!handle.initMlDsaRaw(name, keyData, keyType)) {
72+
if (!handle.initPqcRaw(name, keyData, keyType)) {
7373
throw lazyDOMException('Invalid keyData', 'DataError');
7474
}
7575

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

126-
const { pub } = key[kKeyObject][kHandle].exportJwk({}, false);
127-
return Buffer.alloc(Buffer.byteLength(pub, 'base64url'), pub, 'base64url').buffer;
125+
return key[kKeyObject][kHandle].rawPublicKey().buffer;
128126
}
129127
case kWebCryptoKeyFormatSPKI: {
130128
return key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI).buffer;
131129
}
132130
case kWebCryptoKeyFormatPKCS8: {
133-
const { priv } = key[kKeyObject][kHandle].exportJwk({}, false);
134-
const seed = Buffer.alloc(32, priv, 'base64url');
131+
const seed = key[kKeyObject][kHandle].rawSeed();
135132
const buffer = new Uint8Array(54);
136133
buffer.set([
137134
0x30, 0x34, 0x02, 0x01, 0x00, 0x30, 0x0B, 0x06,

lib/internal/crypto/ml_kem.js

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
'use strict';
2+
3+
const {
4+
PromiseWithResolvers,
5+
SafeSet,
6+
Uint8Array,
7+
} = primordials;
8+
9+
const {
10+
kCryptoJobAsync,
11+
KEMDecapsulateJob,
12+
KEMEncapsulateJob,
13+
KeyObjectHandle,
14+
kKeyFormatDER,
15+
kKeyTypePrivate,
16+
kKeyTypePublic,
17+
kWebCryptoKeyFormatPKCS8,
18+
kWebCryptoKeyFormatRaw,
19+
kWebCryptoKeyFormatSPKI,
20+
} = internalBinding('crypto');
21+
22+
const {
23+
getUsagesUnion,
24+
hasAnyNotIn,
25+
kHandle,
26+
kKeyObject,
27+
} = require('internal/crypto/util');
28+
29+
const {
30+
lazyDOMException,
31+
promisify,
32+
} = require('internal/util');
33+
34+
const {
35+
generateKeyPair: _generateKeyPair,
36+
} = require('internal/crypto/keygen');
37+
38+
const {
39+
InternalCryptoKey,
40+
PrivateKeyObject,
41+
PublicKeyObject,
42+
createPrivateKey,
43+
createPublicKey,
44+
kAlgorithm,
45+
kKeyType,
46+
} = require('internal/crypto/keys');
47+
48+
const generateKeyPair = promisify(_generateKeyPair);
49+
50+
async function mlKemGenerateKey(algorithm, extractable, keyUsages) {
51+
const { name } = algorithm;
52+
53+
const usageSet = new SafeSet(keyUsages);
54+
if (hasAnyNotIn(usageSet, ['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits'])) {
55+
throw lazyDOMException(
56+
`Unsupported key usage for an ${name} key`,
57+
'SyntaxError');
58+
}
59+
60+
const keyPair = await generateKeyPair(name.toLowerCase()).catch((err) => {
61+
throw lazyDOMException(
62+
'The operation failed for an operation-specific reason',
63+
{ name: 'OperationError', cause: err });
64+
});
65+
66+
const publicUsages = getUsagesUnion(usageSet, 'encapsulateBits', 'encapsulateKey');
67+
const privateUsages = getUsagesUnion(usageSet, 'decapsulateBits', 'decapsulateKey');
68+
69+
const keyAlgorithm = { name };
70+
71+
const publicKey =
72+
new InternalCryptoKey(
73+
keyPair.publicKey,
74+
keyAlgorithm,
75+
publicUsages,
76+
true);
77+
78+
const privateKey =
79+
new InternalCryptoKey(
80+
keyPair.privateKey,
81+
keyAlgorithm,
82+
privateUsages,
83+
extractable);
84+
85+
return { __proto__: null, privateKey, publicKey };
86+
}
87+
88+
function mlKemExportKey(key, format) {
89+
try {
90+
switch (format) {
91+
case kWebCryptoKeyFormatRaw: {
92+
if (key[kKeyType] === 'private') {
93+
return key[kKeyObject][kHandle].rawSeed().buffer;
94+
}
95+
96+
return key[kKeyObject][kHandle].rawPublicKey().buffer;
97+
}
98+
case kWebCryptoKeyFormatSPKI: {
99+
return key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI).buffer;
100+
}
101+
case kWebCryptoKeyFormatPKCS8: {
102+
const seed = key[kKeyObject][kHandle].rawSeed();
103+
const buffer = new Uint8Array(86);
104+
buffer.set([
105+
0x30, 0x54, 0x02, 0x01, 0x00, 0x30, 0x0B, 0x06,
106+
0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04,
107+
0x04, 0x00, 0x04, 0x42, 0x80, 0x40,
108+
], 0);
109+
switch (key[kAlgorithm].name) {
110+
case 'ML-KEM-512':
111+
buffer.set([0x01], 17);
112+
break;
113+
case 'ML-KEM-768':
114+
buffer.set([0x02], 17);
115+
break;
116+
case 'ML-KEM-1024':
117+
buffer.set([0x03], 17);
118+
break;
119+
}
120+
buffer.set(seed, 22);
121+
return buffer.buffer;
122+
}
123+
default:
124+
return undefined;
125+
}
126+
} catch (err) {
127+
throw lazyDOMException(
128+
'The operation failed for an operation-specific reason',
129+
{ name: 'OperationError', cause: err });
130+
}
131+
}
132+
133+
function verifyAcceptableMlKemKeyUse(name, isPublic, usages) {
134+
const checkSet = isPublic ? ['encapsulateKey', 'encapsulateBits'] : ['decapsulateKey', 'decapsulateBits'];
135+
if (hasAnyNotIn(usages, checkSet)) {
136+
throw lazyDOMException(
137+
`Unsupported key usage for a ${name} key`,
138+
'SyntaxError');
139+
}
140+
}
141+
142+
function createMlKemRawKey(name, keyData, isPublic) {
143+
const handle = new KeyObjectHandle();
144+
const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate;
145+
if (!handle.initPqcRaw(name, keyData, keyType)) {
146+
throw lazyDOMException('Invalid keyData', 'DataError');
147+
}
148+
149+
return isPublic ? new PublicKeyObject(handle) : new PrivateKeyObject(handle);
150+
}
151+
152+
function mlKemImportKey(
153+
format,
154+
keyData,
155+
algorithm,
156+
extractable,
157+
keyUsages) {
158+
159+
const { name } = algorithm;
160+
let keyObject;
161+
const usagesSet = new SafeSet(keyUsages);
162+
switch (format) {
163+
case 'KeyObject': {
164+
verifyAcceptableMlKemKeyUse(name, keyData.type === 'public', usagesSet);
165+
keyObject = keyData;
166+
break;
167+
}
168+
case 'spki': {
169+
verifyAcceptableMlKemKeyUse(name, true, usagesSet);
170+
try {
171+
keyObject = createPublicKey({
172+
key: keyData,
173+
format: 'der',
174+
type: 'spki',
175+
});
176+
} catch (err) {
177+
throw lazyDOMException(
178+
'Invalid keyData', { name: 'DataError', cause: err });
179+
}
180+
break;
181+
}
182+
case 'pkcs8': {
183+
verifyAcceptableMlKemKeyUse(name, false, usagesSet);
184+
try {
185+
keyObject = createPrivateKey({
186+
key: keyData,
187+
format: 'der',
188+
type: 'pkcs8',
189+
});
190+
} catch (err) {
191+
throw lazyDOMException(
192+
'Invalid keyData', { name: 'DataError', cause: err });
193+
}
194+
break;
195+
}
196+
case 'raw-public':
197+
case 'raw-seed': {
198+
const isPublic = format === 'raw-public';
199+
verifyAcceptableMlKemKeyUse(name, isPublic, usagesSet);
200+
201+
try {
202+
keyObject = createMlKemRawKey(name, keyData, isPublic);
203+
} catch (err) {
204+
throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err });
205+
}
206+
break;
207+
}
208+
default:
209+
return undefined;
210+
}
211+
212+
if (keyObject.asymmetricKeyType !== name.toLowerCase()) {
213+
throw lazyDOMException('Invalid key type', 'DataError');
214+
}
215+
216+
return new InternalCryptoKey(
217+
keyObject,
218+
{ name },
219+
keyUsages,
220+
extractable);
221+
}
222+
223+
function mlKemEncapsulate(encapsulationKey) {
224+
if (encapsulationKey[kKeyType] !== 'public') {
225+
throw lazyDOMException(`Key must be a public key`, 'InvalidAccessError');
226+
}
227+
228+
const { promise, resolve, reject } = PromiseWithResolvers();
229+
230+
const job = new KEMEncapsulateJob(
231+
kCryptoJobAsync,
232+
encapsulationKey[kKeyObject][kHandle],
233+
undefined,
234+
undefined,
235+
undefined);
236+
237+
job.ondone = (error, result) => {
238+
if (error) {
239+
reject(lazyDOMException(
240+
'The operation failed for an operation-specific reason',
241+
{ name: 'OperationError', cause: error }));
242+
} else {
243+
const { 0: sharedKey, 1: ciphertext } = result;
244+
resolve({ sharedKey: sharedKey.buffer, ciphertext: ciphertext.buffer });
245+
}
246+
};
247+
job.run();
248+
249+
return promise;
250+
}
251+
252+
function mlKemDecapsulate(decapsulationKey, ciphertext) {
253+
if (decapsulationKey[kKeyType] !== 'private') {
254+
throw lazyDOMException(`Key must be a private key`, 'InvalidAccessError');
255+
}
256+
257+
const { promise, resolve, reject } = PromiseWithResolvers();
258+
259+
const job = new KEMDecapsulateJob(
260+
kCryptoJobAsync,
261+
decapsulationKey[kKeyObject][kHandle],
262+
undefined,
263+
undefined,
264+
undefined,
265+
ciphertext);
266+
267+
job.ondone = (error, result) => {
268+
if (error) {
269+
reject(lazyDOMException(
270+
'The operation failed for an operation-specific reason',
271+
{ name: 'OperationError', cause: error }));
272+
} else {
273+
resolve(result.buffer);
274+
}
275+
};
276+
job.run();
277+
278+
return promise;
279+
}
280+
281+
module.exports = {
282+
mlKemExportKey,
283+
mlKemImportKey,
284+
mlKemEncapsulate,
285+
mlKemDecapsulate,
286+
mlKemGenerateKey,
287+
};

0 commit comments

Comments
 (0)