Skip to content

Commit 8a6aeb9

Browse files
committed
crypto: support ML-KEM JWK key format
1 parent 8cfcf52 commit 8a6aeb9

File tree

12 files changed

+362
-36
lines changed

12 files changed

+362
-36
lines changed

lib/internal/crypto/keys.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -539,11 +539,27 @@ function getKeyTypes(allowKeyObject, bufferOnly = false) {
539539
return types;
540540
}
541541

542-
function mlDsaPubLen(alg) {
542+
function pqcPubLen(alg) {
543543
switch (alg) {
544544
case 'ML-DSA-44': return 1312;
545545
case 'ML-DSA-65': return 1952;
546546
case 'ML-DSA-87': return 2592;
547+
case 'ML-KEM-512': return 800;
548+
case 'ML-KEM-768': return 1184;
549+
case 'ML-KEM-1024': return 1568;
550+
}
551+
}
552+
553+
function pqcSeedLen(alg) {
554+
switch (alg) {
555+
case 'ML-DSA-44':
556+
case 'ML-DSA-65':
557+
case 'ML-DSA-87':
558+
return 32;
559+
case 'ML-KEM-512':
560+
case 'ML-KEM-768':
561+
case 'ML-KEM-1024':
562+
return 64;
547563
}
548564
}
549565

@@ -560,19 +576,20 @@ function getKeyObjectHandleFromJwk(key, ctx) {
560576

561577
if (key.kty === 'AKP') {
562578
validateOneOf(
563-
key.alg, 'key.alg', ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']);
579+
key.alg, 'key.alg', ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87',
580+
'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']);
564581
validateString(key.pub, 'key.pub');
565582

566583
let keyData;
567584
if (isPublic) {
568585
keyData = Buffer.from(key.pub, 'base64url');
569-
if (keyData.byteLength !== mlDsaPubLen(key.alg)) {
586+
if (keyData.byteLength !== pqcPubLen(key.alg)) {
570587
throw new ERR_CRYPTO_INVALID_JWK();
571588
}
572589
} else {
573590
validateString(key.priv, 'key.priv');
574591
keyData = Buffer.from(key.priv, 'base64url');
575-
if (keyData.byteLength !== 32) {
592+
if (keyData.byteLength !== pqcSeedLen(key.alg)) {
576593
throw new ERR_CRYPTO_INVALID_JWK();
577594
}
578595
}

lib/internal/crypto/ml_kem.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const {
88
Uint8Array,
99
} = primordials;
1010

11+
const { Buffer } = require('buffer');
12+
1113
const {
1214
kCryptoJobAsync,
1315
KEMDecapsulateJob,
@@ -21,9 +23,16 @@ const {
2123
kWebCryptoKeyFormatSPKI,
2224
} = internalBinding('crypto');
2325

26+
const {
27+
codes: {
28+
ERR_CRYPTO_INVALID_JWK,
29+
},
30+
} = require('internal/errors');
31+
2432
const {
2533
getUsagesUnion,
2634
hasAnyNotIn,
35+
validateKeyOps,
2736
kHandle,
2837
kKeyObject,
2938
} = require('internal/crypto/util');
@@ -193,6 +202,63 @@ function mlKemImportKey(
193202
}
194203
break;
195204
}
205+
case 'jwk': {
206+
if (!keyData.kty)
207+
throw lazyDOMException('Invalid keyData', 'DataError');
208+
if (keyData.kty !== 'AKP')
209+
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
210+
if (keyData.alg !== name)
211+
throw lazyDOMException(
212+
'JWK "alg" Parameter and algorithm name mismatch', 'DataError');
213+
const isPublic = keyData.priv === undefined;
214+
215+
if (usagesSet.size > 0 && keyData.use !== undefined) {
216+
if (keyData.use !== 'enc')
217+
throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError');
218+
}
219+
220+
validateKeyOps(keyData.key_ops, usagesSet);
221+
222+
if (keyData.ext !== undefined &&
223+
keyData.ext === false &&
224+
extractable === true) {
225+
throw lazyDOMException(
226+
'JWK "ext" Parameter and extractable mismatch',
227+
'DataError');
228+
}
229+
230+
if (!isPublic && typeof keyData.pub !== 'string') {
231+
throw lazyDOMException('Invalid JWK', 'DataError');
232+
}
233+
234+
verifyAcceptableMlKemKeyUse(
235+
name,
236+
isPublic,
237+
usagesSet);
238+
239+
try {
240+
const publicKeyObject = createMlKemRawKey(
241+
name,
242+
Buffer.from(keyData.pub, 'base64url'),
243+
true);
244+
245+
if (isPublic) {
246+
keyObject = publicKeyObject;
247+
} else {
248+
keyObject = createMlKemRawKey(
249+
name,
250+
Buffer.from(keyData.priv, 'base64url'),
251+
false);
252+
253+
if (!createPublicKey(keyObject).equals(publicKeyObject)) {
254+
throw new ERR_CRYPTO_INVALID_JWK();
255+
}
256+
}
257+
} catch (err) {
258+
throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err });
259+
}
260+
break;
261+
}
196262
case 'raw-public':
197263
case 'raw-seed': {
198264
const isPublic = format === 'raw-public';

lib/internal/crypto/webcrypto.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,12 @@ async function exportKeyJWK(key) {
611611
case 'ML-DSA-65':
612612
// Fall through
613613
case 'ML-DSA-87':
614+
// Fall through
615+
case 'ML-KEM-512':
616+
// Fall through
617+
case 'ML-KEM-768':
618+
// Fall through
619+
case 'ML-KEM-1024':
614620
break;
615621
case 'Ed25519':
616622
// Fall through

node.gyp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@
372372
'src/crypto/crypto_cipher.cc',
373373
'src/crypto/crypto_context.cc',
374374
'src/crypto/crypto_ec.cc',
375-
'src/crypto/crypto_ml_dsa.cc',
375+
'src/crypto/crypto_pqc.cc',
376376
'src/crypto/crypto_kem.cc',
377377
'src/crypto/crypto_hmac.cc',
378378
'src/crypto/crypto_kmac.cc',
@@ -408,7 +408,7 @@
408408
'src/crypto/crypto_clienthello.h',
409409
'src/crypto/crypto_context.h',
410410
'src/crypto/crypto_ec.h',
411-
'src/crypto/crypto_ml_dsa.h',
411+
'src/crypto/crypto_pqc.h',
412412
'src/crypto/crypto_hkdf.h',
413413
'src/crypto/crypto_pbkdf2.h',
414414
'src/crypto/crypto_sig.h',

src/crypto/crypto_keys.cc

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#include "crypto/crypto_dh.h"
66
#include "crypto/crypto_dsa.h"
77
#include "crypto/crypto_ec.h"
8-
#include "crypto/crypto_ml_dsa.h"
8+
#include "crypto/crypto_pqc.h"
99
#include "crypto/crypto_rsa.h"
1010
#include "crypto/crypto_util.h"
1111
#include "env-inl.h"
@@ -183,7 +183,13 @@ bool ExportJWKAsymmetricKey(Environment* env,
183183
case EVP_PKEY_ML_DSA_65:
184184
// Fall through
185185
case EVP_PKEY_ML_DSA_87:
186-
return ExportJwkMlDsaKey(env, key, target);
186+
// Fall through
187+
case EVP_PKEY_ML_KEM_512:
188+
// Fall through
189+
case EVP_PKEY_ML_KEM_768:
190+
// Fall through
191+
case EVP_PKEY_ML_KEM_1024:
192+
return ExportJwkPqcKey(env, key, target);
187193
#endif
188194
}
189195
THROW_ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE(env);
Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#include "crypto/crypto_ml_dsa.h"
1+
#include "crypto/crypto_pqc.h"
22
#include "crypto/crypto_util.h"
33
#include "env-inl.h"
44
#include "string_bytes.h"
@@ -15,35 +15,41 @@ using v8::Value;
1515
namespace crypto {
1616

1717
#if OPENSSL_WITH_PQC
18-
constexpr const char* GetMlDsaAlgorithmName(int id) {
18+
constexpr const char* GetPqcAlgorithmName(int id) {
1919
switch (id) {
2020
case EVP_PKEY_ML_DSA_44:
2121
return "ML-DSA-44";
2222
case EVP_PKEY_ML_DSA_65:
2323
return "ML-DSA-65";
2424
case EVP_PKEY_ML_DSA_87:
2525
return "ML-DSA-87";
26+
case EVP_PKEY_ML_KEM_512:
27+
return "ML-KEM-512";
28+
case EVP_PKEY_ML_KEM_768:
29+
return "ML-KEM-768";
30+
case EVP_PKEY_ML_KEM_1024:
31+
return "ML-KEM-1024";
2632
default:
2733
return nullptr;
2834
}
2935
}
3036

3137
/**
32-
* Exports an ML-DSA key to JWK format.
38+
* Exports a PQC key (ML-DSA or ML-KEM) to JWK format.
3339
*
3440
* The resulting JWK object contains:
3541
* - "kty": "AKP" (Asymmetric Key Pair - required)
36-
* - "alg": "ML-DSA-XX" (Algorithm identifier - required for "AKP")
42+
* - "alg": "ML-(KEM|DSA)-..." (Algorithm identifier - required for "AKP")
3743
* - "pub": "<Base64URL-encoded raw public key>" (required)
3844
* - "priv": <"Base64URL-encoded raw seed>" (required for private keys only)
3945
*/
40-
bool ExportJwkMlDsaKey(Environment* env,
41-
const KeyObjectData& key,
42-
Local<Object> target) {
46+
bool ExportJwkPqcKey(Environment* env,
47+
const KeyObjectData& key,
48+
Local<Object> target) {
4349
Mutex::ScopedLock lock(key.mutex());
4450
const auto& pkey = key.GetAsymmetricKey();
4551

46-
const char* alg = GetMlDsaAlgorithmName(pkey.id());
52+
const char* alg = GetPqcAlgorithmName(pkey.id());
4753
CHECK(alg);
4854

4955
static constexpr auto trySetKey = [](Environment* env,
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
#ifndef SRC_CRYPTO_CRYPTO_ML_DSA_H_
2-
#define SRC_CRYPTO_CRYPTO_ML_DSA_H_
1+
#ifndef SRC_CRYPTO_CRYPTO_PQC_H_
2+
#define SRC_CRYPTO_CRYPTO_PQC_H_
33

44
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
55

@@ -10,12 +10,12 @@
1010
namespace node {
1111
namespace crypto {
1212
#if OPENSSL_WITH_PQC
13-
bool ExportJwkMlDsaKey(Environment* env,
14-
const KeyObjectData& key,
15-
v8::Local<v8::Object> target);
13+
bool ExportJwkPqcKey(Environment* env,
14+
const KeyObjectData& key,
15+
v8::Local<v8::Object> target);
1616
#endif
1717
} // namespace crypto
1818
} // namespace node
1919

2020
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
21-
#endif // SRC_CRYPTO_CRYPTO_ML_DSA_H_
21+
#endif // SRC_CRYPTO_CRYPTO_PQC_H_

src/node_crypto.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@
4646
#endif
4747
#include "crypto/crypto_keygen.h"
4848
#include "crypto/crypto_keys.h"
49-
#include "crypto/crypto_ml_dsa.h"
5049
#include "crypto/crypto_pbkdf2.h"
50+
#include "crypto/crypto_pqc.h"
5151
#include "crypto/crypto_random.h"
5252
#include "crypto/crypto_rsa.h"
5353
#include "crypto/crypto_scrypt.h"

test/parallel/test-crypto-encap-decap.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,10 @@ for (const [name, { supported, publicKey, privateKey, sharedSecretLength, cipher
128128
},
129129
];
130130

131-
// TODO(@panva): ML-KEM does not have a JWK format defined yet, add once standardized
132-
if (!keyObjects.privateKey.asymmetricKeyType.startsWith('ml')) {
133-
keyPairs.push({
134-
publicKey: formatKeyAs(keyObjects.publicKey, { format: 'jwk' }),
135-
privateKey: formatKeyAs(keyObjects.privateKey, { format: 'jwk' })
136-
});
137-
}
131+
keyPairs.push({
132+
publicKey: formatKeyAs(keyObjects.publicKey, { format: 'jwk' }),
133+
privateKey: formatKeyAs(keyObjects.privateKey, { format: 'jwk' })
134+
});
138135

139136
for (const kp of keyPairs) {
140137
// sync

0 commit comments

Comments
 (0)