Skip to content

Commit 53cf996

Browse files
jasnelldanielleadams
authored andcommitted
crypto: implement basic secure heap support
Adds two new command line arguments: * `--secure-heap=n`, which causes node.js to initialize an openssl secure heap of `n` bytes on openssl initialization. * `--secure-heap-min=n`, which specifies the minimum allocation from the secure heap. * A new method `crypto.secureHeapUsed()` that returns details about the total and used secure heap allocation. The secure heap is an openssl feature that allows certain kinds of potentially sensitive information (such as private key BigNums) to be allocated from a dedicated memory area that is protected against pointer over- and underruns. The secure heap is a fixed size, so it's important that users pick a large enough size to cover the crypto operations they intend to utilize. The secure heap is disabled by default. Signed-off-by: James M Snell <[email protected]> PR-URL: #36779 Refs: #36729 Reviewed-By: Tobias Nießen <[email protected]>
1 parent 42aca13 commit 53cf996

10 files changed

+211
-2
lines changed

doc/api/cli.md

+37
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,40 @@ Enables report to be generated on uncaught exceptions. Useful when inspecting
848848
the JavaScript stack in conjunction with native stack and other runtime
849849
environment data.
850850

851+
### `--secure-heap=n`
852+
<!-- YAML
853+
added: REPLACEME
854+
-->
855+
856+
Initializes an OpenSSL secure heap of `n` bytes. When initialized, the
857+
secure heap is used for selected types of allocations within OpenSSL
858+
during key generation and other operations. This is useful, for instance,
859+
to prevent sensitive information from leaking due to pointer overruns
860+
or underruns.
861+
862+
The secure heap is a fixed size and cannot be resized at runtime so,
863+
if used, it is important to select a large enough heap to cover all
864+
application uses.
865+
866+
The heap size given must be a power of two. Any value less than 2
867+
will disable the secure heap.
868+
869+
The secure heap is disabled by default.
870+
871+
The secure heap is not available on Windows.
872+
873+
See [`CRYPTO_secure_malloc_init`][] for more details.
874+
875+
### `--secure-heap-min=n`
876+
<!-- YAML
877+
added: REPLACEME
878+
-->
879+
880+
When using `--secure-heap`, the `--secure-heap-min` flag specifies the
881+
minimum allocation from the secure heap. The minimum value is `2`.
882+
The maximum value is the lesser of `--secure-heap` or `2147483647`.
883+
The value given must be a power of two.
884+
851885
### `--throw-deprecation`
852886
<!-- YAML
853887
added: v0.11.14
@@ -1361,6 +1395,8 @@ Node.js options that are allowed are:
13611395
* `--report-signal`
13621396
* `--report-uncaught-exception`
13631397
* `--require`, `-r`
1398+
* `--secure-heap-min`
1399+
* `--secure-heap`
13641400
* `--throw-deprecation`
13651401
* `--title`
13661402
* `--tls-cipher-list`
@@ -1659,6 +1695,7 @@ $ node --max-old-space-size=1536 index.js
16591695
[`--openssl-config`]: #cli_openssl_config_file
16601696
[`Atomics.wait()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait
16611697
[`Buffer`]: buffer.md#buffer_class_buffer
1698+
[`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man1.1.0/man3/CRYPTO_secure_malloc_init.html
16621699
[`NODE_OPTIONS`]: #cli_node_options_options
16631700
[`SlowBuffer`]: buffer.md#buffer_class_slowbuffer
16641701
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#process_process_setuncaughtexceptioncapturecallback_fn

doc/api/crypto.md

+15
Original file line numberDiff line numberDiff line change
@@ -3545,6 +3545,21 @@ const key2 = crypto.scryptSync('password', 'salt', 64, { N: 1024 });
35453545
console.log(key2.toString('hex')); // '3745e48...aa39b34'
35463546
```
35473547

3548+
### `crypto.secureHeapUsed()`
3549+
<!-- YAML
3550+
added: REPLACEME
3551+
-->
3552+
3553+
* Returns: {Object}
3554+
* `total` {number} The total allocated secure heap size as specified
3555+
using the `--secure-heap=n` command-line flag.
3556+
* `min` {number} The minimum allocation from the secure heap as
3557+
specified using the `--secure-heap-min` command-line flag.
3558+
* `used` {number} The total number of bytes currently allocated from
3559+
the secure heap.
3560+
* `utilization` {number} The calculated ratio of `used` to `total`
3561+
allocated bytes.
3562+
35483563
### `crypto.setEngine(engine[, flags])`
35493564
<!-- YAML
35503565
added: v0.11.11

doc/node.1

+7
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,13 @@ Enables
359359
to be generated on un-caught exceptions. Useful when inspecting JavaScript
360360
stack in conjunction with native stack and other runtime environment data.
361361
.
362+
.It Fl -secure-heap Ns = Ns Ar n
363+
Specify the size of the OpenSSL secure heap. Any value less than 2 disables
364+
the secure heap. The default is 0. The value must be a power of two.
365+
.
366+
.It Fl -secure-heap-min Ns = Ns Ar n
367+
Specify the minimum allocation from the OpenSSL secure heap. The default is 2. The value must be a power of two.
368+
.
362369
.It Fl -throw-deprecation
363370
Throw errors for deprecations.
364371
.

lib/crypto.js

+2
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ const {
118118
setDefaultEncoding,
119119
setEngine,
120120
lazyRequire,
121+
secureHeapUsed,
121122
} = require('internal/crypto/util');
122123
const Certificate = require('internal/crypto/certificate');
123124

@@ -230,6 +231,7 @@ module.exports = {
230231
Sign,
231232
Verify,
232233
X509Certificate,
234+
secureHeapUsed,
233235
};
234236

235237
function setFipsDisabled() {

lib/internal/crypto/util.js

+16
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const {
44
ArrayPrototypeIncludes,
55
ArrayPrototypePush,
66
FunctionPrototypeBind,
7+
Number,
78
Promise,
89
StringPrototypeToLowerCase,
910
StringPrototypeToUpperCase,
@@ -15,8 +16,11 @@ const {
1516
getCurves: _getCurves,
1617
getHashes: _getHashes,
1718
setEngine: _setEngine,
19+
secureHeapUsed: _secureHeapUsed,
1820
} = internalBinding('crypto');
1921

22+
const { getOptionValue } = require('internal/options');
23+
2024
const {
2125
crypto: {
2226
ENGINE_METHOD_ALL
@@ -371,6 +375,17 @@ function validateKeyOps(keyOps, usagesSet) {
371375
}
372376
}
373377

378+
function secureHeapUsed() {
379+
const val = _secureHeapUsed();
380+
if (val === undefined)
381+
return { total: 0, used: 0, utilization: 0, min: 0 };
382+
const used = Number(_secureHeapUsed());
383+
const total = Number(getOptionValue('--secure-heap'));
384+
const min = Number(getOptionValue('--secure-heap-min'));
385+
const utilization = used / total;
386+
return { total, used, utilization, min };
387+
}
388+
374389
module.exports = {
375390
getArrayBufferOrView,
376391
getCiphers,
@@ -402,4 +417,5 @@ module.exports = {
402417
getStringOption,
403418
getUsagesUnion,
404419
getHashLength,
420+
secureHeapUsed,
405421
};

src/crypto/crypto_util.cc

+28
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ namespace node {
1818

1919
using v8::ArrayBuffer;
2020
using v8::BackingStore;
21+
using v8::BigInt;
2122
using v8::Context;
2223
using v8::Exception;
2324
using v8::FunctionCallbackInfo;
@@ -113,6 +114,25 @@ void InitCryptoOnce() {
113114
settings = nullptr;
114115
#endif
115116

117+
#ifndef _WIN32
118+
if (per_process::cli_options->secure_heap != 0) {
119+
switch (CRYPTO_secure_malloc_init(
120+
per_process::cli_options->secure_heap,
121+
static_cast<int>(per_process::cli_options->secure_heap_min))) {
122+
case 0:
123+
fprintf(stderr, "Unable to initialize openssl secure heap.\n");
124+
break;
125+
case 2:
126+
// Not a fatal error but worthy of a warning.
127+
fprintf(stderr, "Unable to memory map openssl secure heap.\n");
128+
break;
129+
case 1:
130+
// OK!
131+
break;
132+
}
133+
}
134+
#endif
135+
116136
#ifdef NODE_FIPS_MODE
117137
/* Override FIPS settings in cnf file, if needed. */
118138
unsigned long err = 0; // NOLINT(runtime/int)
@@ -617,6 +637,13 @@ void SecureBuffer(const FunctionCallbackInfo<Value>& args) {
617637
Local<ArrayBuffer> buffer = ArrayBuffer::New(env->isolate(), store);
618638
args.GetReturnValue().Set(Uint8Array::New(buffer, 0, len));
619639
}
640+
641+
void SecureHeapUsed(const FunctionCallbackInfo<Value>& args) {
642+
Environment* env = Environment::GetCurrent(args);
643+
if (CRYPTO_secure_malloc_initialized())
644+
args.GetReturnValue().Set(
645+
BigInt::New(env->isolate(), CRYPTO_secure_used()));
646+
}
620647
} // namespace
621648

622649
namespace Util {
@@ -634,6 +661,7 @@ void Initialize(Environment* env, Local<Object> target) {
634661
NODE_DEFINE_CONSTANT(target, kCryptoJobSync);
635662

636663
env->SetMethod(target, "secureBuffer", SecureBuffer);
664+
env->SetMethod(target, "secureHeapUsed", SecureHeapUsed);
637665
}
638666
} // namespace Util
639667

src/node_options.cc

+24
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
#include <errno.h>
99
#include <sstream>
10+
#include <limits>
11+
#include <algorithm>
1012
#include <cstdlib> // strtoul, errno
1113

1214
using v8::Boolean;
@@ -64,6 +66,20 @@ void PerProcessOptions::CheckOptions(std::vector<std::string>* errors) {
6466
errors->push_back("either --use-openssl-ca or --use-bundled-ca can be "
6567
"used, not both");
6668
}
69+
70+
// Any value less than 2 disables use of the secure heap.
71+
if (secure_heap >= 2) {
72+
if ((secure_heap & (secure_heap - 1)) != 0)
73+
errors->push_back("--secure-heap must be a power of 2");
74+
secure_heap_min =
75+
std::min({
76+
secure_heap,
77+
secure_heap_min,
78+
static_cast<int64_t>(std::numeric_limits<int>::max())});
79+
secure_heap_min = std::max(static_cast<int64_t>(2), secure_heap_min);
80+
if ((secure_heap_min & (secure_heap_min - 1)) != 0)
81+
errors->push_back("--secure-heap-min must be a power of 2");
82+
}
6783
#endif
6884
if (use_largepages != "off" &&
6985
use_largepages != "on" &&
@@ -760,6 +776,14 @@ PerProcessOptionsParser::PerProcessOptionsParser(
760776
&PerProcessOptions::force_fips_crypto,
761777
kAllowedInEnvironment);
762778
#endif
779+
AddOption("--secure-heap",
780+
"total size of the OpenSSL secure heap",
781+
&PerProcessOptions::secure_heap,
782+
kAllowedInEnvironment);
783+
AddOption("--secure-heap-min",
784+
"minimum allocation size from the OpenSSL secure heap",
785+
&PerProcessOptions::secure_heap_min,
786+
kAllowedInEnvironment);
763787
#endif
764788
AddOption("--use-largepages",
765789
"Map the Node.js static code to large pages. Options are "

src/node_options.h

+2
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ class PerProcessOptions : public Options {
236236
#if HAVE_OPENSSL
237237
std::string openssl_config;
238238
std::string tls_cipher_list = DEFAULT_CIPHER_LIST_CORE;
239+
int64_t secure_heap = 0;
240+
int64_t secure_heap_min = 2;
239241
#ifdef NODE_OPENSSL_CERT_STORE
240242
bool ssl_openssl_cert_store = true;
241243
#else
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
if (common.isWindows)
8+
common.skip('Not supported on Windows');
9+
10+
const assert = require('assert');
11+
const { fork } = require('child_process');
12+
const fixtures = require('../common/fixtures');
13+
const {
14+
secureHeapUsed,
15+
createDiffieHellman,
16+
} = require('crypto');
17+
18+
if (process.argv[2] === 'child') {
19+
20+
const a = secureHeapUsed();
21+
22+
assert(a);
23+
assert.strictEqual(typeof a, 'object');
24+
assert.strictEqual(a.total, 65536);
25+
assert.strictEqual(a.min, 4);
26+
assert.strictEqual(a.used, 0);
27+
28+
{
29+
const dh1 = createDiffieHellman(common.hasFipsCrypto ? 1024 : 256);
30+
const p1 = dh1.getPrime('buffer');
31+
const dh2 = createDiffieHellman(p1, 'buffer');
32+
const key1 = dh1.generateKeys();
33+
const key2 = dh2.generateKeys('hex');
34+
dh1.computeSecret(key2, 'hex', 'base64');
35+
dh2.computeSecret(key1, 'latin1', 'buffer');
36+
37+
const b = secureHeapUsed();
38+
assert(b);
39+
assert.strictEqual(typeof b, 'object');
40+
assert.strictEqual(b.total, 65536);
41+
assert.strictEqual(b.min, 4);
42+
// The amount used can vary on a number of factors
43+
assert(b.used > 0);
44+
assert(b.utilization > 0.0);
45+
}
46+
47+
return;
48+
}
49+
50+
const child = fork(
51+
process.argv[1],
52+
['child'],
53+
{ execArgv: ['--secure-heap=65536', '--secure-heap-min=4'] });
54+
55+
child.on('exit', common.mustCall((code) => {
56+
assert.strictEqual(code, 0);
57+
}));
58+
59+
{
60+
const child = fork(fixtures.path('a.js'), {
61+
execArgv: ['--secure-heap=3', '--secure-heap-min=3'],
62+
stdio: 'pipe'
63+
});
64+
let res = '';
65+
child.on('exit', common.mustCall((code) => {
66+
assert.notStrictEqual(code, 0);
67+
assert.match(res, /--secure-heap must be a power of 2/);
68+
assert.match(res, /--secure-heap-min must be a power of 2/);
69+
}));
70+
child.stderr.setEncoding('utf8');
71+
child.stderr.on('data', (chunk) => res += chunk);
72+
}

test/parallel/test-process-env-allowed-flags-are-documented.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,14 @@ for (const line of [...nodeOptionsLines, ...v8OptionsLines]) {
4343
const conditionalOpts = [
4444
{ include: common.hasCrypto,
4545
filter: (opt) => {
46-
return ['--openssl-config', '--tls-cipher-list', '--use-bundled-ca',
47-
'--use-openssl-ca' ].includes(opt);
46+
return [
47+
'--openssl-config',
48+
'--tls-cipher-list',
49+
'--use-bundled-ca',
50+
'--use-openssl-ca',
51+
'--secure-heap',
52+
'--secure-heap-min',
53+
].includes(opt);
4854
} },
4955
{
5056
// We are using openssl_is_fips from the configuration because it could be

0 commit comments

Comments
 (0)