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);
}
// ============================================================================
// 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

View File

@ -1574,6 +1574,40 @@ DataPointer argon2(const Buffer<const char>& pass,
#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
#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
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])`
<!-- YAML
@ -3767,6 +3801,43 @@ ECDH operation.
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`
<!-- YAML
@ -6366,6 +6437,10 @@ See the [list of SSL OP Flags][] for details.
</tr>
</table>
[^openssl30]: Requires OpenSSL >= 3.0
[^openssl32]: Requires OpenSSL >= 3.2
[^openssl35]: Requires OpenSSL >= 3.5
[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
[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>
### `ERR_CRYPTO_OPERATION_FAILED`

View File

@ -122,6 +122,10 @@ const {
secureHeapUsed,
} = require('internal/crypto/util');
const Certificate = require('internal/crypto/certificate');
const {
encapsulate,
decapsulate,
} = require('internal/crypto/kem');
let webcrypto;
function lazyWebCrypto() {
@ -225,6 +229,8 @@ module.exports = {
setFips,
verify: verifyOneShot,
hash,
encapsulate,
decapsulate,
// Classes
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',
'Invalid key object type %s, expected %s.', TypeError);
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_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error);
// Switch to TypeError. The current implementation does not seem right.

View File

@ -345,6 +345,7 @@
'src/crypto/crypto_context.cc',
'src/crypto/crypto_ec.cc',
'src/crypto/crypto_ml_dsa.cc',
'src/crypto/crypto_kem.cc',
'src/crypto/crypto_hmac.cc',
'src/crypto/crypto_random.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)
#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
#define SCRYPT_NAMESPACE_LIST(V)
#else
@ -75,6 +82,7 @@ namespace crypto {
#define CRYPTO_NAMESPACE_LIST(V) \
CRYPTO_NAMESPACE_LIST_BASE(V) \
ARGON2_NAMESPACE_LIST(V) \
KEM_NAMESPACE_LIST(V) \
SCRYPT_NAMESPACE_LIST(V)
void Initialize(Local<Object> target,

View File

@ -40,6 +40,9 @@
#include "crypto/crypto_hash.h"
#include "crypto/crypto_hkdf.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_keys.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');
}));
}));
}
}