Skip to content
Merged
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
140 changes: 140 additions & 0 deletions benchmark/crypto/kem.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
121 changes: 121 additions & 0 deletions deps/ncrypto/ncrypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
34 changes: 34 additions & 0 deletions deps/ncrypto/ncrypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading
Loading