crypto: support ML-DSA in Web Cryptography · nodejs/node@a553822
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+};