Skip to content
Open
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
117 changes: 117 additions & 0 deletions benchmark/crypto/ecdh-compute-secret.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
'use strict';

const common = require('../common.js');
const assert = require('node:assert');
const crypto = require('node:crypto');

const kCurve = 'prime256v1';
const kPeerPoolSize = 32;
const scenarios = [
'first-after-generate',
'full-lifecycle',
'reused-local-same-peer',
'reused-local-peer-pool',
];

const bench = common.createBenchmark(main, {
scenario: scenarios,
n: [5_000],
}, {
test: { scenario: 'first-after-generate', n: 1 },
});

function generateContext() {
const context = crypto.createECDH(kCurve);
context.generateKeys();
return context;
}

function verifySecret(secret, local, peer) {
assert.deepStrictEqual(secret, peer.computeSecret(local.getPublicKey()));
}

function firstAfterGenerate(n) {
const peer = generateContext();
const peerPublicKey = peer.getPublicKey();
const warmup = generateContext();
warmup.computeSecret(peerPublicKey);

const locals = Array.from({ length: n }, generateContext);
const secrets = new Array(n);

bench.start();
for (let i = 0; i < n; i++)
secrets[i] = locals[i].computeSecret(peerPublicKey);
bench.end(n);

verifySecret(secrets[n - 1], locals[n - 1], peer);
}

function fullLifecycle(n) {
const peer = generateContext();
const peerPublicKey = peer.getPublicKey();
const warmup = generateContext();
warmup.computeSecret(peerPublicKey);

const locals = new Array(n);
const secrets = new Array(n);

bench.start();
for (let i = 0; i < n; i++) {
const local = locals[i] = generateContext();
secrets[i] = local.computeSecret(peerPublicKey);
}
bench.end(n);

verifySecret(secrets[n - 1], locals[n - 1], peer);
}

function reusedLocalSamePeer(n) {
const local = generateContext();
const peer = generateContext();
const peerPublicKey = peer.getPublicKey();
local.computeSecret(peerPublicKey);

const secrets = new Array(n);

bench.start();
for (let i = 0; i < n; i++)
secrets[i] = local.computeSecret(peerPublicKey);
bench.end(n);

verifySecret(secrets[n - 1], local, peer);
}

function reusedLocalPeerPool(n) {
const local = generateContext();
const peers = Array.from(
{ length: Math.min(n, kPeerPoolSize) },
generateContext);
const peerPublicKeys = peers.map((peer) => peer.getPublicKey());
local.computeSecret(peerPublicKeys[0]);

const secrets = new Array(n);

bench.start();
for (let i = 0; i < n; i++)
secrets[i] = local.computeSecret(peerPublicKeys[i % peers.length]);
bench.end(n);

const lastPeer = peers[(n - 1) % peers.length];
verifySecret(secrets[n - 1], local, lastPeer);
}

function main({ scenario, n }) {
switch (scenario) {
case 'first-after-generate':
return firstAfterGenerate(n);
case 'full-lifecycle':
return fullLifecycle(n);
case 'reused-local-same-peer':
return reusedLocalSamePeer(n);
case 'reused-local-peer-pool':
return reusedLocalPeerPool(n);
default:
throw new Error(`Unsupported scenario: ${scenario}`);
}
}
20 changes: 19 additions & 1 deletion src/crypto/crypto_ec.cc
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,12 @@ void ECDH::GenerateKeys(const FunctionCallbackInfo<Value>& args) {
ECDH* ecdh;
ASSIGN_OR_RETURN_UNWRAP(&ecdh, args.This());

const uint64_t generation = ncrypto::getFipsStateGeneration();
ecdh->has_valid_key_pair_ = false;
if (!ecdh->key_.generate()) {
return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to generate key");
}
ecdh->MaybeCacheValidKeyPair(generation);
}

ECPointPointer ECDH::BufferToPoint(Environment* env,
Expand Down Expand Up @@ -307,6 +310,7 @@ void ECDH::SetPrivateKey(const FunctionCallbackInfo<Value>& args) {

ecdh->key_ = std::move(new_key);
ecdh->group_ = ecdh->key_.getGroup();
ecdh->has_valid_key_pair_ = false;
}

void ECDH::SetPublicKey(const FunctionCallbackInfo<Value>& args) {
Expand All @@ -325,6 +329,7 @@ void ECDH::SetPublicKey(const FunctionCallbackInfo<Value>& args) {
"Failed to convert Buffer to EC_POINT");
}

ecdh->has_valid_key_pair_ = false;
if (!ecdh->key_.setPublicKey(pub)) {
return THROW_ERR_CRYPTO_OPERATION_FAILED(env,
"Failed to set EC_POINT as the public key");
Expand All @@ -345,9 +350,22 @@ bool ECDH::IsKeyValidForCurve(const BignumPointer& private_key) {
private_key < order;
}

void ECDH::MaybeCacheValidKeyPair(uint64_t generation) {
has_valid_key_pair_ = generation == ncrypto::getFipsStateGeneration();
if (has_valid_key_pair_) valid_key_pair_generation_ = generation;
}

bool ECDH::IsKeyPairValid() {
const uint64_t generation = ncrypto::getFipsStateGeneration();
if (has_valid_key_pair_ && valid_key_pair_generation_ == generation) {
return true;
}
has_valid_key_pair_ = false;

MarkPopErrorOnReturn mark_pop_error_on_return;
return key_.checkKey();
const bool is_valid = key_.checkKey();
if (is_valid) MaybeCacheValidKeyPair(generation);
return is_valid;
}

// Convert the input public key to compressed, uncompressed, or hybrid formats.
Expand Down
3 changes: 3 additions & 0 deletions src/crypto/crypto_ec.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,14 @@ class ECDH final : public BaseObject {
static void GetPublicKey(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetPublicKey(const v8::FunctionCallbackInfo<v8::Value>& args);

void MaybeCacheValidKeyPair(uint64_t generation);
bool IsKeyPairValid();
bool IsKeyValidForCurve(const ncrypto::BignumPointer& private_key);

ncrypto::ECKeyPointer key_;
const EC_GROUP* group_;
bool has_valid_key_pair_ = false;
uint64_t valid_key_pair_generation_ = 0;
};

struct EcKeyPairParams final : public MemoryRetainer {
Expand Down
36 changes: 36 additions & 0 deletions test/parallel/test-crypto-dh-curves.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,15 @@ if (availableCurves.has('prime256v1') && availableCurves.has('secp256k1')) {
ecdh4.setPrivateKey(ecdh1.getPrivateKey());
ecdh4.setPublicKey(ecdh1.getPublicKey());

const ecdh4Secret = ecdh4.computeSecret(ecdh2.getPublicKey());
assert.deepStrictEqual(ecdh4.computeSecret(ecdh2.getPublicKey()),
ecdh4Secret);

assert.throws(() => {
ecdh4.setPublicKey(ecdh3.getPublicKey());
}, { message: 'Failed to convert Buffer to EC_POINT' });
assert.deepStrictEqual(ecdh4.computeSecret(ecdh2.getPublicKey()),
ecdh4Secret);

// Verify that we can use ECDH without having to use newly generated keys.
const ecdh5 = crypto.createECDH('secp256k1');
Expand Down Expand Up @@ -190,6 +196,8 @@ if (availableCurves.has('prime256v1') && availableCurves.has('secp256k1')) {
sharedSecret);
assert.strictEqual(ecdh5.computeSecret(peerPubPtUnComp, 'hex', 'hex'),
sharedSecret);
assert.strictEqual(ecdh5.computeSecret(peerPubPtComp, 'hex', 'hex'),
sharedSecret);

// Verify that we still have the same key pair as before the computation.
assert.strictEqual(ecdh5.getPrivateKey('hex'), cafebabeKey);
Expand Down Expand Up @@ -260,3 +268,31 @@ if (availableCurves.has('prime256v1') && availableHashes.has('sha256')) {
'-----END EC PRIVATE KEY-----';
crypto.createSign('SHA256').sign(ecPrivateKey);
}

if (hasFIPS(3) && availableCurves.has('secp256k1')) {
const originalFips = crypto.getFips();

try {
crypto.setFips(0);
const local = crypto.createECDH('secp256k1');
const peer = crypto.createECDH('secp256k1');
local.generateKeys();
const peerPublicKey = peer.generateKeys();

local.computeSecret(peerPublicKey);
crypto.setFips(1);
assert.throws(() => local.computeSecret(peerPublicKey), {
code: 'ERR_CRYPTO_INVALID_KEYPAIR',
name: 'RangeError',
});

const installed = crypto.createECDH('secp256k1');
installed.setPrivateKey(Buffer.from('cafebabe'.repeat(8), 'hex'));
assert.throws(() => installed.computeSecret(peerPublicKey), {
code: 'ERR_CRYPTO_INVALID_KEYPAIR',
name: 'RangeError',
});
} finally {
crypto.setFips(originalFips);
}
}
Loading