Skip to content

Commit 1474153

Browse files
panvatargos
authored andcommitted
crypto: support ML-KEM, DHKEM, and RSASVE key encapsulation mechanisms
PR-URL: #59491 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
1 parent 89dd770 commit 1474153

File tree

14 files changed

+1098
-0
lines changed

14 files changed

+1098
-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: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4510,4 +4510,125 @@ const Digest Digest::FromName(const char* name) {
45104510
return ncrypto::getDigestByName(name);
45114511
}
45124512

4513+
// ============================================================================
4514+
// KEM Implementation
4515+
#if OPENSSL_VERSION_MAJOR >= 3
4516+
#if !OPENSSL_VERSION_PREREQ(3, 5)
4517+
bool KEM::SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key) {
4518+
const char* operation = nullptr;
4519+
4520+
switch (EVP_PKEY_id(key.get())) {
4521+
case EVP_PKEY_RSA:
4522+
operation = OSSL_KEM_PARAM_OPERATION_RSASVE;
4523+
break;
4524+
#if OPENSSL_VERSION_PREREQ(3, 2)
4525+
case EVP_PKEY_EC:
4526+
case EVP_PKEY_X25519:
4527+
case EVP_PKEY_X448:
4528+
operation = OSSL_KEM_PARAM_OPERATION_DHKEM;
4529+
break;
4530+
#endif
4531+
default:
4532+
unreachable();
4533+
}
4534+
4535+
if (operation != nullptr) {
4536+
OSSL_PARAM params[] = {
4537+
OSSL_PARAM_utf8_string(
4538+
OSSL_KEM_PARAM_OPERATION, const_cast<char*>(operation), 0),
4539+
OSSL_PARAM_END};
4540+
4541+
if (EVP_PKEY_CTX_set_params(ctx, params) <= 0) {
4542+
return false;
4543+
}
4544+
}
4545+
4546+
return true;
4547+
}
4548+
#endif
4549+
4550+
std::optional<KEM::EncapsulateResult> KEM::Encapsulate(
4551+
const EVPKeyPointer& public_key) {
4552+
ClearErrorOnReturn clear_error_on_return;
4553+
4554+
auto ctx = public_key.newCtx();
4555+
if (!ctx) return std::nullopt;
4556+
4557+
if (EVP_PKEY_encapsulate_init(ctx.get(), nullptr) <= 0) {
4558+
return std::nullopt;
4559+
}
4560+
4561+
#if !OPENSSL_VERSION_PREREQ(3, 5)
4562+
if (!SetOperationParameter(ctx.get(), public_key)) {
4563+
return std::nullopt;
4564+
}
4565+
#endif
4566+
4567+
// Determine output buffer sizes
4568+
size_t ciphertext_len = 0;
4569+
size_t shared_key_len = 0;
4570+
4571+
if (EVP_PKEY_encapsulate(
4572+
ctx.get(), nullptr, &ciphertext_len, nullptr, &shared_key_len) <= 0) {
4573+
return std::nullopt;
4574+
}
4575+
4576+
auto ciphertext = DataPointer::Alloc(ciphertext_len);
4577+
auto shared_key = DataPointer::Alloc(shared_key_len);
4578+
if (!ciphertext || !shared_key) return std::nullopt;
4579+
4580+
if (EVP_PKEY_encapsulate(ctx.get(),
4581+
static_cast<unsigned char*>(ciphertext.get()),
4582+
&ciphertext_len,
4583+
static_cast<unsigned char*>(shared_key.get()),
4584+
&shared_key_len) <= 0) {
4585+
return std::nullopt;
4586+
}
4587+
4588+
return EncapsulateResult(std::move(ciphertext), std::move(shared_key));
4589+
}
4590+
4591+
DataPointer KEM::Decapsulate(const EVPKeyPointer& private_key,
4592+
const Buffer<const void>& ciphertext) {
4593+
ClearErrorOnReturn clear_error_on_return;
4594+
4595+
auto ctx = private_key.newCtx();
4596+
if (!ctx) return {};
4597+
4598+
if (EVP_PKEY_decapsulate_init(ctx.get(), nullptr) <= 0) {
4599+
return {};
4600+
}
4601+
4602+
#if !OPENSSL_VERSION_PREREQ(3, 5)
4603+
if (!SetOperationParameter(ctx.get(), private_key)) {
4604+
return {};
4605+
}
4606+
#endif
4607+
4608+
// First pass: determine shared secret size
4609+
size_t shared_key_len = 0;
4610+
if (EVP_PKEY_decapsulate(ctx.get(),
4611+
nullptr,
4612+
&shared_key_len,
4613+
static_cast<const unsigned char*>(ciphertext.data),
4614+
ciphertext.len) <= 0) {
4615+
return {};
4616+
}
4617+
4618+
auto shared_key = DataPointer::Alloc(shared_key_len);
4619+
if (!shared_key) return {};
4620+
4621+
if (EVP_PKEY_decapsulate(ctx.get(),
4622+
static_cast<unsigned char*>(shared_key.get()),
4623+
&shared_key_len,
4624+
static_cast<const unsigned char*>(ciphertext.data),
4625+
ciphertext.len) <= 0) {
4626+
return {};
4627+
}
4628+
4629+
return shared_key;
4630+
}
4631+
4632+
#endif // OPENSSL_VERSION_MAJOR >= 3
4633+
45134634
} // namespace ncrypto

deps/ncrypto/ncrypto.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,6 +1574,40 @@ DataPointer argon2(const Buffer<const char>& pass,
15741574
#endif
15751575
#endif
15761576

1577+
// ============================================================================
1578+
// KEM (Key Encapsulation Mechanism)
1579+
#if OPENSSL_VERSION_MAJOR >= 3
1580+
1581+
class KEM final {
1582+
public:
1583+
struct EncapsulateResult {
1584+
DataPointer ciphertext;
1585+
DataPointer shared_key;
1586+
1587+
EncapsulateResult() = default;
1588+
EncapsulateResult(DataPointer ct, DataPointer sk)
1589+
: ciphertext(std::move(ct)), shared_key(std::move(sk)) {}
1590+
};
1591+
1592+
// Encapsulate a shared secret using KEM with a public key.
1593+
// Returns both the ciphertext and shared secret.
1594+
static std::optional<EncapsulateResult> Encapsulate(
1595+
const EVPKeyPointer& public_key);
1596+
1597+
// Decapsulate a shared secret using KEM with a private key and ciphertext.
1598+
// Returns the shared secret.
1599+
static DataPointer Decapsulate(const EVPKeyPointer& private_key,
1600+
const Buffer<const void>& ciphertext);
1601+
1602+
private:
1603+
#if !OPENSSL_VERSION_PREREQ(3, 5)
1604+
static bool SetOperationParameter(EVP_PKEY_CTX* ctx,
1605+
const EVPKeyPointer& key);
1606+
#endif
1607+
};
1608+
1609+
#endif // OPENSSL_VERSION_MAJOR >= 3
1610+
15771611
// ============================================================================
15781612
// Version metadata
15791613
#define NCRYPTO_VERSION "0.0.1"

0 commit comments

Comments
 (0)