crypto: support ML-KEM, DHKEM, and RSASVE key encapsulation mechanisms

PR-URL: https://github.com/nodejs/node/pull/59491
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
This commit is contained in:
Filip Skokan 2025-08-20 16:30:58 +02:00 committed by GitHub
parent 70690be494
commit f8d68d30ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1098 additions and 0 deletions

140
benchmark/crypto/kem.js Normal file
View File

@ -0,0 +1,140 @@
'use strict';
const common = require('../common.js');
const { hasOpenSSL } = require('../../test/common/crypto.js');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const fixtures_keydir = path.resolve(__dirname, '../../test/fixtures/keys/');
function readKey(name) {
return fs.readFileSync(`${fixtures_keydir}/${name}.pem`, 'utf8');
}
const keyFixtures = {};
if (hasOpenSSL(3, 5)) {
keyFixtures['ml-kem-512'] = readKey('ml_kem_512_private');
keyFixtures['ml-kem-768'] = readKey('ml_kem_768_private');
keyFixtures['ml-kem-1024'] = readKey('ml_kem_1024_private');
}
if (hasOpenSSL(3, 2)) {
keyFixtures['p-256'] = readKey('ec_p256_private');
keyFixtures['p-384'] = readKey('ec_p384_private');
keyFixtures['p-521'] = readKey('ec_p521_private');
keyFixtures.x25519 = readKey('x25519_private');
keyFixtures.x448 = readKey('x448_private');
}
if (hasOpenSSL(3, 0)) {
keyFixtures.rsa = readKey('rsa_private_2048');
}
if (Object.keys(keyFixtures).length === 0) {
console.log('no supported key types available for this OpenSSL version');
process.exit(0);
}
const bench = common.createBenchmark(main, {
keyType: Object.keys(keyFixtures),
mode: ['sync', 'async', 'async-parallel'],
keyFormat: ['keyObject', 'keyObject.unique'],
op: ['encapsulate', 'decapsulate'],
n: [1e3],
}, {
combinationFilter(p) {
// "keyObject.unique" allows to compare the result with "keyObject" to
// assess whether mutexes over the key material impact the operation
return p.keyFormat !== 'keyObject.unique' ||
(p.keyFormat === 'keyObject.unique' && p.mode === 'async-parallel');
},
});
function measureSync(n, op, privateKey, keys, ciphertexts) {
bench.start();
for (let i = 0; i < n; ++i) {
const key = privateKey || keys[i];
if (op === 'encapsulate') {
crypto.encapsulate(key);
} else {
crypto.decapsulate(key, ciphertexts[i]);
}
}
bench.end(n);
}
function measureAsync(n, op, privateKey, keys, ciphertexts) {
let remaining = n;
function done() {
if (--remaining === 0)
bench.end(n);
else
one();
}
function one() {
const key = privateKey || keys[n - remaining];
if (op === 'encapsulate') {
crypto.encapsulate(key, done);
} else {
crypto.decapsulate(key, ciphertexts[n - remaining], done);
}
}
bench.start();
one();
}
function measureAsyncParallel(n, op, privateKey, keys, ciphertexts) {
let remaining = n;
function done() {
if (--remaining === 0)
bench.end(n);
}
bench.start();
for (let i = 0; i < n; ++i) {
const key = privateKey || keys[i];
if (op === 'encapsulate') {
crypto.encapsulate(key, done);
} else {
crypto.decapsulate(key, ciphertexts[i], done);
}
}
}
function main({ n, mode, keyFormat, keyType, op }) {
const pems = [...Buffer.alloc(n)].map(() => keyFixtures[keyType]);
const keyObjects = pems.map(crypto.createPrivateKey);
let privateKey, keys, ciphertexts;
switch (keyFormat) {
case 'keyObject':
privateKey = keyObjects[0];
break;
case 'keyObject.unique':
keys = keyObjects;
break;
default:
throw new Error('not implemented');
}
// Pre-generate ciphertexts for decapsulate operations
if (op === 'decapsulate') {
if (privateKey) {
ciphertexts = [...Buffer.alloc(n)].map(() => crypto.encapsulate(privateKey).ciphertext);
} else {
ciphertexts = keys.map((key) => crypto.encapsulate(key).ciphertext);
}
}
switch (mode) {
case 'sync':
measureSync(n, op, privateKey, keys, ciphertexts);
break;
case 'async':
measureAsync(n, op, privateKey, keys, ciphertexts);
break;
case 'async-parallel':
measureAsyncParallel(n, op, privateKey, keys, ciphertexts);
break;
}
}

View File

@ -4510,4 +4510,125 @@ const Digest Digest::FromName(const char* name) {
return ncrypto::getDigestByName(name); return ncrypto::getDigestByName(name);
} }
// ============================================================================
// KEM Implementation
#if OPENSSL_VERSION_MAJOR >= 3
#if !OPENSSL_VERSION_PREREQ(3, 5)
bool KEM::SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key) {
const char* operation = nullptr;
switch (EVP_PKEY_id(key.get())) {
case EVP_PKEY_RSA:
operation = OSSL_KEM_PARAM_OPERATION_RSASVE;
break;
#if OPENSSL_VERSION_PREREQ(3, 2)
case EVP_PKEY_EC:
case EVP_PKEY_X25519:
case EVP_PKEY_X448:
operation = OSSL_KEM_PARAM_OPERATION_DHKEM;
break;
#endif
default:
unreachable();
}
if (operation != nullptr) {
OSSL_PARAM params[] = {
OSSL_PARAM_utf8_string(
OSSL_KEM_PARAM_OPERATION, const_cast<char*>(operation), 0),
OSSL_PARAM_END};
if (EVP_PKEY_CTX_set_params(ctx, params) <= 0) {
return false;
}
}
return true;
}
#endif
std::optional<KEM::EncapsulateResult> KEM::Encapsulate(
const EVPKeyPointer& public_key) {
ClearErrorOnReturn clear_error_on_return;
auto ctx = public_key.newCtx();
if (!ctx) return std::nullopt;
if (EVP_PKEY_encapsulate_init(ctx.get(), nullptr) <= 0) {
return std::nullopt;
}
#if !OPENSSL_VERSION_PREREQ(3, 5)
if (!SetOperationParameter(ctx.get(), public_key)) {
return std::nullopt;
}
#endif
// Determine output buffer sizes
size_t ciphertext_len = 0;
size_t shared_key_len = 0;
if (EVP_PKEY_encapsulate(
ctx.get(), nullptr, &ciphertext_len, nullptr, &shared_key_len) <= 0) {
return std::nullopt;
}
auto ciphertext = DataPointer::Alloc(ciphertext_len);
auto shared_key = DataPointer::Alloc(shared_key_len);
if (!ciphertext || !shared_key) return std::nullopt;
if (EVP_PKEY_encapsulate(ctx.get(),
static_cast<unsigned char*>(ciphertext.get()),
&ciphertext_len,
static_cast<unsigned char*>(shared_key.get()),
&shared_key_len) <= 0) {
return std::nullopt;
}
return EncapsulateResult(std::move(ciphertext), std::move(shared_key));
}
DataPointer KEM::Decapsulate(const EVPKeyPointer& private_key,
const Buffer<const void>& ciphertext) {
ClearErrorOnReturn clear_error_on_return;
auto ctx = private_key.newCtx();
if (!ctx) return {};
if (EVP_PKEY_decapsulate_init(ctx.get(), nullptr) <= 0) {
return {};
}
#if !OPENSSL_VERSION_PREREQ(3, 5)
if (!SetOperationParameter(ctx.get(), private_key)) {
return {};
}
#endif
// First pass: determine shared secret size
size_t shared_key_len = 0;
if (EVP_PKEY_decapsulate(ctx.get(),
nullptr,
&shared_key_len,
static_cast<const unsigned char*>(ciphertext.data),
ciphertext.len) <= 0) {
return {};
}
auto shared_key = DataPointer::Alloc(shared_key_len);
if (!shared_key) return {};
if (EVP_PKEY_decapsulate(ctx.get(),
static_cast<unsigned char*>(shared_key.get()),
&shared_key_len,
static_cast<const unsigned char*>(ciphertext.data),
ciphertext.len) <= 0) {
return {};
}
return shared_key;
}
#endif // OPENSSL_VERSION_MAJOR >= 3
} // namespace ncrypto } // namespace ncrypto

View File

@ -1574,6 +1574,40 @@ DataPointer argon2(const Buffer<const char>& pass,
#endif #endif
#endif #endif
// ============================================================================
// KEM (Key Encapsulation Mechanism)
#if OPENSSL_VERSION_MAJOR >= 3
class KEM final {
public:
struct EncapsulateResult {
DataPointer ciphertext;
DataPointer shared_key;
EncapsulateResult() = default;
EncapsulateResult(DataPointer ct, DataPointer sk)
: ciphertext(std::move(ct)), shared_key(std::move(sk)) {}
};
// Encapsulate a shared secret using KEM with a public key.
// Returns both the ciphertext and shared secret.
static std::optional<EncapsulateResult> Encapsulate(
const EVPKeyPointer& public_key);
// Decapsulate a shared secret using KEM with a private key and ciphertext.
// Returns the shared secret.
static DataPointer Decapsulate(const EVPKeyPointer& private_key,
const Buffer<const void>& ciphertext);
private:
#if !OPENSSL_VERSION_PREREQ(3, 5)
static bool SetOperationParameter(EVP_PKEY_CTX* ctx,
const EVPKeyPointer& key);
#endif
};
#endif // OPENSSL_VERSION_MAJOR >= 3
// ============================================================================ // ============================================================================
// Version metadata // Version metadata
#define NCRYPTO_VERSION "0.0.1" #define NCRYPTO_VERSION "0.0.1"

View File

@ -3741,6 +3741,40 @@ the corresponding digest algorithm. This does not work for all signature
algorithms, such as `'ecdsa-with-SHA256'`, so it is best to always use digest algorithms, such as `'ecdsa-with-SHA256'`, so it is best to always use digest
algorithm names. algorithm names.
### `crypto.decapsulate(key, ciphertext[, callback])`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.2 - Release candidate
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject} Private Key
* `ciphertext` {ArrayBuffer|Buffer|TypedArray|DataView}
* `callback` {Function}
* `err` {Error}
* `sharedKey` {Buffer}
* Returns: {Buffer} if the `callback` function is not provided.
<!--lint enable maximum-line-length remark-lint-->
Key decapsulation using a KEM algorithm with a private key.
Supported key types and their KEM algorithms are:
* `'rsa'`[^openssl30] RSA Secret Value Encapsulation
* `'ec'`[^openssl32] DHKEM(P-256, HKDF-SHA256), DHKEM(P-384, HKDF-SHA256), DHKEM(P-521, HKDF-SHA256)
* `'x25519'`[^openssl32] DHKEM(X25519, HKDF-SHA256)
* `'x448'`[^openssl32] DHKEM(X448, HKDF-SHA512)
* `'ml-kem-512'`[^openssl35] ML-KEM
* `'ml-kem-768'`[^openssl35] ML-KEM
* `'ml-kem-1024'`[^openssl35] ML-KEM
If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
passed to [`crypto.createPrivateKey()`][].
If the `callback` function is provided this function uses libuv's threadpool.
### `crypto.diffieHellman(options[, callback])` ### `crypto.diffieHellman(options[, callback])`
<!-- YAML <!-- YAML
@ -3767,6 +3801,43 @@ ECDH operation.
If the `callback` function is provided this function uses libuv's threadpool. If the `callback` function is provided this function uses libuv's threadpool.
### `crypto.encapsulate(key[, callback])`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.2 - Release candidate
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject} Public Key
* `callback` {Function}
* `err` {Error}
* `result` {Object}
* `sharedKey` {Buffer}
* `ciphertext` {Buffer}
* Returns: {Object} if the `callback` function is not provided.
* `sharedKey` {Buffer}
* `ciphertext` {Buffer}
<!--lint enable maximum-line-length remark-lint-->
Key encapsulation using a KEM algorithm with a public key.
Supported key types and their KEM algorithms are:
* `'rsa'`[^openssl30] RSA Secret Value Encapsulation
* `'ec'`[^openssl32] DHKEM(P-256, HKDF-SHA256), DHKEM(P-384, HKDF-SHA256), DHKEM(P-521, HKDF-SHA256)
* `'x25519'`[^openssl32] DHKEM(X25519, HKDF-SHA256)
* `'x448'`[^openssl32] DHKEM(X448, HKDF-SHA512)
* `'ml-kem-512'`[^openssl35] ML-KEM
* `'ml-kem-768'`[^openssl35] ML-KEM
* `'ml-kem-1024'`[^openssl35] ML-KEM
If `key` is not a [`KeyObject`][], this function behaves as if `key` had been
passed to [`crypto.createPublicKey()`][].
If the `callback` function is provided this function uses libuv's threadpool.
### `crypto.fips` ### `crypto.fips`
<!-- YAML <!-- YAML
@ -6366,6 +6437,10 @@ See the [list of SSL OP Flags][] for details.
</tr> </tr>
</table> </table>
[^openssl30]: Requires OpenSSL >= 3.0
[^openssl32]: Requires OpenSSL >= 3.2
[^openssl35]: Requires OpenSSL >= 3.5 [^openssl35]: Requires OpenSSL >= 3.5
[AEAD algorithms]: https://en.wikipedia.org/wiki/Authenticated_encryption [AEAD algorithms]: https://en.wikipedia.org/wiki/Authenticated_encryption

View File

@ -1065,6 +1065,17 @@ Key's Elliptic Curve is not registered for use in the
Key's Asymmetric Key Type is not registered for use in the Key's Asymmetric Key Type is not registered for use in the
[JSON Web Key Types Registry][]. [JSON Web Key Types Registry][].
<a id="ERR_CRYPTO_KEM_NOT_SUPPORTED"></a>
### `ERR_CRYPTO_KEM_NOT_SUPPORTED`
<!-- YAML
added: REPLACEME
-->
Attempted to use KEM operations while Node.js was not compiled with
OpenSSL with KEM support.
<a id="ERR_CRYPTO_OPERATION_FAILED"></a> <a id="ERR_CRYPTO_OPERATION_FAILED"></a>
### `ERR_CRYPTO_OPERATION_FAILED` ### `ERR_CRYPTO_OPERATION_FAILED`

View File

@ -122,6 +122,10 @@ const {
secureHeapUsed, secureHeapUsed,
} = require('internal/crypto/util'); } = require('internal/crypto/util');
const Certificate = require('internal/crypto/certificate'); const Certificate = require('internal/crypto/certificate');
const {
encapsulate,
decapsulate,
} = require('internal/crypto/kem');
let webcrypto; let webcrypto;
function lazyWebCrypto() { function lazyWebCrypto() {
@ -225,6 +229,8 @@ module.exports = {
setFips, setFips,
verify: verifyOneShot, verify: verifyOneShot,
hash, hash,
encapsulate,
decapsulate,
// Classes // Classes
Certificate, Certificate,

112
lib/internal/crypto/kem.js Normal file
View File

@ -0,0 +1,112 @@
'use strict';
const {
FunctionPrototypeCall,
} = primordials;
const {
codes: {
ERR_CRYPTO_KEM_NOT_SUPPORTED,
},
} = require('internal/errors');
const {
validateFunction,
} = require('internal/validators');
const {
kCryptoJobAsync,
kCryptoJobSync,
KEMDecapsulateJob,
KEMEncapsulateJob,
} = internalBinding('crypto');
const {
preparePrivateKey,
preparePublicOrPrivateKey,
} = require('internal/crypto/keys');
const {
getArrayBufferOrView,
} = require('internal/crypto/util');
function encapsulate(key, callback) {
if (!KEMEncapsulateJob)
throw new ERR_CRYPTO_KEM_NOT_SUPPORTED();
if (callback !== undefined)
validateFunction(callback, 'callback');
const {
data: keyData,
format: keyFormat,
type: keyType,
passphrase: keyPassphrase,
} = preparePublicOrPrivateKey(key);
const job = new KEMEncapsulateJob(
callback ? kCryptoJobAsync : kCryptoJobSync,
keyData,
keyFormat,
keyType,
keyPassphrase);
if (!callback) {
const { 0: err, 1: result } = job.run();
if (err !== undefined)
throw err;
const { 0: sharedKey, 1: ciphertext } = result;
return { sharedKey, ciphertext };
}
job.ondone = (error, result) => {
if (error) return FunctionPrototypeCall(callback, job, error);
const { 0: sharedKey, 1: ciphertext } = result;
FunctionPrototypeCall(callback, job, null, { sharedKey, ciphertext });
};
job.run();
}
function decapsulate(key, ciphertext, callback) {
if (!KEMDecapsulateJob)
throw new ERR_CRYPTO_KEM_NOT_SUPPORTED();
if (callback !== undefined)
validateFunction(callback, 'callback');
const {
data: keyData,
format: keyFormat,
type: keyType,
passphrase: keyPassphrase,
} = preparePrivateKey(key);
ciphertext = getArrayBufferOrView(ciphertext, 'ciphertext');
const job = new KEMDecapsulateJob(
callback ? kCryptoJobAsync : kCryptoJobSync,
keyData,
keyFormat,
keyType,
keyPassphrase,
ciphertext);
if (!callback) {
const { 0: err, 1: result } = job.run();
if (err !== undefined)
throw err;
return result;
}
job.ondone = (error, result) => {
if (error) return FunctionPrototypeCall(callback, job, error);
FunctionPrototypeCall(callback, job, null, result);
};
job.run();
}
module.exports = {
encapsulate,
decapsulate,
};

View File

@ -1184,6 +1184,7 @@ E('ERR_CRYPTO_INVALID_JWK', 'Invalid JWK data', TypeError);
E('ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', E('ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE',
'Invalid key object type %s, expected %s.', TypeError); 'Invalid key object type %s, expected %s.', TypeError);
E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error); E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error);
E('ERR_CRYPTO_KEM_NOT_SUPPORTED', 'KEM is not supported', Error);
E('ERR_CRYPTO_PBKDF2_ERROR', 'PBKDF2 error', Error); E('ERR_CRYPTO_PBKDF2_ERROR', 'PBKDF2 error', Error);
E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error); E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error);
// Switch to TypeError. The current implementation does not seem right. // Switch to TypeError. The current implementation does not seem right.

View File

@ -345,6 +345,7 @@
'src/crypto/crypto_context.cc', 'src/crypto/crypto_context.cc',
'src/crypto/crypto_ec.cc', 'src/crypto/crypto_ec.cc',
'src/crypto/crypto_ml_dsa.cc', 'src/crypto/crypto_ml_dsa.cc',
'src/crypto/crypto_kem.cc',
'src/crypto/crypto_hmac.cc', 'src/crypto/crypto_hmac.cc',
'src/crypto/crypto_random.cc', 'src/crypto/crypto_random.cc',
'src/crypto/crypto_rsa.cc', 'src/crypto/crypto_rsa.cc',

262
src/crypto/crypto_kem.cc Normal file
View File

@ -0,0 +1,262 @@
#include "crypto/crypto_kem.h"
#if OPENSSL_VERSION_MAJOR >= 3
#include "async_wrap-inl.h"
#include "base_object-inl.h"
#include "crypto/crypto_keys.h"
#include "crypto/crypto_util.h"
#include "env-inl.h"
#include "memory_tracker-inl.h"
#include "node_buffer.h"
#include "threadpoolwork-inl.h"
#include "v8.h"
namespace node {
using ncrypto::EVPKeyPointer;
using v8::Array;
using v8::FunctionCallbackInfo;
using v8::Local;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Nothing;
using v8::Object;
using v8::Value;
namespace crypto {
KEMConfiguration::KEMConfiguration(KEMConfiguration&& other) noexcept
: job_mode(other.job_mode),
mode(other.mode),
key(std::move(other.key)),
ciphertext(std::move(other.ciphertext)) {}
KEMConfiguration& KEMConfiguration::operator=(
KEMConfiguration&& other) noexcept {
if (&other == this) return *this;
this->~KEMConfiguration();
return *new (this) KEMConfiguration(std::move(other));
}
void KEMConfiguration::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("key", key);
if (job_mode == kCryptoJobAsync) {
tracker->TrackFieldWithSize("ciphertext", ciphertext.size());
}
}
namespace {
bool DoKEMEncapsulate(Environment* env,
const EVPKeyPointer& public_key,
ByteSource* out,
CryptoJobMode mode) {
auto result = ncrypto::KEM::Encapsulate(public_key);
if (!result) {
if (mode == kCryptoJobSync) {
THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to perform encapsulation");
}
return false;
}
// Pack the result: [ciphertext_len][shared_key_len][ciphertext][shared_key]
size_t ciphertext_len = result->ciphertext.size();
size_t shared_key_len = result->shared_key.size();
size_t total_len =
sizeof(uint32_t) + sizeof(uint32_t) + ciphertext_len + shared_key_len;
auto data = ncrypto::DataPointer::Alloc(total_len);
if (!data) {
if (mode == kCryptoJobSync) {
THROW_ERR_CRYPTO_OPERATION_FAILED(env,
"Failed to allocate output buffer");
}
return false;
}
unsigned char* ptr = static_cast<unsigned char*>(data.get());
// Write size headers
*reinterpret_cast<uint32_t*>(ptr) = static_cast<uint32_t>(ciphertext_len);
*reinterpret_cast<uint32_t*>(ptr + sizeof(uint32_t)) =
static_cast<uint32_t>(shared_key_len);
// Write ciphertext and shared key data
unsigned char* ciphertext_ptr = ptr + 2 * sizeof(uint32_t);
unsigned char* shared_key_ptr = ciphertext_ptr + ciphertext_len;
std::memcpy(ciphertext_ptr, result->ciphertext.get(), ciphertext_len);
std::memcpy(shared_key_ptr, result->shared_key.get(), shared_key_len);
*out = ByteSource::Allocated(data.release());
return true;
}
bool DoKEMDecapsulate(Environment* env,
const EVPKeyPointer& private_key,
const ByteSource& ciphertext,
ByteSource* out,
CryptoJobMode mode) {
ncrypto::Buffer<const void> ciphertext_buf{ciphertext.data(),
ciphertext.size()};
auto shared_key = ncrypto::KEM::Decapsulate(private_key, ciphertext_buf);
if (!shared_key) {
if (mode == kCryptoJobSync) {
THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to perform decapsulation");
}
return false;
}
*out = ByteSource::Allocated(shared_key.release());
return true;
}
} // anonymous namespace
// KEMEncapsulateTraits implementation
Maybe<void> KEMEncapsulateTraits::AdditionalConfig(
CryptoJobMode mode,
const FunctionCallbackInfo<Value>& args,
unsigned int offset,
KEMConfiguration* params) {
params->job_mode = mode;
params->mode = KEMMode::Encapsulate;
unsigned int key_offset = offset;
auto public_key_data =
KeyObjectData::GetPublicOrPrivateKeyFromJs(args, &key_offset);
if (!public_key_data) {
return Nothing<void>();
}
params->key = std::move(public_key_data);
return v8::JustVoid();
}
bool KEMEncapsulateTraits::DeriveBits(Environment* env,
const KEMConfiguration& params,
ByteSource* out,
CryptoJobMode mode) {
Mutex::ScopedLock lock(params.key.mutex());
const auto& public_key = params.key.GetAsymmetricKey();
return DoKEMEncapsulate(env, public_key, out, mode);
}
MaybeLocal<Value> KEMEncapsulateTraits::EncodeOutput(
Environment* env, const KEMConfiguration& params, ByteSource* out) {
// The output contains:
// [ciphertext_len][shared_key_len][ciphertext][shared_key]
const unsigned char* data = out->data<unsigned char>();
uint32_t ciphertext_len = *reinterpret_cast<const uint32_t*>(data);
uint32_t shared_key_len =
*reinterpret_cast<const uint32_t*>(data + sizeof(uint32_t));
const unsigned char* ciphertext_ptr = data + 2 * sizeof(uint32_t);
const unsigned char* shared_key_ptr = ciphertext_ptr + ciphertext_len;
MaybeLocal<Object> ciphertext_buf =
node::Buffer::Copy(env->isolate(),
reinterpret_cast<const char*>(ciphertext_ptr),
ciphertext_len);
MaybeLocal<Object> shared_key_buf =
node::Buffer::Copy(env->isolate(),
reinterpret_cast<const char*>(shared_key_ptr),
shared_key_len);
Local<Object> ciphertext_obj;
Local<Object> shared_key_obj;
if (!ciphertext_buf.ToLocal(&ciphertext_obj) ||
!shared_key_buf.ToLocal(&shared_key_obj)) {
return MaybeLocal<Value>();
}
// Return an array [sharedKey, ciphertext].
Local<Array> result = Array::New(env->isolate(), 2);
if (result->Set(env->context(), 0, shared_key_obj).IsNothing() ||
result->Set(env->context(), 1, ciphertext_obj).IsNothing()) {
return MaybeLocal<Value>();
}
return result;
}
// KEMDecapsulateTraits implementation
Maybe<void> KEMDecapsulateTraits::AdditionalConfig(
CryptoJobMode mode,
const FunctionCallbackInfo<Value>& args,
unsigned int offset,
KEMConfiguration* params) {
Environment* env = Environment::GetCurrent(args);
params->job_mode = mode;
params->mode = KEMMode::Decapsulate;
unsigned int key_offset = offset;
auto private_key_data =
KeyObjectData::GetPrivateKeyFromJs(args, &key_offset, true);
if (!private_key_data) {
return Nothing<void>();
}
params->key = std::move(private_key_data);
ArrayBufferOrViewContents<unsigned char> ciphertext(args[key_offset]);
if (!ciphertext.CheckSizeInt32()) {
THROW_ERR_OUT_OF_RANGE(env, "ciphertext is too big");
return Nothing<void>();
}
params->ciphertext =
mode == kCryptoJobAsync ? ciphertext.ToCopy() : ciphertext.ToByteSource();
return v8::JustVoid();
}
bool KEMDecapsulateTraits::DeriveBits(Environment* env,
const KEMConfiguration& params,
ByteSource* out,
CryptoJobMode mode) {
Mutex::ScopedLock lock(params.key.mutex());
const auto& private_key = params.key.GetAsymmetricKey();
return DoKEMDecapsulate(env, private_key, params.ciphertext, out, mode);
}
MaybeLocal<Value> KEMDecapsulateTraits::EncodeOutput(
Environment* env, const KEMConfiguration& params, ByteSource* out) {
return out->ToBuffer(env);
}
void InitializeKEM(Environment* env, Local<Object> target) {
KEMEncapsulateJob::Initialize(env, target);
KEMDecapsulateJob::Initialize(env, target);
constexpr int kKEMEncapsulate = static_cast<int>(KEMMode::Encapsulate);
constexpr int kKEMDecapsulate = static_cast<int>(KEMMode::Decapsulate);
NODE_DEFINE_CONSTANT(target, kKEMEncapsulate);
NODE_DEFINE_CONSTANT(target, kKEMDecapsulate);
}
void RegisterKEMExternalReferences(ExternalReferenceRegistry* registry) {
KEMEncapsulateJob::RegisterExternalReferences(registry);
KEMDecapsulateJob::RegisterExternalReferences(registry);
}
namespace KEM {
void Initialize(Environment* env, Local<Object> target) {
InitializeKEM(env, target);
}
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
RegisterKEMExternalReferences(registry);
}
} // namespace KEM
} // namespace crypto
} // namespace node
#endif

113
src/crypto/crypto_kem.h Normal file
View File

@ -0,0 +1,113 @@
#ifndef SRC_CRYPTO_CRYPTO_KEM_H_
#define SRC_CRYPTO_CRYPTO_KEM_H_
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#include "base_object.h"
#include "crypto/crypto_keys.h"
#include "crypto/crypto_util.h"
#include "env.h"
#include "memory_tracker.h"
#include "node_external_reference.h"
#if OPENSSL_VERSION_MAJOR >= 3
namespace node {
namespace crypto {
enum class KEMMode { Encapsulate, Decapsulate };
struct KEMConfiguration final : public MemoryRetainer {
CryptoJobMode job_mode;
KEMMode mode;
KeyObjectData key;
ByteSource ciphertext;
KEMConfiguration() = default;
explicit KEMConfiguration(KEMConfiguration&& other) noexcept;
KEMConfiguration& operator=(KEMConfiguration&& other) noexcept;
void MemoryInfo(MemoryTracker* tracker) const override;
SET_MEMORY_INFO_NAME(KEMConfiguration)
SET_SELF_SIZE(KEMConfiguration)
};
struct KEMEncapsulateTraits final {
using AdditionalParameters = KEMConfiguration;
static constexpr const char* JobName = "KEMEncapsulateJob";
static constexpr AsyncWrap::ProviderType Provider =
AsyncWrap::PROVIDER_DERIVEBITSREQUEST;
static v8::Maybe<void> AdditionalConfig(
CryptoJobMode mode,
const v8::FunctionCallbackInfo<v8::Value>& args,
unsigned int offset,
KEMConfiguration* params);
static bool DeriveBits(Environment* env,
const KEMConfiguration& params,
ByteSource* out,
CryptoJobMode mode);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const KEMConfiguration& params,
ByteSource* out);
};
struct KEMDecapsulateTraits final {
using AdditionalParameters = KEMConfiguration;
static constexpr const char* JobName = "KEMDecapsulateJob";
static constexpr AsyncWrap::ProviderType Provider =
AsyncWrap::PROVIDER_DERIVEBITSREQUEST;
static v8::Maybe<void> AdditionalConfig(
CryptoJobMode mode,
const v8::FunctionCallbackInfo<v8::Value>& args,
unsigned int offset,
KEMConfiguration* params);
static bool DeriveBits(Environment* env,
const KEMConfiguration& params,
ByteSource* out,
CryptoJobMode mode);
static v8::MaybeLocal<v8::Value> EncodeOutput(Environment* env,
const KEMConfiguration& params,
ByteSource* out);
};
using KEMEncapsulateJob = DeriveBitsJob<KEMEncapsulateTraits>;
using KEMDecapsulateJob = DeriveBitsJob<KEMDecapsulateTraits>;
void InitializeKEM(Environment* env, v8::Local<v8::Object> target);
void RegisterKEMExternalReferences(ExternalReferenceRegistry* registry);
namespace KEM {
void Initialize(Environment* env, v8::Local<v8::Object> target);
void RegisterExternalReferences(ExternalReferenceRegistry* registry);
} // namespace KEM
} // namespace crypto
} // namespace node
#else
// Provide stub implementations when OpenSSL < 3.0
namespace node {
namespace crypto {
namespace KEM {
inline void Initialize(Environment* env, v8::Local<v8::Object> target) {
// No-op when OpenSSL < 3.0
}
inline void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
// No-op when OpenSSL < 3.0
}
} // namespace KEM
} // namespace crypto
} // namespace node
#endif
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
#endif // SRC_CRYPTO_CRYPTO_KEM_H_

View File

@ -66,6 +66,13 @@ namespace crypto {
#define ARGON2_NAMESPACE_LIST(V) #define ARGON2_NAMESPACE_LIST(V)
#endif // !OPENSSL_NO_ARGON2 && OpenSSL >= 3.2 #endif // !OPENSSL_NO_ARGON2 && OpenSSL >= 3.2
// KEM functionality requires OpenSSL 3.0.0 or later
#if OPENSSL_VERSION_MAJOR >= 3
#define KEM_NAMESPACE_LIST(V) V(KEM)
#else
#define KEM_NAMESPACE_LIST(V)
#endif
#ifdef OPENSSL_NO_SCRYPT #ifdef OPENSSL_NO_SCRYPT
#define SCRYPT_NAMESPACE_LIST(V) #define SCRYPT_NAMESPACE_LIST(V)
#else #else
@ -75,6 +82,7 @@ namespace crypto {
#define CRYPTO_NAMESPACE_LIST(V) \ #define CRYPTO_NAMESPACE_LIST(V) \
CRYPTO_NAMESPACE_LIST_BASE(V) \ CRYPTO_NAMESPACE_LIST_BASE(V) \
ARGON2_NAMESPACE_LIST(V) \ ARGON2_NAMESPACE_LIST(V) \
KEM_NAMESPACE_LIST(V) \
SCRYPT_NAMESPACE_LIST(V) SCRYPT_NAMESPACE_LIST(V)
void Initialize(Local<Object> target, void Initialize(Local<Object> target,

View File

@ -40,6 +40,9 @@
#include "crypto/crypto_hash.h" #include "crypto/crypto_hash.h"
#include "crypto/crypto_hkdf.h" #include "crypto/crypto_hkdf.h"
#include "crypto/crypto_hmac.h" #include "crypto/crypto_hmac.h"
#if OPENSSL_VERSION_MAJOR >= 3
#include "crypto/crypto_kem.h"
#endif
#include "crypto/crypto_keygen.h" #include "crypto/crypto_keygen.h"
#include "crypto/crypto_keys.h" #include "crypto/crypto_keys.h"
#include "crypto/crypto_ml_dsa.h" #include "crypto/crypto_ml_dsa.h"

View File

@ -0,0 +1,211 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const crypto = require('crypto');
const fixtures = require('../common/fixtures');
const { hasOpenSSL } = require('../common/crypto');
const { promisify } = require('util');
if (!hasOpenSSL(3)) {
assert.throws(() => crypto.encapsulate(), { code: 'ERR_CRYPTO_KEM_NOT_SUPPORTED' });
return;
}
assert.throws(() => crypto.encapsulate(), { code: 'ERR_INVALID_ARG_TYPE',
message: /The "key" argument must be of type/ });
assert.throws(() => crypto.decapsulate(), { code: 'ERR_INVALID_ARG_TYPE',
message: /The "key" argument must be of type/ });
const keys = {
'rsa': {
supported: hasOpenSSL(3), // RSASVE was added in 3.0
publicKey: fixtures.readKey('rsa_public_2048.pem', 'ascii'),
privateKey: fixtures.readKey('rsa_private_2048.pem', 'ascii'),
sharedSecretLength: 256,
ciphertextLength: 256,
},
'rsa-pss': {
supported: false, // Only raw RSA is supported
publicKey: fixtures.readKey('rsa_pss_public_2048.pem', 'ascii'),
privateKey: fixtures.readKey('rsa_pss_private_2048.pem', 'ascii'),
},
'p-256': {
supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2
publicKey: fixtures.readKey('ec_p256_public.pem', 'ascii'),
privateKey: fixtures.readKey('ec_p256_private.pem', 'ascii'),
sharedSecretLength: 32,
ciphertextLength: 65,
},
'p-384': {
supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2
publicKey: fixtures.readKey('ec_p384_public.pem', 'ascii'),
privateKey: fixtures.readKey('ec_p384_private.pem', 'ascii'),
sharedSecretLength: 48,
ciphertextLength: 97,
},
'p-521': {
supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2
publicKey: fixtures.readKey('ec_p521_public.pem', 'ascii'),
privateKey: fixtures.readKey('ec_p521_private.pem', 'ascii'),
sharedSecretLength: 64,
ciphertextLength: 133,
},
'secp256k1': {
supported: false, // only P-256, P-384, and P-521 are supported
publicKey: fixtures.readKey('ec_secp256k1_public.pem', 'ascii'),
privateKey: fixtures.readKey('ec_secp256k1_private.pem', 'ascii'),
},
'x25519': {
supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2
publicKey: fixtures.readKey('x25519_public.pem', 'ascii'),
privateKey: fixtures.readKey('x25519_private.pem', 'ascii'),
sharedSecretLength: 32,
ciphertextLength: 32,
},
'x448': {
supported: hasOpenSSL(3, 2), // DHKEM was added in 3.2
publicKey: fixtures.readKey('x448_public.pem', 'ascii'),
privateKey: fixtures.readKey('x448_private.pem', 'ascii'),
sharedSecretLength: 64,
ciphertextLength: 56,
},
'ml-kem-512': {
supported: hasOpenSSL(3, 5),
publicKey: fixtures.readKey('ml_kem_512_public.pem', 'ascii'),
privateKey: fixtures.readKey('ml_kem_512_private.pem', 'ascii'),
sharedSecretLength: 32,
ciphertextLength: 768,
},
'ml-kem-768': {
supported: hasOpenSSL(3, 5),
publicKey: fixtures.readKey('ml_kem_768_public.pem', 'ascii'),
privateKey: fixtures.readKey('ml_kem_768_private.pem', 'ascii'),
sharedSecretLength: 32,
ciphertextLength: 1088,
},
'ml-kem-1024': {
supported: hasOpenSSL(3, 5),
publicKey: fixtures.readKey('ml_kem_1024_public.pem', 'ascii'),
privateKey: fixtures.readKey('ml_kem_1024_private.pem', 'ascii'),
sharedSecretLength: 32,
ciphertextLength: 1568,
},
};
for (const [name, { supported, publicKey, privateKey, sharedSecretLength, ciphertextLength }] of Object.entries(keys)) {
if (!supported) {
assert.throws(() => crypto.encapsulate(publicKey),
{ code: /ERR_OSSL_EVP_DECODE_ERROR|ERR_CRYPTO_OPERATION_FAILED/ });
continue;
}
{
assert.throws(() => crypto.decapsulate(privateKey, null),
{
code: 'ERR_INVALID_ARG_TYPE',
message: /instance of ArrayBuffer, Buffer, TypedArray, or DataView\. Received null/
});
}
function formatKeyAs(key, params) {
return { ...params, key: key.export(params) };
}
const keyObjects = {
publicKey: crypto.createPublicKey(publicKey),
privateKey: crypto.createPrivateKey(privateKey),
};
const keyPairs = [
keyObjects,
{ publicKey, privateKey },
{
publicKey: formatKeyAs(keyObjects.publicKey, { format: 'der', type: 'spki' }),
privateKey: formatKeyAs(keyObjects.privateKey, { format: 'der', type: 'pkcs8' })
},
];
// 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' })
});
}
for (const kp of keyPairs) {
// sync
{
const { sharedKey, ciphertext } = crypto.encapsulate(kp.publicKey);
assert(Buffer.isBuffer(sharedKey));
assert.strictEqual(sharedKey.byteLength, sharedSecretLength);
assert(Buffer.isBuffer(ciphertext));
assert.strictEqual(ciphertext.byteLength, ciphertextLength);
const sharedKey2 = crypto.decapsulate(kp.privateKey, ciphertext);
assert(Buffer.isBuffer(sharedKey2));
assert.strictEqual(sharedKey2.byteLength, sharedSecretLength);
assert(sharedKey.equals(sharedKey2));
}
// async
{
crypto.encapsulate(kp.publicKey, common.mustSucceed(({ sharedKey, ciphertext }) => {
assert(Buffer.isBuffer(sharedKey));
assert.strictEqual(sharedKey.byteLength, sharedSecretLength);
assert(Buffer.isBuffer(ciphertext));
assert.strictEqual(ciphertext.byteLength, ciphertextLength);
crypto.decapsulate(kp.privateKey, ciphertext, common.mustSucceed((sharedKey2) => {
assert(Buffer.isBuffer(sharedKey2));
assert.strictEqual(sharedKey2.byteLength, sharedSecretLength);
assert(sharedKey.equals(sharedKey2));
}));
}));
}
// promisified
(async () => {
const { sharedKey, ciphertext } = await promisify(crypto.encapsulate)(kp.publicKey);
assert(Buffer.isBuffer(sharedKey));
assert.strictEqual(sharedKey.byteLength, sharedSecretLength);
assert(Buffer.isBuffer(ciphertext));
assert.strictEqual(ciphertext.byteLength, ciphertextLength);
const sharedKey2 = await promisify(crypto.decapsulate)(kp.privateKey, ciphertext);
assert(Buffer.isBuffer(sharedKey2));
assert.strictEqual(sharedKey2.byteLength, sharedSecretLength);
assert(sharedKey.equals(sharedKey2));
})().then(common.mustCall());
}
let wrongPrivateKey;
if (name.startsWith('x')) {
wrongPrivateKey = name === 'x448' ? keys.x25519.privateKey : keys.x448.privateKey;
} else if (name.startsWith('p-')) {
wrongPrivateKey = name === 'p-256' ? keys['p-384'].privateKey : keys['p-256'].privateKey;
} else if (name.startsWith('ml-')) {
wrongPrivateKey = name === 'ml-kem-512' ? keys['ml-kem-768'].privateKey : keys['ml-kem-512'].privateKey;
} else {
wrongPrivateKey = keys.x25519.privateKey;
}
// sync errors
{
const { ciphertext } = crypto.encapsulate(publicKey);
assert.throws(() => crypto.decapsulate(wrongPrivateKey, ciphertext), {
message: /Failed to (initialize|perform) decapsulation/,
code: 'ERR_CRYPTO_OPERATION_FAILED',
});
}
// async errors
{
crypto.encapsulate(publicKey, common.mustSucceed(({ ciphertext }) => {
crypto.decapsulate(wrongPrivateKey, ciphertext, common.mustCall((err) => {
assert(err);
assert.strictEqual(err.message, 'Deriving bits failed');
}));
}));
}
}