diff --git a/benchmark/crypto/create-hmac.js b/benchmark/crypto/create-hmac.js new file mode 100644 index 00000000000000..a6c9b546756d4d --- /dev/null +++ b/benchmark/crypto/create-hmac.js @@ -0,0 +1,24 @@ +'use strict'; + +const common = require('../common.js'); +const { createHmac } = require('crypto'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + n: [1e5], + algo: ['sha1', 'sha256', 'sha512'], + keylen: [0, 16, 64, 1024], +}); + +function main({ n, algo, keylen }) { + const key = Buffer.alloc(keylen, 'k'); + const hmacs = new Array(n); + + bench.start(); + for (let i = 0; i < n; ++i) { + hmacs[i] = createHmac(algo, key); + } + bench.end(n); + + assert.strictEqual(typeof hmacs[n - 1], 'object'); +} diff --git a/benchmark/crypto/hmac-throughput.js b/benchmark/crypto/hmac-throughput.js new file mode 100644 index 00000000000000..bc1d2d13c4636a --- /dev/null +++ b/benchmark/crypto/hmac-throughput.js @@ -0,0 +1,70 @@ +// Throughput benchmark +// creates a single HMAC, then pushes a bunch of data through it +'use strict'; + +const common = require('../common.js'); +const { createHmac } = require('crypto'); + +const bench = common.createBenchmark(main, { + n: [500], + algo: ['sha1', 'sha256', 'sha512'], + keylen: [64], + type: ['asc', 'utf', 'buf'], + len: [2, 1024, 102400, 1024 * 1024], + api: ['update', 'stream'], +}); + +function main({ api, type, len, algo, keylen, n }) { + let message; + let encoding; + switch (type) { + case 'asc': + message = 'a'.repeat(len); + encoding = 'ascii'; + break; + case 'utf': + message = '\u00fc'.repeat(len / 2); + encoding = 'utf8'; + break; + case 'buf': + message = Buffer.alloc(len, 'b'); + break; + default: + throw new Error(`unknown message type: ${type}`); + } + + const fn = api === 'stream' ? streamWrite : updateDigest; + const key = Buffer.alloc(keylen, 'k'); + + bench.start(); + fn(algo, key, message, encoding, n, len); +} + +function updateDigest(algo, key, message, encoding, n, len) { + const written = n * len; + const bits = written * 8; + const gbits = bits / (1024 * 1024 * 1024); + const h = createHmac(algo, key); + + while (n-- > 0) + h.update(message, encoding); + + h.digest(); + + bench.end(gbits); +} + +function streamWrite(algo, key, message, encoding, n, len) { + const written = n * len; + const bits = written * 8; + const gbits = bits / (1024 * 1024 * 1024); + const h = createHmac(algo, key); + + while (n-- > 0) + h.write(message, encoding); + + h.end(); + h.read(); + + bench.end(gbits); +} diff --git a/benchmark/crypto/webcrypto-hmac.js b/benchmark/crypto/webcrypto-hmac.js new file mode 100644 index 00000000000000..643d44747b7943 --- /dev/null +++ b/benchmark/crypto/webcrypto-hmac.js @@ -0,0 +1,80 @@ +'use strict'; + +const common = require('../common.js'); +const { subtle } = globalThis.crypto; + +const signParams = { name: 'HMAC' }; + +let keys; +let currentHash; +let currentKeyLength; + +const bench = common.createBenchmark(main, { + hash: ['SHA-1', 'SHA-256', 'SHA-512'], + mode: ['serial', 'parallel'], + keyReuse: ['shared', 'unique'], + keylen: [512], + len: [0, 256, 4096], + n: [1e3], +}, { + test: { + keylen: 512, + }, + combinationFilter(p) { + // Unique only differs from shared when operations overlap (parallel); + // sequential calls have no contention so unique+serial adds no value. + if (p.keyReuse === 'unique') return p.mode === 'parallel'; + return true; + }, +}); + +async function createSigningKeys(n, hash, keylen) { + keys = new Array(n); + currentHash = hash; + currentKeyLength = keylen; + + const algorithm = { name: 'HMAC', hash, length: keylen }; + const key = await subtle.generateKey(algorithm, true, ['sign']); + const raw = await subtle.exportKey('raw', key); + for (let i = 0; i < n; ++i) { + keys[i] = await subtle.importKey('raw', raw, algorithm, false, ['sign']); + } +} + +async function measureSerial(n, sharedKey, data) { + bench.start(); + for (let i = 0; i < n; ++i) { + await subtle.sign(signParams, sharedKey || keys[i], data); + } + bench.end(n); +} + +async function measureParallel(n, sharedKey, data) { + const promises = new Array(n); + bench.start(); + for (let i = 0; i < n; ++i) { + promises[i] = subtle.sign(signParams, sharedKey || keys[i], data); + } + await Promise.all(promises); + bench.end(n); +} + +async function main({ n, mode, keyReuse, hash, keylen, len }) { + if (!keys || keys.length !== n || + currentHash !== hash || currentKeyLength !== keylen) { + await createSigningKeys(n, hash, keylen); + } + + const data = new Uint8Array(len); + data.fill(0x62); + const sharedKey = keyReuse === 'shared' ? keys[0] : undefined; + + switch (mode) { + case 'serial': + await measureSerial(n, sharedKey, data); + break; + case 'parallel': + await measureParallel(n, sharedKey, data); + break; + } +} diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 38378b730aca66..c0587bff836a11 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -4629,6 +4629,7 @@ bool extractP1363(const Buffer& buf, // ============================================================================ +#if !OPENSSL_WITH_EVP_MAC HMACCtxPointer::HMACCtxPointer() : ctx_(nullptr) {} HMACCtxPointer::HMACCtxPointer(HMAC_CTX* ctx) : ctx_(ctx) {} @@ -4688,8 +4689,9 @@ bool HMACCtxPointer::digestInto(Buffer* buf) { HMACCtxPointer HMACCtxPointer::New() { return HMACCtxPointer(HMAC_CTX_new()); } +#endif // !OPENSSL_WITH_EVP_MAC -#if OPENSSL_WITH_KMAC +#if OPENSSL_WITH_EVP_MAC EVPMacPointer::EVPMacPointer(EVP_MAC* mac) : mac_(mac) {} EVPMacPointer::EVPMacPointer(EVPMacPointer&& other) noexcept @@ -4777,7 +4779,92 @@ EVPMacCtxPointer EVPMacCtxPointer::New(EVP_MAC* mac) { if (!mac) return EVPMacCtxPointer(); return EVPMacCtxPointer(EVP_MAC_CTX_new(mac)); } -#endif // OPENSSL_WITH_KMAC + +HMACCtxPointer::HMACCtxPointer() = default; + +HMACCtxPointer::HMACCtxPointer(EVPMacPointer&& mac, EVPMacCtxPointer&& ctx) + : mac_(std::move(mac)), ctx_(std::move(ctx)) {} + +HMACCtxPointer::HMACCtxPointer(HMACCtxPointer&& other) noexcept + : mac_(std::move(other.mac_)), + ctx_(std::move(other.ctx_)), + md_size_(other.md_size_) { + other.md_size_ = 0; +} + +HMACCtxPointer& HMACCtxPointer::operator=(HMACCtxPointer&& other) noexcept { + if (this == &other) return *this; + mac_ = std::move(other.mac_); + ctx_ = std::move(other.ctx_); + md_size_ = other.md_size_; + other.md_size_ = 0; + return *this; +} + +HMACCtxPointer::~HMACCtxPointer() { + reset(); +} + +void HMACCtxPointer::reset() { + ctx_.reset(); + mac_.reset(); + md_size_ = 0; +} + +bool HMACCtxPointer::init(const Buffer& buf, const Digest& md) { + if (!ctx_ || !md) return false; + + const char* md_name = EVP_MD_get0_name(md); + if (md_name == nullptr) return false; + + OSSL_PARAM params[] = { + OSSL_PARAM_construct_utf8_string( + OSSL_MAC_PARAM_DIGEST, const_cast(md_name), 0), + OSSL_PARAM_construct_end(), + }; + + if (!ctx_.init(buf, params)) return false; + md_size_ = md.size(); + return true; +} + +bool HMACCtxPointer::update(const Buffer& buf) { + if (!ctx_) return false; + return ctx_.update(buf); +} + +DataPointer HMACCtxPointer::digest() { + if (md_size_ == 0) return {}; + auto data = DataPointer::Alloc(md_size_); + if (!data) return {}; + Buffer buf = data; + if (!digestInto(&buf)) return {}; + return data.resize(buf.len); +} + +bool HMACCtxPointer::digestInto(Buffer* buf) { + if (!ctx_) return false; + + size_t len = buf->len; + if (EVP_MAC_final( + ctx_.get(), static_cast(buf->data), &len, buf->len) != + 1) + return false; + + buf->len = len; + return true; +} + +HMACCtxPointer HMACCtxPointer::New() { + auto mac = EVPMacPointer::Fetch(OSSL_MAC_NAME_HMAC); + if (!mac) return {}; + + auto ctx = EVPMacCtxPointer::New(mac.get()); + if (!ctx) return {}; + + return HMACCtxPointer(std::move(mac), std::move(ctx)); +} +#endif // OPENSSL_WITH_EVP_MAC DataPointer hashDigest(const Buffer& buf, const EVP_MD* md) { diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index b27e2e76c3dcfc..14088d8391fe52 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -65,9 +65,9 @@ #endif #if OPENSSL_VERSION_PREREQ(3, 0) -#define OPENSSL_WITH_KMAC 1 +#define OPENSSL_WITH_EVP_MAC 1 #else -#define OPENSSL_WITH_KMAC 0 +#define OPENSSL_WITH_EVP_MAC 0 #endif #if defined(OPENSSL_IS_BORINGSSL) || OPENSSL_VERSION_PREREQ(3, 2) @@ -1528,6 +1528,7 @@ class EVPMDCtxPointer final { DeleteFnPtr ctx_; }; +#if !OPENSSL_WITH_EVP_MAC class HMACCtxPointer final { public: HMACCtxPointer(); @@ -1554,8 +1555,9 @@ class HMACCtxPointer final { private: DeleteFnPtr ctx_; }; +#endif // !OPENSSL_WITH_EVP_MAC -#if OPENSSL_WITH_KMAC +#if OPENSSL_WITH_EVP_MAC class EVPMacPointer final { public: EVPMacPointer() = default; @@ -1603,7 +1605,34 @@ class EVPMacCtxPointer final { private: DeleteFnPtr ctx_; }; -#endif // OPENSSL_WITH_KMAC + +class HMACCtxPointer final { + public: + HMACCtxPointer(); + HMACCtxPointer(HMACCtxPointer&& other) noexcept; + HMACCtxPointer& operator=(HMACCtxPointer&& other) noexcept; + NCRYPTO_DISALLOW_COPY(HMACCtxPointer) + ~HMACCtxPointer(); + + inline bool operator==(std::nullptr_t) noexcept { return ctx_ == nullptr; } + inline operator bool() const { return ctx_ != nullptr; } + void reset(); + + bool init(const Buffer& buf, const Digest& md); + bool update(const Buffer& buf); + DataPointer digest(); + bool digestInto(Buffer* buf); + + static HMACCtxPointer New(); + + private: + HMACCtxPointer(EVPMacPointer&& mac, EVPMacCtxPointer&& ctx); + + EVPMacPointer mac_; + EVPMacCtxPointer ctx_; + size_t md_size_ = 0; +}; +#endif // OPENSSL_WITH_EVP_MAC #ifndef OPENSSL_NO_ENGINE class EnginePointer final { diff --git a/src/crypto/README.md b/src/crypto/README.md index 4059ae23711b84..8d66f34c6c29b9 100644 --- a/src/crypto/README.md +++ b/src/crypto/README.md @@ -90,12 +90,18 @@ using ECPointPointer = DeleteFnPtr; using ECKeyPointer = DeleteFnPtr; using DHPointer = DeleteFnPtr; using ECDSASigPointer = DeleteFnPtr; -using HMACCtxPointer = DeleteFnPtr; using CipherCtxPointer = DeleteFnPtr; ``` Examples of these being used are pervasive through the `src/crypto` code. +`HMACCtxPointer` is a dedicated HMAC state wrapper rather than a plain +`DeleteFnPtr` alias. On OpenSSL 3 and later it owns the provider-backed +`EVP_MAC`/`EVP_MAC_CTX` state. On OpenSSL 1.1.1 and BoringSSL it owns the +legacy `HMAC_CTX` state. HMAC call sites should use `HMACCtxPointer::New()`, +`init()`, `update()`, and `digest()`/`digestInto()` so the backend selection +stays contained in ncrypto. + ### `ByteSource` The `ByteSource` class is a helper utility representing a _read-only_ byte diff --git a/src/crypto/crypto_hmac.cc b/src/crypto/crypto_hmac.cc index acd4b819de38fc..42f3b53da0eaec 100644 --- a/src/crypto/crypto_hmac.cc +++ b/src/crypto/crypto_hmac.cc @@ -30,9 +30,7 @@ using v8::Uint32; using v8::Value; namespace crypto { -Hmac::Hmac(Environment* env, Local wrap) - : BaseObject(env, wrap), - ctx_(nullptr) { +Hmac::Hmac(Environment* env, Local wrap) : BaseObject(env, wrap) { MakeWeak(); } diff --git a/src/crypto/crypto_kmac.cc b/src/crypto/crypto_kmac.cc index 1b685bb5f6983c..01e9e85092aedc 100644 --- a/src/crypto/crypto_kmac.cc +++ b/src/crypto/crypto_kmac.cc @@ -3,7 +3,7 @@ #include "node_internals.h" #include "threadpoolwork-inl.h" -#if OPENSSL_WITH_KMAC +#if OPENSSL_WITH_EVP_MAC #include #include #include "crypto/crypto_keys.h" @@ -220,4 +220,4 @@ void Kmac::RegisterExternalReferences(ExternalReferenceRegistry* registry) { } // namespace node::crypto -#endif // OPENSSL_WITH_KMAC +#endif // OPENSSL_WITH_EVP_MAC diff --git a/src/crypto/crypto_kmac.h b/src/crypto/crypto_kmac.h index 5a8c9e5039f22b..fa44cc8d80cde6 100644 --- a/src/crypto/crypto_kmac.h +++ b/src/crypto/crypto_kmac.h @@ -10,7 +10,7 @@ namespace node::crypto { -#if OPENSSL_WITH_KMAC +#if OPENSSL_WITH_EVP_MAC enum class KmacVariant { KMAC128, KMAC256 }; @@ -71,7 +71,7 @@ namespace Kmac { void Initialize(Environment* env, v8::Local target) {} void RegisterExternalReferences(ExternalReferenceRegistry* registry) {} } // namespace Kmac -#endif // OPENSSL_WITH_KMAC +#endif // OPENSSL_WITH_EVP_MAC } // namespace node::crypto diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 91d80e0dd379ba..7d52d835ca4cb3 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -73,11 +73,11 @@ namespace crypto { #define KEM_NAMESPACE_LIST(V) #endif -#if OPENSSL_WITH_KMAC +#if OPENSSL_WITH_EVP_MAC #define KMAC_NAMESPACE_LIST(V) V(Kmac) #else #define KMAC_NAMESPACE_LIST(V) -#endif // OPENSSL_WITH_KMAC +#endif // OPENSSL_WITH_EVP_MAC #define TURBOSHAKE_NAMESPACE_LIST(V) V(TurboShake) diff --git a/src/node_crypto.h b/src/node_crypto.h index ecc2b8c6a358c8..3bb95cb340d2ce 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -43,7 +43,7 @@ #if OPENSSL_WITH_KEM #include "crypto/crypto_kem.h" #endif -#if OPENSSL_WITH_KMAC +#if OPENSSL_WITH_EVP_MAC #include "crypto/crypto_kmac.h" #endif #include "crypto/crypto_keygen.h" diff --git a/test/parallel/test-crypto-hmac.js b/test/parallel/test-crypto-hmac.js index 7af6ce37b578e2..9ddc4a4b880f2a 100644 --- a/test/parallel/test-crypto-hmac.js +++ b/test/parallel/test-crypto-hmac.js @@ -61,6 +61,18 @@ function testHmac(algo, key, data, expected) { '19fd6e1ba73d9ed2224dd5094a71babe85d9a892'); } +{ + // Historically, dss1 and DSS1 are SHA-1 aliases. + const expected = + crypto.createHmac('sha1', 'key').update('data').digest('hex'); + + for (const algo of ['dss1', 'DSS1']) { + assert.strictEqual( + crypto.createHmac(algo, 'key').update('data').digest('hex'), + expected); + } +} + // Test HMAC (Wikipedia Test Cases) const wikipedia = [ {