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 <jasnell@gmail.com>

PR-URL: https://github.com/nodejs/node/pull/36779
Refs: https://github.com/nodejs/node/pull/36729
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
This commit is contained in:
James M Snell 2021-01-04 09:06:26 -08:00
parent b4378aa38a
commit 03c056401f
No known key found for this signature in database
GPG Key ID: 7341B15C070877AC
10 changed files with 211 additions and 2 deletions

View File

@ -848,6 +848,40 @@ Enables report to be generated on uncaught exceptions. Useful when inspecting
the JavaScript stack in conjunction with native stack and other runtime
environment data.
### `--secure-heap=n`
<!-- YAML
added: REPLACEME
-->
Initializes an OpenSSL secure heap of `n` bytes. When initialized, the
secure heap is used for selected types of allocations within OpenSSL
during key generation and other operations. This is useful, for instance,
to prevent sensitive information from leaking due to pointer overruns
or underruns.
The secure heap is a fixed size and cannot be resized at runtime so,
if used, it is important to select a large enough heap to cover all
application uses.
The heap size given must be a power of two. Any value less than 2
will disable the secure heap.
The secure heap is disabled by default.
The secure heap is not available on Windows.
See [`CRYPTO_secure_malloc_init`][] for more details.
### `--secure-heap-min=n`
<!-- YAML
added: REPLACEME
-->
When using `--secure-heap`, the `--secure-heap-min` flag specifies the
minimum allocation from the secure heap. The minimum value is `2`.
The maximum value is the lesser of `--secure-heap` or `2147483647`.
The value given must be a power of two.
### `--throw-deprecation`
<!-- YAML
added: v0.11.14
@ -1361,6 +1395,8 @@ Node.js options that are allowed are:
* `--report-signal`
* `--report-uncaught-exception`
* `--require`, `-r`
* `--secure-heap-min`
* `--secure-heap`
* `--throw-deprecation`
* `--title`
* `--tls-cipher-list`
@ -1659,6 +1695,7 @@ $ node --max-old-space-size=1536 index.js
[`--openssl-config`]: #cli_openssl_config_file
[`Atomics.wait()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait
[`Buffer`]: buffer.md#buffer_class_buffer
[`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man1.1.0/man3/CRYPTO_secure_malloc_init.html
[`NODE_OPTIONS`]: #cli_node_options_options
[`SlowBuffer`]: buffer.md#buffer_class_slowbuffer
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#process_process_setuncaughtexceptioncapturecallback_fn

View File

@ -3545,6 +3545,21 @@ const key2 = crypto.scryptSync('password', 'salt', 64, { N: 1024 });
console.log(key2.toString('hex')); // '3745e48...aa39b34'
```
### `crypto.secureHeapUsed()`
<!-- YAML
added: REPLACEME
-->
* Returns: {Object}
* `total` {number} The total allocated secure heap size as specified
using the `--secure-heap=n` command-line flag.
* `min` {number} The minimum allocation from the secure heap as
specified using the `--secure-heap-min` command-line flag.
* `used` {number} The total number of bytes currently allocated from
the secure heap.
* `utilization` {number} The calculated ratio of `used` to `total`
allocated bytes.
### `crypto.setEngine(engine[, flags])`
<!-- YAML
added: v0.11.11

View File

@ -359,6 +359,13 @@ Enables
to be generated on un-caught exceptions. Useful when inspecting JavaScript
stack in conjunction with native stack and other runtime environment data.
.
.It Fl -secure-heap Ns = Ns Ar n
Specify the size of the OpenSSL secure heap. Any value less than 2 disables
the secure heap. The default is 0. The value must be a power of two.
.
.It Fl -secure-heap-min Ns = Ns Ar n
Specify the minimum allocation from the OpenSSL secure heap. The default is 2. The value must be a power of two.
.
.It Fl -throw-deprecation
Throw errors for deprecations.
.

View File

@ -118,6 +118,7 @@ const {
setDefaultEncoding,
setEngine,
lazyRequire,
secureHeapUsed,
} = require('internal/crypto/util');
const Certificate = require('internal/crypto/certificate');
@ -230,6 +231,7 @@ module.exports = {
Sign,
Verify,
X509Certificate,
secureHeapUsed,
};
function setFipsDisabled() {

View File

@ -4,6 +4,7 @@ const {
ArrayPrototypeIncludes,
ArrayPrototypePush,
FunctionPrototypeBind,
Number,
Promise,
StringPrototypeToLowerCase,
StringPrototypeToUpperCase,
@ -15,8 +16,11 @@ const {
getCurves: _getCurves,
getHashes: _getHashes,
setEngine: _setEngine,
secureHeapUsed: _secureHeapUsed,
} = internalBinding('crypto');
const { getOptionValue } = require('internal/options');
const {
crypto: {
ENGINE_METHOD_ALL
@ -371,6 +375,17 @@ function validateKeyOps(keyOps, usagesSet) {
}
}
function secureHeapUsed() {
const val = _secureHeapUsed();
if (val === undefined)
return { total: 0, used: 0, utilization: 0, min: 0 };
const used = Number(_secureHeapUsed());
const total = Number(getOptionValue('--secure-heap'));
const min = Number(getOptionValue('--secure-heap-min'));
const utilization = used / total;
return { total, used, utilization, min };
}
module.exports = {
getArrayBufferOrView,
getCiphers,
@ -402,4 +417,5 @@ module.exports = {
getStringOption,
getUsagesUnion,
getHashLength,
secureHeapUsed,
};

View File

@ -18,6 +18,7 @@ namespace node {
using v8::ArrayBuffer;
using v8::BackingStore;
using v8::BigInt;
using v8::Context;
using v8::Exception;
using v8::FunctionCallbackInfo;
@ -113,6 +114,25 @@ void InitCryptoOnce() {
settings = nullptr;
#endif
#ifndef _WIN32
if (per_process::cli_options->secure_heap != 0) {
switch (CRYPTO_secure_malloc_init(
per_process::cli_options->secure_heap,
static_cast<int>(per_process::cli_options->secure_heap_min))) {
case 0:
fprintf(stderr, "Unable to initialize openssl secure heap.\n");
break;
case 2:
// Not a fatal error but worthy of a warning.
fprintf(stderr, "Unable to memory map openssl secure heap.\n");
break;
case 1:
// OK!
break;
}
}
#endif
#ifdef NODE_FIPS_MODE
/* Override FIPS settings in cnf file, if needed. */
unsigned long err = 0; // NOLINT(runtime/int)
@ -617,6 +637,13 @@ void SecureBuffer(const FunctionCallbackInfo<Value>& args) {
Local<ArrayBuffer> buffer = ArrayBuffer::New(env->isolate(), store);
args.GetReturnValue().Set(Uint8Array::New(buffer, 0, len));
}
void SecureHeapUsed(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (CRYPTO_secure_malloc_initialized())
args.GetReturnValue().Set(
BigInt::New(env->isolate(), CRYPTO_secure_used()));
}
} // namespace
namespace Util {
@ -634,6 +661,7 @@ void Initialize(Environment* env, Local<Object> target) {
NODE_DEFINE_CONSTANT(target, kCryptoJobSync);
env->SetMethod(target, "secureBuffer", SecureBuffer);
env->SetMethod(target, "secureHeapUsed", SecureHeapUsed);
}
} // namespace Util

View File

@ -7,6 +7,8 @@
#include <errno.h>
#include <sstream>
#include <limits>
#include <algorithm>
#include <cstdlib> // strtoul, errno
using v8::Boolean;
@ -64,6 +66,20 @@ void PerProcessOptions::CheckOptions(std::vector<std::string>* errors) {
errors->push_back("either --use-openssl-ca or --use-bundled-ca can be "
"used, not both");
}
// Any value less than 2 disables use of the secure heap.
if (secure_heap >= 2) {
if ((secure_heap & (secure_heap - 1)) != 0)
errors->push_back("--secure-heap must be a power of 2");
secure_heap_min =
std::min({
secure_heap,
secure_heap_min,
static_cast<int64_t>(std::numeric_limits<int>::max())});
secure_heap_min = std::max(static_cast<int64_t>(2), secure_heap_min);
if ((secure_heap_min & (secure_heap_min - 1)) != 0)
errors->push_back("--secure-heap-min must be a power of 2");
}
#endif
if (use_largepages != "off" &&
use_largepages != "on" &&
@ -760,6 +776,14 @@ PerProcessOptionsParser::PerProcessOptionsParser(
&PerProcessOptions::force_fips_crypto,
kAllowedInEnvironment);
#endif
AddOption("--secure-heap",
"total size of the OpenSSL secure heap",
&PerProcessOptions::secure_heap,
kAllowedInEnvironment);
AddOption("--secure-heap-min",
"minimum allocation size from the OpenSSL secure heap",
&PerProcessOptions::secure_heap_min,
kAllowedInEnvironment);
#endif
AddOption("--use-largepages",
"Map the Node.js static code to large pages. Options are "

View File

@ -236,6 +236,8 @@ class PerProcessOptions : public Options {
#if HAVE_OPENSSL
std::string openssl_config;
std::string tls_cipher_list = DEFAULT_CIPHER_LIST_CORE;
int64_t secure_heap = 0;
int64_t secure_heap_min = 2;
#ifdef NODE_OPENSSL_CERT_STORE
bool ssl_openssl_cert_store = true;
#else

View File

@ -0,0 +1,72 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
if (common.isWindows)
common.skip('Not supported on Windows');
const assert = require('assert');
const { fork } = require('child_process');
const fixtures = require('../common/fixtures');
const {
secureHeapUsed,
createDiffieHellman,
} = require('crypto');
if (process.argv[2] === 'child') {
const a = secureHeapUsed();
assert(a);
assert.strictEqual(typeof a, 'object');
assert.strictEqual(a.total, 65536);
assert.strictEqual(a.min, 4);
assert.strictEqual(a.used, 0);
{
const dh1 = createDiffieHellman(common.hasFipsCrypto ? 1024 : 256);
const p1 = dh1.getPrime('buffer');
const dh2 = createDiffieHellman(p1, 'buffer');
const key1 = dh1.generateKeys();
const key2 = dh2.generateKeys('hex');
dh1.computeSecret(key2, 'hex', 'base64');
dh2.computeSecret(key1, 'latin1', 'buffer');
const b = secureHeapUsed();
assert(b);
assert.strictEqual(typeof b, 'object');
assert.strictEqual(b.total, 65536);
assert.strictEqual(b.min, 4);
// The amount used can vary on a number of factors
assert(b.used > 0);
assert(b.utilization > 0.0);
}
return;
}
const child = fork(
process.argv[1],
['child'],
{ execArgv: ['--secure-heap=65536', '--secure-heap-min=4'] });
child.on('exit', common.mustCall((code) => {
assert.strictEqual(code, 0);
}));
{
const child = fork(fixtures.path('a.js'), {
execArgv: ['--secure-heap=3', '--secure-heap-min=3'],
stdio: 'pipe'
});
let res = '';
child.on('exit', common.mustCall((code) => {
assert.notStrictEqual(code, 0);
assert.match(res, /--secure-heap must be a power of 2/);
assert.match(res, /--secure-heap-min must be a power of 2/);
}));
child.stderr.setEncoding('utf8');
child.stderr.on('data', (chunk) => res += chunk);
}

View File

@ -43,8 +43,14 @@ for (const line of [...nodeOptionsLines, ...v8OptionsLines]) {
const conditionalOpts = [
{ include: common.hasCrypto,
filter: (opt) => {
return ['--openssl-config', '--tls-cipher-list', '--use-bundled-ca',
'--use-openssl-ca' ].includes(opt);
return [
'--openssl-config',
'--tls-cipher-list',
'--use-bundled-ca',
'--use-openssl-ca',
'--secure-heap',
'--secure-heap-min',
].includes(opt);
} },
{
// We are using openssl_is_fips from the configuration because it could be