diff --git a/benchmark/crypto/ecdh-compute-secret.js b/benchmark/crypto/ecdh-compute-secret.js new file mode 100644 index 000000000000..3061c5b2cc36 --- /dev/null +++ b/benchmark/crypto/ecdh-compute-secret.js @@ -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}`); + } +} diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index b08d4b7a9106..d6920c8b30e2 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -134,9 +134,12 @@ void ECDH::GenerateKeys(const FunctionCallbackInfo& 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, @@ -307,6 +310,7 @@ void ECDH::SetPrivateKey(const FunctionCallbackInfo& args) { ecdh->key_ = std::move(new_key); ecdh->group_ = ecdh->key_.getGroup(); + ecdh->has_valid_key_pair_ = false; } void ECDH::SetPublicKey(const FunctionCallbackInfo& args) { @@ -325,6 +329,7 @@ void ECDH::SetPublicKey(const FunctionCallbackInfo& 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"); @@ -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. diff --git a/src/crypto/crypto_ec.h b/src/crypto/crypto_ec.h index 9bc817f3d7f5..3e83d541d7ae 100644 --- a/src/crypto/crypto_ec.h +++ b/src/crypto/crypto_ec.h @@ -48,11 +48,14 @@ class ECDH final : public BaseObject { static void GetPublicKey(const v8::FunctionCallbackInfo& args); static void SetPublicKey(const v8::FunctionCallbackInfo& 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 { diff --git a/test/parallel/test-crypto-dh-curves.js b/test/parallel/test-crypto-dh-curves.js index ee8849163ae8..f7530aecfc5d 100644 --- a/test/parallel/test-crypto-dh-curves.js +++ b/test/parallel/test-crypto-dh-curves.js @@ -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'); @@ -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); @@ -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); + } +}