Skip to content

Commit

Permalink
crypto: implement basic secure heap support
Browse files Browse the repository at this point in the history
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: #36779
Refs: #36729
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
  • Loading branch information
jasnell authored and danielleadams committed Jan 12, 2021
1 parent 42aca13 commit 53cf996
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 2 deletions.
37 changes: 37 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -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.
.
Expand Down
2 changes: 2 additions & 0 deletions lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ const {
setDefaultEncoding,
setEngine,
lazyRequire,
secureHeapUsed,
} = require('internal/crypto/util');
const Certificate = require('internal/crypto/certificate');

Expand Down Expand Up @@ -230,6 +231,7 @@ module.exports = {
Sign,
Verify,
X509Certificate,
secureHeapUsed,
};

function setFipsDisabled() {
Expand Down
16 changes: 16 additions & 0 deletions lib/internal/crypto/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const {
ArrayPrototypeIncludes,
ArrayPrototypePush,
FunctionPrototypeBind,
Number,
Promise,
StringPrototypeToLowerCase,
StringPrototypeToUpperCase,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -402,4 +417,5 @@ module.exports = {
getStringOption,
getUsagesUnion,
getHashLength,
secureHeapUsed,
};
28 changes: 28 additions & 0 deletions src/crypto/crypto_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ namespace node {

using v8::ArrayBuffer;
using v8::BackingStore;
using v8::BigInt;
using v8::Context;
using v8::Exception;
using v8::FunctionCallbackInfo;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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

Expand Down
24 changes: 24 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#include <errno.h>
#include <sstream>
#include <limits>
#include <algorithm>
#include <cstdlib> // strtoul, errno

using v8::Boolean;
Expand Down Expand Up @@ -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" &&
Expand Down Expand Up @@ -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 "
Expand Down
2 changes: 2 additions & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions test/parallel/test-crypto-secure-heap.js
Original file line number Diff line number Diff line change
@@ -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);
}
10 changes: 8 additions & 2 deletions test/parallel/test-process-env-allowed-flags-are-documented.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 53cf996

Please sign in to comment.