Skip to content

Commit 8fa5954

Browse files
authored
crypto: use EVP_MAC for HMAC on OpenSSL >=3
Closes: #59493 Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #63942 Fixes: #59493 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent a1074b8 commit 8fa5954

12 files changed

Lines changed: 323 additions & 17 deletions

File tree

benchmark/crypto/create-hmac.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const { createHmac } = require('crypto');
5+
const assert = require('assert');
6+
7+
const bench = common.createBenchmark(main, {
8+
n: [1e5],
9+
algo: ['sha1', 'sha256', 'sha512'],
10+
keylen: [0, 16, 64, 1024],
11+
});
12+
13+
function main({ n, algo, keylen }) {
14+
const key = Buffer.alloc(keylen, 'k');
15+
const hmacs = new Array(n);
16+
17+
bench.start();
18+
for (let i = 0; i < n; ++i) {
19+
hmacs[i] = createHmac(algo, key);
20+
}
21+
bench.end(n);
22+
23+
assert.strictEqual(typeof hmacs[n - 1], 'object');
24+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Throughput benchmark
2+
// creates a single HMAC, then pushes a bunch of data through it
3+
'use strict';
4+
5+
const common = require('../common.js');
6+
const { createHmac } = require('crypto');
7+
8+
const bench = common.createBenchmark(main, {
9+
n: [500],
10+
algo: ['sha1', 'sha256', 'sha512'],
11+
keylen: [64],
12+
type: ['asc', 'utf', 'buf'],
13+
len: [2, 1024, 102400, 1024 * 1024],
14+
api: ['update', 'stream'],
15+
});
16+
17+
function main({ api, type, len, algo, keylen, n }) {
18+
let message;
19+
let encoding;
20+
switch (type) {
21+
case 'asc':
22+
message = 'a'.repeat(len);
23+
encoding = 'ascii';
24+
break;
25+
case 'utf':
26+
message = '\u00fc'.repeat(len / 2);
27+
encoding = 'utf8';
28+
break;
29+
case 'buf':
30+
message = Buffer.alloc(len, 'b');
31+
break;
32+
default:
33+
throw new Error(`unknown message type: ${type}`);
34+
}
35+
36+
const fn = api === 'stream' ? streamWrite : updateDigest;
37+
const key = Buffer.alloc(keylen, 'k');
38+
39+
bench.start();
40+
fn(algo, key, message, encoding, n, len);
41+
}
42+
43+
function updateDigest(algo, key, message, encoding, n, len) {
44+
const written = n * len;
45+
const bits = written * 8;
46+
const gbits = bits / (1024 * 1024 * 1024);
47+
const h = createHmac(algo, key);
48+
49+
while (n-- > 0)
50+
h.update(message, encoding);
51+
52+
h.digest();
53+
54+
bench.end(gbits);
55+
}
56+
57+
function streamWrite(algo, key, message, encoding, n, len) {
58+
const written = n * len;
59+
const bits = written * 8;
60+
const gbits = bits / (1024 * 1024 * 1024);
61+
const h = createHmac(algo, key);
62+
63+
while (n-- > 0)
64+
h.write(message, encoding);
65+
66+
h.end();
67+
h.read();
68+
69+
bench.end(gbits);
70+
}

benchmark/crypto/webcrypto-hmac.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const { subtle } = globalThis.crypto;
5+
6+
const signParams = { name: 'HMAC' };
7+
8+
let keys;
9+
let currentHash;
10+
let currentKeyLength;
11+
12+
const bench = common.createBenchmark(main, {
13+
hash: ['SHA-1', 'SHA-256', 'SHA-512'],
14+
mode: ['serial', 'parallel'],
15+
keyReuse: ['shared', 'unique'],
16+
keylen: [512],
17+
len: [0, 256, 4096],
18+
n: [1e3],
19+
}, {
20+
test: {
21+
keylen: 512,
22+
},
23+
combinationFilter(p) {
24+
// Unique only differs from shared when operations overlap (parallel);
25+
// sequential calls have no contention so unique+serial adds no value.
26+
if (p.keyReuse === 'unique') return p.mode === 'parallel';
27+
return true;
28+
},
29+
});
30+
31+
async function createSigningKeys(n, hash, keylen) {
32+
keys = new Array(n);
33+
currentHash = hash;
34+
currentKeyLength = keylen;
35+
36+
const algorithm = { name: 'HMAC', hash, length: keylen };
37+
const key = await subtle.generateKey(algorithm, true, ['sign']);
38+
const raw = await subtle.exportKey('raw', key);
39+
for (let i = 0; i < n; ++i) {
40+
keys[i] = await subtle.importKey('raw', raw, algorithm, false, ['sign']);
41+
}
42+
}
43+
44+
async function measureSerial(n, sharedKey, data) {
45+
bench.start();
46+
for (let i = 0; i < n; ++i) {
47+
await subtle.sign(signParams, sharedKey || keys[i], data);
48+
}
49+
bench.end(n);
50+
}
51+
52+
async function measureParallel(n, sharedKey, data) {
53+
const promises = new Array(n);
54+
bench.start();
55+
for (let i = 0; i < n; ++i) {
56+
promises[i] = subtle.sign(signParams, sharedKey || keys[i], data);
57+
}
58+
await Promise.all(promises);
59+
bench.end(n);
60+
}
61+
62+
async function main({ n, mode, keyReuse, hash, keylen, len }) {
63+
if (!keys || keys.length !== n ||
64+
currentHash !== hash || currentKeyLength !== keylen) {
65+
await createSigningKeys(n, hash, keylen);
66+
}
67+
68+
const data = new Uint8Array(len);
69+
data.fill(0x62);
70+
const sharedKey = keyReuse === 'shared' ? keys[0] : undefined;
71+
72+
switch (mode) {
73+
case 'serial':
74+
await measureSerial(n, sharedKey, data);
75+
break;
76+
case 'parallel':
77+
await measureParallel(n, sharedKey, data);
78+
break;
79+
}
80+
}

deps/ncrypto/ncrypto.cc

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4636,6 +4636,7 @@ bool extractP1363(const Buffer<const unsigned char>& buf,
46364636

46374637
// ============================================================================
46384638

4639+
#if !OPENSSL_WITH_EVP_MAC
46394640
HMACCtxPointer::HMACCtxPointer() : ctx_(nullptr) {}
46404641

46414642
HMACCtxPointer::HMACCtxPointer(HMAC_CTX* ctx) : ctx_(ctx) {}
@@ -4695,8 +4696,9 @@ bool HMACCtxPointer::digestInto(Buffer<void>* buf) {
46954696
HMACCtxPointer HMACCtxPointer::New() {
46964697
return HMACCtxPointer(HMAC_CTX_new());
46974698
}
4699+
#endif // !OPENSSL_WITH_EVP_MAC
46984700

4699-
#if OPENSSL_WITH_KMAC
4701+
#if OPENSSL_WITH_EVP_MAC
47004702
EVPMacPointer::EVPMacPointer(EVP_MAC* mac) : mac_(mac) {}
47014703

47024704
EVPMacPointer::EVPMacPointer(EVPMacPointer&& other) noexcept
@@ -4784,7 +4786,92 @@ EVPMacCtxPointer EVPMacCtxPointer::New(EVP_MAC* mac) {
47844786
if (!mac) return EVPMacCtxPointer();
47854787
return EVPMacCtxPointer(EVP_MAC_CTX_new(mac));
47864788
}
4787-
#endif // OPENSSL_WITH_KMAC
4789+
4790+
HMACCtxPointer::HMACCtxPointer() = default;
4791+
4792+
HMACCtxPointer::HMACCtxPointer(EVPMacPointer&& mac, EVPMacCtxPointer&& ctx)
4793+
: mac_(std::move(mac)), ctx_(std::move(ctx)) {}
4794+
4795+
HMACCtxPointer::HMACCtxPointer(HMACCtxPointer&& other) noexcept
4796+
: mac_(std::move(other.mac_)),
4797+
ctx_(std::move(other.ctx_)),
4798+
md_size_(other.md_size_) {
4799+
other.md_size_ = 0;
4800+
}
4801+
4802+
HMACCtxPointer& HMACCtxPointer::operator=(HMACCtxPointer&& other) noexcept {
4803+
if (this == &other) return *this;
4804+
mac_ = std::move(other.mac_);
4805+
ctx_ = std::move(other.ctx_);
4806+
md_size_ = other.md_size_;
4807+
other.md_size_ = 0;
4808+
return *this;
4809+
}
4810+
4811+
HMACCtxPointer::~HMACCtxPointer() {
4812+
reset();
4813+
}
4814+
4815+
void HMACCtxPointer::reset() {
4816+
ctx_.reset();
4817+
mac_.reset();
4818+
md_size_ = 0;
4819+
}
4820+
4821+
bool HMACCtxPointer::init(const Buffer<const void>& buf, const Digest& md) {
4822+
if (!ctx_ || !md) return false;
4823+
4824+
const char* md_name = EVP_MD_get0_name(md);
4825+
if (md_name == nullptr) return false;
4826+
4827+
OSSL_PARAM params[] = {
4828+
OSSL_PARAM_construct_utf8_string(
4829+
OSSL_MAC_PARAM_DIGEST, const_cast<char*>(md_name), 0),
4830+
OSSL_PARAM_construct_end(),
4831+
};
4832+
4833+
if (!ctx_.init(buf, params)) return false;
4834+
md_size_ = md.size();
4835+
return true;
4836+
}
4837+
4838+
bool HMACCtxPointer::update(const Buffer<const void>& buf) {
4839+
if (!ctx_) return false;
4840+
return ctx_.update(buf);
4841+
}
4842+
4843+
DataPointer HMACCtxPointer::digest() {
4844+
if (md_size_ == 0) return {};
4845+
auto data = DataPointer::Alloc(md_size_);
4846+
if (!data) return {};
4847+
Buffer<void> buf = data;
4848+
if (!digestInto(&buf)) return {};
4849+
return data.resize(buf.len);
4850+
}
4851+
4852+
bool HMACCtxPointer::digestInto(Buffer<void>* buf) {
4853+
if (!ctx_) return false;
4854+
4855+
size_t len = buf->len;
4856+
if (EVP_MAC_final(
4857+
ctx_.get(), static_cast<unsigned char*>(buf->data), &len, buf->len) !=
4858+
1)
4859+
return false;
4860+
4861+
buf->len = len;
4862+
return true;
4863+
}
4864+
4865+
HMACCtxPointer HMACCtxPointer::New() {
4866+
auto mac = EVPMacPointer::Fetch(OSSL_MAC_NAME_HMAC);
4867+
if (!mac) return {};
4868+
4869+
auto ctx = EVPMacCtxPointer::New(mac.get());
4870+
if (!ctx) return {};
4871+
4872+
return HMACCtxPointer(std::move(mac), std::move(ctx));
4873+
}
4874+
#endif // OPENSSL_WITH_EVP_MAC
47884875

47894876
DataPointer hashDigest(const Buffer<const unsigned char>& buf,
47904877
const EVP_MD* md) {

deps/ncrypto/ncrypto.h

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@
6565
#endif
6666

6767
#if OPENSSL_VERSION_PREREQ(3, 0)
68-
#define OPENSSL_WITH_KMAC 1
68+
#define OPENSSL_WITH_EVP_MAC 1
6969
#else
70-
#define OPENSSL_WITH_KMAC 0
70+
#define OPENSSL_WITH_EVP_MAC 0
7171
#endif
7272

7373
#if defined(OPENSSL_IS_BORINGSSL) || OPENSSL_VERSION_PREREQ(3, 2)
@@ -1529,6 +1529,7 @@ class EVPMDCtxPointer final {
15291529
DeleteFnPtr<EVP_MD_CTX, EVP_MD_CTX_free> ctx_;
15301530
};
15311531

1532+
#if !OPENSSL_WITH_EVP_MAC
15321533
class HMACCtxPointer final {
15331534
public:
15341535
HMACCtxPointer();
@@ -1555,8 +1556,9 @@ class HMACCtxPointer final {
15551556
private:
15561557
DeleteFnPtr<HMAC_CTX, HMAC_CTX_free> ctx_;
15571558
};
1559+
#endif // !OPENSSL_WITH_EVP_MAC
15581560

1559-
#if OPENSSL_WITH_KMAC
1561+
#if OPENSSL_WITH_EVP_MAC
15601562
class EVPMacPointer final {
15611563
public:
15621564
EVPMacPointer() = default;
@@ -1604,7 +1606,34 @@ class EVPMacCtxPointer final {
16041606
private:
16051607
DeleteFnPtr<EVP_MAC_CTX, EVP_MAC_CTX_free> ctx_;
16061608
};
1607-
#endif // OPENSSL_WITH_KMAC
1609+
1610+
class HMACCtxPointer final {
1611+
public:
1612+
HMACCtxPointer();
1613+
HMACCtxPointer(HMACCtxPointer&& other) noexcept;
1614+
HMACCtxPointer& operator=(HMACCtxPointer&& other) noexcept;
1615+
NCRYPTO_DISALLOW_COPY(HMACCtxPointer)
1616+
~HMACCtxPointer();
1617+
1618+
inline bool operator==(std::nullptr_t) noexcept { return ctx_ == nullptr; }
1619+
inline operator bool() const { return ctx_ != nullptr; }
1620+
void reset();
1621+
1622+
bool init(const Buffer<const void>& buf, const Digest& md);
1623+
bool update(const Buffer<const void>& buf);
1624+
DataPointer digest();
1625+
bool digestInto(Buffer<void>* buf);
1626+
1627+
static HMACCtxPointer New();
1628+
1629+
private:
1630+
HMACCtxPointer(EVPMacPointer&& mac, EVPMacCtxPointer&& ctx);
1631+
1632+
EVPMacPointer mac_;
1633+
EVPMacCtxPointer ctx_;
1634+
size_t md_size_ = 0;
1635+
};
1636+
#endif // OPENSSL_WITH_EVP_MAC
16081637

16091638
#ifndef OPENSSL_NO_ENGINE
16101639
class EnginePointer final {

src/crypto/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,18 @@ using ECPointPointer = DeleteFnPtr<EC_POINT, EC_POINT_free>;
9090
using ECKeyPointer = DeleteFnPtr<EC_KEY, EC_KEY_free>;
9191
using DHPointer = DeleteFnPtr<DH, DH_free>;
9292
using ECDSASigPointer = DeleteFnPtr<ECDSA_SIG, ECDSA_SIG_free>;
93-
using HMACCtxPointer = DeleteFnPtr<HMAC_CTX, HMAC_CTX_free>;
9493
using CipherCtxPointer = DeleteFnPtr<EVP_CIPHER_CTX, EVP_CIPHER_CTX_free>;
9594
```
9695

9796
Examples of these being used are pervasive through the `src/crypto` code.
9897

98+
`HMACCtxPointer` is a dedicated HMAC state wrapper rather than a plain
99+
`DeleteFnPtr` alias. On OpenSSL 3 and later it owns the provider-backed
100+
`EVP_MAC`/`EVP_MAC_CTX` state. On OpenSSL 1.1.1 and BoringSSL it owns the
101+
legacy `HMAC_CTX` state. HMAC call sites should use `HMACCtxPointer::New()`,
102+
`init()`, `update()`, and `digest()`/`digestInto()` so the backend selection
103+
stays contained in ncrypto.
104+
99105
### `ByteSource`
100106

101107
The `ByteSource` class is a helper utility representing a _read-only_ byte

src/crypto/crypto_hmac.cc

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ using v8::Uint32;
3030
using v8::Value;
3131

3232
namespace crypto {
33-
Hmac::Hmac(Environment* env, Local<Object> wrap)
34-
: BaseObject(env, wrap),
35-
ctx_(nullptr) {
33+
Hmac::Hmac(Environment* env, Local<Object> wrap) : BaseObject(env, wrap) {
3634
MakeWeak();
3735
}
3836

0 commit comments

Comments
 (0)