Skip to content
Draft
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
18 changes: 12 additions & 6 deletions doc/api/webcrypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,9 @@ The algorithms currently supported include:
<!-- YAML
added: v15.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/60933
description: ML-KEM jwk format is now supported.
- version: v24.8.0
pr-url: https://github.com/nodejs/node/pull/59647
description: KMAC algorithms are now supported.
Expand Down Expand Up @@ -1170,9 +1173,9 @@ specification.
| `'ML-DSA-44'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ |
| `'ML-DSA-65'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ |
| `'ML-DSA-87'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ |
| `'ML-KEM-512'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'ML-KEM-768'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'ML-KEM-1024'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'ML-KEM-512'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'ML-KEM-768'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'ML-KEM-1024'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'RSA-OAEP'` | ✔ | ✔ | ✔ | | | | |
| `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | |
| `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | | | |
Expand Down Expand Up @@ -1260,6 +1263,9 @@ The {CryptoKey} (secret key) generating algorithms supported include:
<!-- YAML
added: v15.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/60933
description: ML-KEM jwk format is now supported.
- version: v24.8.0
pr-url: https://github.com/nodejs/node/pull/59647
description: KMAC algorithms are now supported.
Expand Down Expand Up @@ -1329,9 +1335,9 @@ The algorithms currently supported include:
| `'ML-DSA-44'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ |
| `'ML-DSA-65'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ |
| `'ML-DSA-87'`[^modern-algos] | ✔ | ✔ | ✔ | | | ✔ | ✔ |
| `'ML-KEM-512'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'ML-KEM-768'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'ML-KEM-1024'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'ML-KEM-512'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'ML-KEM-768'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'ML-KEM-1024'`[^modern-algos] | ✔ | ✔ | | | | ✔ | ✔ |
| `'PBKDF2'` | | | | ✔ | ✔ | | |
| `'RSA-OAEP'` | ✔ | ✔ | ✔ | | | | |
| `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | |
Expand Down
25 changes: 21 additions & 4 deletions lib/internal/crypto/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -539,11 +539,27 @@ function getKeyTypes(allowKeyObject, bufferOnly = false) {
return types;
}

function mlDsaPubLen(alg) {
function pqcPubLen(alg) {
switch (alg) {
case 'ML-DSA-44': return 1312;
case 'ML-DSA-65': return 1952;
case 'ML-DSA-87': return 2592;
case 'ML-KEM-512': return 800;
case 'ML-KEM-768': return 1184;
case 'ML-KEM-1024': return 1568;
}
}

function pqcSeedLen(alg) {
switch (alg) {
case 'ML-DSA-44':
case 'ML-DSA-65':
case 'ML-DSA-87':
return 32;
case 'ML-KEM-512':
case 'ML-KEM-768':
case 'ML-KEM-1024':
return 64;
}
}

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

if (key.kty === 'AKP') {
validateOneOf(
key.alg, 'key.alg', ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']);
key.alg, 'key.alg', ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87',
'ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']);
validateString(key.pub, 'key.pub');

let keyData;
if (isPublic) {
keyData = Buffer.from(key.pub, 'base64url');
if (keyData.byteLength !== mlDsaPubLen(key.alg)) {
if (keyData.byteLength !== pqcPubLen(key.alg)) {
throw new ERR_CRYPTO_INVALID_JWK();
}
} else {
validateString(key.priv, 'key.priv');
keyData = Buffer.from(key.priv, 'base64url');
if (keyData.byteLength !== 32) {
if (keyData.byteLength !== pqcSeedLen(key.alg)) {
throw new ERR_CRYPTO_INVALID_JWK();
}
}
Expand Down
66 changes: 66 additions & 0 deletions lib/internal/crypto/ml_kem.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const {
Uint8Array,
} = primordials;

const { Buffer } = require('buffer');

const {
kCryptoJobAsync,
KEMDecapsulateJob,
Expand All @@ -21,9 +23,16 @@ const {
kWebCryptoKeyFormatSPKI,
} = internalBinding('crypto');

const {
codes: {
ERR_CRYPTO_INVALID_JWK,
},
} = require('internal/errors');

const {
getUsagesUnion,
hasAnyNotIn,
validateKeyOps,
kHandle,
kKeyObject,
} = require('internal/crypto/util');
Expand Down Expand Up @@ -193,6 +202,63 @@ function mlKemImportKey(
}
break;
}
case 'jwk': {
if (!keyData.kty)
throw lazyDOMException('Invalid keyData', 'DataError');
if (keyData.kty !== 'AKP')
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
if (keyData.alg !== name)
throw lazyDOMException(
'JWK "alg" Parameter and algorithm name mismatch', 'DataError');
const isPublic = keyData.priv === undefined;

if (usagesSet.size > 0 && keyData.use !== undefined) {
if (keyData.use !== 'enc')
throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError');
}

validateKeyOps(keyData.key_ops, usagesSet);

if (keyData.ext !== undefined &&
keyData.ext === false &&
extractable === true) {
throw lazyDOMException(
'JWK "ext" Parameter and extractable mismatch',
'DataError');
}

if (!isPublic && typeof keyData.pub !== 'string') {
throw lazyDOMException('Invalid JWK', 'DataError');
}

verifyAcceptableMlKemKeyUse(
name,
isPublic,
usagesSet);

try {
const publicKeyObject = createMlKemRawKey(
name,
Buffer.from(keyData.pub, 'base64url'),
true);

if (isPublic) {
keyObject = publicKeyObject;
} else {
keyObject = createMlKemRawKey(
name,
Buffer.from(keyData.priv, 'base64url'),
false);

if (!createPublicKey(keyObject).equals(publicKeyObject)) {
throw new ERR_CRYPTO_INVALID_JWK();
}
}
} catch (err) {
throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err });
}
break;
}
case 'raw-public':
case 'raw-seed': {
const isPublic = format === 'raw-public';
Expand Down
6 changes: 6 additions & 0 deletions lib/internal/crypto/webcrypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,12 @@ async function exportKeyJWK(key) {
case 'ML-DSA-65':
// Fall through
case 'ML-DSA-87':
// Fall through
case 'ML-KEM-512':
// Fall through
case 'ML-KEM-768':
// Fall through
case 'ML-KEM-1024':
break;
case 'Ed25519':
// Fall through
Expand Down
4 changes: 2 additions & 2 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@
'src/crypto/crypto_cipher.cc',
'src/crypto/crypto_context.cc',
'src/crypto/crypto_ec.cc',
'src/crypto/crypto_ml_dsa.cc',
'src/crypto/crypto_pqc.cc',
'src/crypto/crypto_kem.cc',
'src/crypto/crypto_hmac.cc',
'src/crypto/crypto_kmac.cc',
Expand Down Expand Up @@ -408,7 +408,7 @@
'src/crypto/crypto_clienthello.h',
'src/crypto/crypto_context.h',
'src/crypto/crypto_ec.h',
'src/crypto/crypto_ml_dsa.h',
'src/crypto/crypto_pqc.h',
'src/crypto/crypto_hkdf.h',
'src/crypto/crypto_pbkdf2.h',
'src/crypto/crypto_sig.h',
Expand Down
10 changes: 8 additions & 2 deletions src/crypto/crypto_keys.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#include "crypto/crypto_dh.h"
#include "crypto/crypto_dsa.h"
#include "crypto/crypto_ec.h"
#include "crypto/crypto_ml_dsa.h"
#include "crypto/crypto_pqc.h"
#include "crypto/crypto_rsa.h"
#include "crypto/crypto_util.h"
#include "env-inl.h"
Expand Down Expand Up @@ -183,7 +183,13 @@ bool ExportJWKAsymmetricKey(Environment* env,
case EVP_PKEY_ML_DSA_65:
// Fall through
case EVP_PKEY_ML_DSA_87:
return ExportJwkMlDsaKey(env, key, target);
// Fall through
case EVP_PKEY_ML_KEM_512:
// Fall through
case EVP_PKEY_ML_KEM_768:
// Fall through
case EVP_PKEY_ML_KEM_1024:
return ExportJwkPqcKey(env, key, target);
#endif
}
THROW_ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE(env);
Expand Down
22 changes: 14 additions & 8 deletions src/crypto/crypto_ml_dsa.cc → src/crypto/crypto_pqc.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include "crypto/crypto_ml_dsa.h"
#include "crypto/crypto_pqc.h"
#include "crypto/crypto_util.h"
#include "env-inl.h"
#include "string_bytes.h"
Expand All @@ -15,35 +15,41 @@ using v8::Value;
namespace crypto {

#if OPENSSL_WITH_PQC
constexpr const char* GetMlDsaAlgorithmName(int id) {
constexpr const char* GetPqcAlgorithmName(int id) {
switch (id) {
case EVP_PKEY_ML_DSA_44:
return "ML-DSA-44";
case EVP_PKEY_ML_DSA_65:
return "ML-DSA-65";
case EVP_PKEY_ML_DSA_87:
return "ML-DSA-87";
case EVP_PKEY_ML_KEM_512:
return "ML-KEM-512";
case EVP_PKEY_ML_KEM_768:
return "ML-KEM-768";
case EVP_PKEY_ML_KEM_1024:
return "ML-KEM-1024";
default:
return nullptr;
}
}

/**
* Exports an ML-DSA key to JWK format.
* Exports a PQC key (ML-DSA or ML-KEM) to JWK format.
*
* The resulting JWK object contains:
* - "kty": "AKP" (Asymmetric Key Pair - required)
* - "alg": "ML-DSA-XX" (Algorithm identifier - required for "AKP")
* - "alg": "ML-(KEM|DSA)-..." (Algorithm identifier - required for "AKP")
* - "pub": "<Base64URL-encoded raw public key>" (required)
* - "priv": <"Base64URL-encoded raw seed>" (required for private keys only)
*/
bool ExportJwkMlDsaKey(Environment* env,
const KeyObjectData& key,
Local<Object> target) {
bool ExportJwkPqcKey(Environment* env,
const KeyObjectData& key,
Local<Object> target) {
Mutex::ScopedLock lock(key.mutex());
const auto& pkey = key.GetAsymmetricKey();

const char* alg = GetMlDsaAlgorithmName(pkey.id());
const char* alg = GetPqcAlgorithmName(pkey.id());
CHECK(alg);

static constexpr auto trySetKey = [](Environment* env,
Expand Down
12 changes: 6 additions & 6 deletions src/crypto/crypto_ml_dsa.h → src/crypto/crypto_pqc.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#ifndef SRC_CRYPTO_CRYPTO_ML_DSA_H_
#define SRC_CRYPTO_CRYPTO_ML_DSA_H_
#ifndef SRC_CRYPTO_CRYPTO_PQC_H_
#define SRC_CRYPTO_CRYPTO_PQC_H_

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

Expand All @@ -10,12 +10,12 @@
namespace node {
namespace crypto {
#if OPENSSL_WITH_PQC
bool ExportJwkMlDsaKey(Environment* env,
const KeyObjectData& key,
v8::Local<v8::Object> target);
bool ExportJwkPqcKey(Environment* env,
const KeyObjectData& key,
v8::Local<v8::Object> target);
#endif
} // namespace crypto
} // namespace node

#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_CRYPTO_CRYPTO_ML_DSA_H_
#endif // SRC_CRYPTO_CRYPTO_PQC_H_
2 changes: 1 addition & 1 deletion src/node_crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@
#endif
#include "crypto/crypto_keygen.h"
#include "crypto/crypto_keys.h"
#include "crypto/crypto_ml_dsa.h"
#include "crypto/crypto_pbkdf2.h"
#include "crypto/crypto_pqc.h"
#include "crypto/crypto_random.h"
#include "crypto/crypto_rsa.h"
#include "crypto/crypto_scrypt.h"
Expand Down
11 changes: 4 additions & 7 deletions test/parallel/test-crypto-encap-decap.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,10 @@ for (const [name, { supported, publicKey, privateKey, sharedSecretLength, cipher
},
];

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

for (const kp of keyPairs) {
// sync
Expand Down
Loading