Skip to content

Commit 4e3bb02

Browse files
committed
crypto: support ML-KEM, DHKEM, and RSASVE key encapsulation mechanisms
1 parent 66e25cf commit 4e3bb02

File tree

14 files changed

+1115
-0
lines changed

14 files changed

+1115
-0
lines changed

benchmark/crypto/kem.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const { hasOpenSSL } = require('../../test/common/crypto.js');
5+
const crypto = require('crypto');
6+
const fs = require('fs');
7+
const path = require('path');
8+
const fixtures_keydir = path.resolve(__dirname, '../../test/fixtures/keys/');
9+
10+
function readKey(name) {
11+
return fs.readFileSync(`${fixtures_keydir}/${name}.pem`, 'utf8');
12+
}
13+
14+
const keyFixtures = {};
15+
16+
if (hasOpenSSL(3, 5)) {
17+
keyFixtures['ml-kem-512'] = readKey('ml_kem_512_private');
18+
keyFixtures['ml-kem-768'] = readKey('ml_kem_768_private');
19+
keyFixtures['ml-kem-1024'] = readKey('ml_kem_1024_private');
20+
}
21+
if (hasOpenSSL(3, 2)) {
22+
keyFixtures['p-256'] = readKey('ec_p256_private');
23+
keyFixtures['p-384'] = readKey('ec_p384_private');
24+
keyFixtures['p-521'] = readKey('ec_p521_private');
25+
keyFixtures.x25519 = readKey('x25519_private');
26+
keyFixtures.x448 = readKey('x448_private');
27+
}
28+
if (hasOpenSSL(3, 0)) {
29+
keyFixtures.rsa = readKey('rsa_private_2048');
30+
}
31+
32+
if (Object.keys(keyFixtures).length === 0) {
33+
console.log('no supported key types available for this OpenSSL version');
34+
process.exit(0);
35+
}
36+
37+
const bench = common.createBenchmark(main, {
38+
keyType: Object.keys(keyFixtures),
39+
mode: ['sync', 'async', 'async-parallel'],
40+
keyFormat: ['keyObject', 'keyObject.unique'],
41+
op: ['encapsulate', 'decapsulate'],
42+
n: [1e3],
43+
}, {
44+
combinationFilter(p) {
45+
// "keyObject.unique" allows to compare the result with "keyObject" to
46+
// assess whether mutexes over the key material impact the operation
47+
return p.keyFormat !== 'keyObject.unique' ||
48+
(p.keyFormat === 'keyObject.unique' && p.mode === 'async-parallel');
49+
},
50+
});
51+
52+
function measureSync(n, op, privateKey, keys, ciphertexts) {
53+
bench.start();
54+
for (let i = 0; i < n; ++i) {
55+
const key = privateKey || keys[i];
56+
if (op === 'encapsulate') {
57+
crypto.encapsulate(key);
58+
} else {
59+
crypto.decapsulate(key, ciphertexts[i]);
60+
}
61+
}
62+
bench.end(n);
63+
}
64+
65+
function measureAsync(n, op, privateKey, keys, ciphertexts) {
66+
let remaining = n;
67+
function done() {
68+
if (--remaining === 0)
69+
bench.end(n);
70+
else
71+
one();
72+
}
73+
74+
function one() {
75+
const key = privateKey || keys[n - remaining];
76+
if (op === 'encapsulate') {
77+
crypto.encapsulate(key, done);
78+
} else {
79+
crypto.decapsulate(key, ciphertexts[n - remaining], done);
80+
}
81+
}
82+
bench.start();
83+
one();
84+
}
85+
86+
function measureAsyncParallel(n, op, privateKey, keys, ciphertexts) {
87+
let remaining = n;
88+
function done() {
89+
if (--remaining === 0)
90+
bench.end(n);
91+
}
92+
bench.start();
93+
for (let i = 0; i < n; ++i) {
94+
const key = privateKey || keys[i];
95+
if (op === 'encapsulate') {
96+
crypto.encapsulate(key, done);
97+
} else {
98+
crypto.decapsulate(key, ciphertexts[i], done);
99+
}
100+
}
101+
}
102+
103+
function main({ n, mode, keyFormat, keyType, op }) {
104+
const pems = [...Buffer.alloc(n)].map(() => keyFixtures[keyType]);
105+
const keyObjects = pems.map(crypto.createPrivateKey);
106+
107+
let privateKey, keys, ciphertexts;
108+
109+
switch (keyFormat) {
110+
case 'keyObject':
111+
privateKey = keyObjects[0];
112+
break;
113+
case 'keyObject.unique':
114+
keys = keyObjects;
115+
break;
116+
default:
117+
throw new Error('not implemented');
118+
}
119+
120+
// Pre-generate ciphertexts for decapsulate operations
121+
if (op === 'decapsulate') {
122+
if (privateKey) {
123+
ciphertexts = [...Buffer.alloc(n)].map(() => crypto.encapsulate(privateKey).ciphertext);
124+
} else {
125+
ciphertexts = keys.map((key) => crypto.encapsulate(key).ciphertext);
126+
}
127+
}
128+
129+
switch (mode) {
130+
case 'sync':
131+
measureSync(n, op, privateKey, keys, ciphertexts);
132+
break;
133+
case 'async':
134+
measureAsync(n, op, privateKey, keys, ciphertexts);
135+
break;
136+
case 'async-parallel':
137+
measureAsyncParallel(n, op, privateKey, keys, ciphertexts);
138+
break;
139+
}
140+
}

deps/ncrypto/ncrypto.cc

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <algorithm>
1111
#include <cstring>
1212
#if OPENSSL_VERSION_MAJOR >= 3
13+
#include <openssl/core_names.h>
1314
#include <openssl/provider.h>
1415
#endif
1516
#if OPENSSL_WITH_PQC
@@ -4409,4 +4410,125 @@ const Digest Digest::FromName(const char* name) {
44094410
return ncrypto::getDigestByName(name);
44104411
}
44114412

4413+
// ============================================================================
4414+
// KEM Implementation
4415+
#if OPENSSL_VERSION_MAJOR >= 3
4416+
#if !OPENSSL_VERSION_PREREQ(3, 5)
4417+
bool KEM::SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key) {
4418+
const char* operation = nullptr;
4419+
4420+
switch (EVP_PKEY_id(key.get())) {
4421+
case EVP_PKEY_RSA:
4422+
operation = OSSL_KEM_PARAM_OPERATION_RSASVE;
4423+
break;
4424+
#if OPENSSL_VERSION_PREREQ(3, 2)
4425+
case EVP_PKEY_EC:
4426+
case EVP_PKEY_X25519:
4427+
case EVP_PKEY_X448:
4428+
operation = OSSL_KEM_PARAM_OPERATION_DHKEM;
4429+
break;
4430+
#endif
4431+
default:
4432+
unreachable();
4433+
}
4434+
4435+
if (operation != nullptr) {
4436+
OSSL_PARAM params[] = {
4437+
OSSL_PARAM_utf8_string(
4438+
OSSL_KEM_PARAM_OPERATION, const_cast<char*>(operation), 0),
4439+
OSSL_PARAM_END};
4440+
4441+
if (EVP_PKEY_CTX_set_params(ctx, params) <= 0) {
4442+
return false;
4443+
}
4444+
}
4445+
4446+
return true;
4447+
}
4448+
#endif
4449+
4450+
std::optional<KEM::EncapsulateResult> KEM::Encapsulate(
4451+
const EVPKeyPointer& public_key) {
4452+
ClearErrorOnReturn clear_error_on_return;
4453+
4454+
auto ctx = public_key.newCtx();
4455+
if (!ctx) return std::nullopt;
4456+
4457+
if (EVP_PKEY_encapsulate_init(ctx.get(), nullptr) <= 0) {
4458+
return std::nullopt;
4459+
}
4460+
4461+
#if !OPENSSL_VERSION_PREREQ(3, 5)
4462+
if (!SetOperationParameter(ctx.get(), public_key)) {
4463+
return std::nullopt;
4464+
}
4465+
#endif
4466+
4467+
// Determine output buffer sizes
4468+
size_t ciphertext_len = 0;
4469+
size_t shared_key_len = 0;
4470+
4471+
if (EVP_PKEY_encapsulate(
4472+
ctx.get(), nullptr, &ciphertext_len, nullptr, &shared_key_len) <= 0) {
4473+
return std::nullopt;
4474+
}
4475+
4476+
auto ciphertext = DataPointer::Alloc(ciphertext_len);
4477+
auto shared_key = DataPointer::Alloc(shared_key_len);
4478+
if (!ciphertext || !shared_key) return std::nullopt;
4479+
4480+
if (EVP_PKEY_encapsulate(ctx.get(),
4481+
static_cast<unsigned char*>(ciphertext.get()),
4482+
&ciphertext_len,
4483+
static_cast<unsigned char*>(shared_key.get()),
4484+
&shared_key_len) <= 0) {
4485+
return std::nullopt;
4486+
}
4487+
4488+
return EncapsulateResult(std::move(ciphertext), std::move(shared_key));
4489+
}
4490+
4491+
DataPointer KEM::Decapsulate(const EVPKeyPointer& private_key,
4492+
const Buffer<const void>& ciphertext) {
4493+
ClearErrorOnReturn clear_error_on_return;
4494+
4495+
auto ctx = private_key.newCtx();
4496+
if (!ctx) return {};
4497+
4498+
if (EVP_PKEY_decapsulate_init(ctx.get(), nullptr) <= 0) {
4499+
return {};
4500+
}
4501+
4502+
#if !OPENSSL_VERSION_PREREQ(3, 5)
4503+
if (!SetOperationParameter(ctx.get(), private_key)) {
4504+
return {};
4505+
}
4506+
#endif
4507+
4508+
// First pass: determine shared secret size
4509+
size_t shared_key_len = 0;
4510+
if (EVP_PKEY_decapsulate(ctx.get(),
4511+
nullptr,
4512+
&shared_key_len,
4513+
static_cast<const unsigned char*>(ciphertext.data),
4514+
ciphertext.len) <= 0) {
4515+
return {};
4516+
}
4517+
4518+
auto shared_key = DataPointer::Alloc(shared_key_len);
4519+
if (!shared_key) return {};
4520+
4521+
if (EVP_PKEY_decapsulate(ctx.get(),
4522+
static_cast<unsigned char*>(shared_key.get()),
4523+
&shared_key_len,
4524+
static_cast<const unsigned char*>(ciphertext.data),
4525+
ciphertext.len) <= 0) {
4526+
return {};
4527+
}
4528+
4529+
return shared_key;
4530+
}
4531+
4532+
#endif // OPENSSL_VERSION_MAJOR >= 3
4533+
44124534
} // namespace ncrypto

deps/ncrypto/ncrypto.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,6 +1556,40 @@ DataPointer pbkdf2(const Digest& md,
15561556
uint32_t iterations,
15571557
size_t length);
15581558

1559+
// ============================================================================
1560+
// KEM (Key Encapsulation Mechanism)
1561+
#if OPENSSL_VERSION_MAJOR >= 3
1562+
1563+
class KEM final {
1564+
public:
1565+
struct EncapsulateResult {
1566+
DataPointer ciphertext;
1567+
DataPointer shared_key;
1568+
1569+
EncapsulateResult() = default;
1570+
EncapsulateResult(DataPointer ct, DataPointer sk)
1571+
: ciphertext(std::move(ct)), shared_key(std::move(sk)) {}
1572+
};
1573+
1574+
// Encapsulate a shared secret using KEM with a public key
1575+
// Returns both the ciphertext and shared secret
1576+
static std::optional<EncapsulateResult> Encapsulate(
1577+
const EVPKeyPointer& public_key);
1578+
1579+
// Decapsulate a shared secret using KEM with a private key and ciphertext
1580+
// Returns the shared secret
1581+
static DataPointer Decapsulate(const EVPKeyPointer& private_key,
1582+
const Buffer<const void>& ciphertext);
1583+
1584+
private:
1585+
#if !OPENSSL_VERSION_PREREQ(3, 5)
1586+
static bool SetOperationParameter(EVP_PKEY_CTX* ctx,
1587+
const EVPKeyPointer& key);
1588+
#endif
1589+
};
1590+
1591+
#endif // OPENSSL_VERSION_MAJOR >= 3
1592+
15591593
// ============================================================================
15601594
// Version metadata
15611595
#define NCRYPTO_VERSION "0.0.1"

0 commit comments

Comments
 (0)