Skip to content

Commit a553822

Browse files
panvatargos
authored andcommitted
crypto: support ML-DSA in Web Cryptography
PR-URL: #59365 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Ethan Arrowood <[email protected]> Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: Joyee Cheung <[email protected]>
1 parent b68e0d1 commit a553822

14 files changed

+1348
-144
lines changed

doc/api/webcrypto.md

Lines changed: 233 additions & 122 deletions
Large diffs are not rendered by default.

lib/internal/crypto/keys.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,14 @@ const {
293293
result = require('internal/crypto/cfrg')
294294
.cfrgImportKey('KeyObject', this, algorithm, extractable, keyUsages);
295295
break;
296+
case 'ML-DSA-44':
297+
// Fall through
298+
case 'ML-DSA-65':
299+
// Fall through
300+
case 'ML-DSA-87':
301+
result = require('internal/crypto/ml_dsa')
302+
.mlDsaImportKey('KeyObject', this, algorithm, extractable, keyUsages);
303+
break;
296304
default:
297305
throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError');
298306
}

lib/internal/crypto/ml_dsa.js

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
'use strict';
2+
3+
const {
4+
SafeSet,
5+
} = primordials;
6+
7+
const { Buffer } = require('buffer');
8+
9+
const {
10+
KeyObjectHandle,
11+
SignJob,
12+
kCryptoJobAsync,
13+
kKeyTypePrivate,
14+
kKeyTypePublic,
15+
kSignJobModeSign,
16+
kSignJobModeVerify,
17+
} = internalBinding('crypto');
18+
19+
const {
20+
codes: {
21+
ERR_CRYPTO_INVALID_JWK,
22+
},
23+
} = require('internal/errors');
24+
25+
const {
26+
getUsagesUnion,
27+
hasAnyNotIn,
28+
jobPromise,
29+
validateKeyOps,
30+
kHandle,
31+
kKeyObject,
32+
} = require('internal/crypto/util');
33+
34+
const {
35+
lazyDOMException,
36+
promisify,
37+
} = require('internal/util');
38+
39+
const {
40+
generateKeyPair: _generateKeyPair,
41+
} = require('internal/crypto/keygen');
42+
43+
const {
44+
InternalCryptoKey,
45+
PrivateKeyObject,
46+
PublicKeyObject,
47+
createPublicKey,
48+
} = require('internal/crypto/keys');
49+
50+
const generateKeyPair = promisify(_generateKeyPair);
51+
52+
function verifyAcceptableMlDsaKeyUse(name, isPublic, usages) {
53+
const checkSet = isPublic ? ['verify'] : ['sign'];
54+
if (hasAnyNotIn(usages, checkSet)) {
55+
throw lazyDOMException(
56+
`Unsupported key usage for a ${name} key`,
57+
'SyntaxError');
58+
}
59+
}
60+
61+
function createMlDsaRawKey(name, keyData, isPublic) {
62+
const handle = new KeyObjectHandle();
63+
const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate;
64+
if (!handle.initMlDsaRaw(name, keyData, keyType)) {
65+
throw lazyDOMException('Invalid keyData', 'DataError');
66+
}
67+
68+
return isPublic ? new PublicKeyObject(handle) : new PrivateKeyObject(handle);
69+
}
70+
71+
async function mlDsaGenerateKey(algorithm, extractable, keyUsages) {
72+
const { name } = algorithm;
73+
74+
const usageSet = new SafeSet(keyUsages);
75+
if (hasAnyNotIn(usageSet, ['sign', 'verify'])) {
76+
throw lazyDOMException(
77+
`Unsupported key usage for an ${name} key`,
78+
'SyntaxError');
79+
}
80+
81+
const keyPair = await generateKeyPair(name.toLowerCase()).catch((err) => {
82+
throw lazyDOMException(
83+
'The operation failed for an operation-specific reason',
84+
{ name: 'OperationError', cause: err });
85+
});
86+
87+
const publicUsages = getUsagesUnion(usageSet, 'verify');
88+
const privateUsages = getUsagesUnion(usageSet, 'sign');
89+
90+
const keyAlgorithm = { name };
91+
92+
const publicKey =
93+
new InternalCryptoKey(
94+
keyPair.publicKey,
95+
keyAlgorithm,
96+
publicUsages,
97+
true);
98+
99+
const privateKey =
100+
new InternalCryptoKey(
101+
keyPair.privateKey,
102+
keyAlgorithm,
103+
privateUsages,
104+
extractable);
105+
106+
return { __proto__: null, privateKey, publicKey };
107+
}
108+
109+
function mlDsaExportKey(key) {
110+
try {
111+
if (key.type === 'private') {
112+
const { priv } = key[kKeyObject][kHandle].exportJwk({}, false);
113+
return Buffer.alloc(32, priv, 'base64url').buffer;
114+
}
115+
116+
const { pub } = key[kKeyObject][kHandle].exportJwk({}, false);
117+
return Buffer.alloc(Buffer.byteLength(pub, 'base64url'), pub, 'base64url').buffer;
118+
} catch (err) {
119+
throw lazyDOMException(
120+
'The operation failed for an operation-specific reason',
121+
{ name: 'OperationError', cause: err });
122+
}
123+
}
124+
125+
function mlDsaImportKey(
126+
format,
127+
keyData,
128+
algorithm,
129+
extractable,
130+
keyUsages) {
131+
132+
const { name } = algorithm;
133+
let keyObject;
134+
const usagesSet = new SafeSet(keyUsages);
135+
switch (format) {
136+
case 'KeyObject': {
137+
verifyAcceptableMlDsaKeyUse(name, keyData.type === 'public', usagesSet);
138+
keyObject = keyData;
139+
break;
140+
}
141+
case 'jwk': {
142+
if (!keyData.kty)
143+
throw lazyDOMException('Invalid keyData', 'DataError');
144+
if (keyData.kty !== 'AKP')
145+
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
146+
if (keyData.alg !== name)
147+
throw lazyDOMException(
148+
'JWK "alg" Parameter and algorithm name mismatch', 'DataError');
149+
const isPublic = keyData.priv === undefined;
150+
151+
if (usagesSet.size > 0 && keyData.use !== undefined) {
152+
if (keyData.use !== 'sig')
153+
throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError');
154+
}
155+
156+
validateKeyOps(keyData.key_ops, usagesSet);
157+
158+
if (keyData.ext !== undefined &&
159+
keyData.ext === false &&
160+
extractable === true) {
161+
throw lazyDOMException(
162+
'JWK "ext" Parameter and extractable mismatch',
163+
'DataError');
164+
}
165+
166+
if (!isPublic && typeof keyData.pub !== 'string') {
167+
throw lazyDOMException('Invalid JWK', 'DataError');
168+
}
169+
170+
verifyAcceptableMlDsaKeyUse(
171+
name,
172+
isPublic,
173+
usagesSet);
174+
175+
try {
176+
const publicKeyObject = createMlDsaRawKey(
177+
name,
178+
Buffer.from(keyData.pub, 'base64url'),
179+
true);
180+
181+
if (isPublic) {
182+
keyObject = publicKeyObject;
183+
} else {
184+
keyObject = createMlDsaRawKey(
185+
name,
186+
Buffer.from(keyData.priv, 'base64url'),
187+
false);
188+
189+
if (!createPublicKey(keyObject).equals(publicKeyObject)) {
190+
throw new ERR_CRYPTO_INVALID_JWK();
191+
}
192+
}
193+
} catch (err) {
194+
throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err });
195+
}
196+
break;
197+
}
198+
case 'raw-public':
199+
case 'raw-seed': {
200+
const isPublic = format === 'raw-public';
201+
verifyAcceptableMlDsaKeyUse(name, isPublic, usagesSet);
202+
203+
try {
204+
keyObject = createMlDsaRawKey(name, keyData, isPublic);
205+
} catch (err) {
206+
throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err });
207+
}
208+
break;
209+
}
210+
default:
211+
return undefined;
212+
}
213+
214+
if (keyObject.asymmetricKeyType !== name.toLowerCase()) {
215+
throw lazyDOMException('Invalid key type', 'DataError');
216+
}
217+
218+
return new InternalCryptoKey(
219+
keyObject,
220+
{ name },
221+
keyUsages,
222+
extractable);
223+
}
224+
225+
function mlDsaSignVerify(key, data, algorithm, signature) {
226+
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
227+
const type = mode === kSignJobModeSign ? 'private' : 'public';
228+
229+
if (key.type !== type)
230+
throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError');
231+
232+
return jobPromise(() => new SignJob(
233+
kCryptoJobAsync,
234+
mode,
235+
key[kKeyObject][kHandle],
236+
undefined,
237+
undefined,
238+
undefined,
239+
data,
240+
undefined,
241+
undefined,
242+
undefined,
243+
undefined,
244+
signature));
245+
}
246+
247+
module.exports = {
248+
mlDsaExportKey,
249+
mlDsaImportKey,
250+
mlDsaGenerateKey,
251+
mlDsaSignVerify,
252+
};

lib/internal/crypto/util.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ const {
3333
secureHeapUsed: _secureHeapUsed,
3434
getCachedAliases,
3535
getOpenSSLSecLevelCrypto: getOpenSSLSecLevel,
36+
EVP_PKEY_ML_DSA_44,
37+
EVP_PKEY_ML_DSA_65,
38+
EVP_PKEY_ML_DSA_87,
3639
} = internalBinding('crypto');
3740

3841
const { getOptionValue } = require('internal/options');
@@ -284,6 +287,22 @@ const experimentalAlgorithms = ObjectEntries({
284287
},
285288
});
286289

290+
for (const { 0: algorithm, 1: nid } of [
291+
['ML-DSA-44', EVP_PKEY_ML_DSA_44],
292+
['ML-DSA-65', EVP_PKEY_ML_DSA_65],
293+
['ML-DSA-87', EVP_PKEY_ML_DSA_87],
294+
]) {
295+
if (nid) {
296+
ArrayPrototypePush(experimentalAlgorithms, [algorithm, {
297+
generateKey: null,
298+
sign: 'ContextParams',
299+
verify: 'ContextParams',
300+
importKey: null,
301+
exportKey: null,
302+
}]);
303+
}
304+
}
305+
287306
for (let i = 0; i < experimentalAlgorithms.length; i++) {
288307
const name = experimentalAlgorithms[i][0];
289308
const ops = ObjectEntries(experimentalAlgorithms[i][1]);
@@ -314,6 +333,7 @@ const simpleAlgorithmDictionaries = {
314333
info: 'BufferSource',
315334
},
316335
Ed448Params: { context: 'BufferSource' },
336+
ContextParams: { context: 'BufferSource' },
317337
Pbkdf2Params: { hash: 'HashAlgorithmIdentifier', salt: 'BufferSource' },
318338
RsaOaepParams: { label: 'BufferSource' },
319339
RsaHashedImportParams: { hash: 'HashAlgorithmIdentifier' },

0 commit comments

Comments
 (0)