Skip to content

Commit 1309975

Browse files
panvanodejs-github-bot
authored andcommitted
crypto: add strict mode to --force-fips
Allow --force-fips to take an optional provider or strict mode. Keep provider as the current default and explicit compatibility mode. In strict mode, return failure from the OpenSSL 3.4+ FIPS indicator callback when it reports a non-approved operation. Install the callback for enforcement without implicitly enabling diagnostics; --enable-fips-indicator-events remains the independent observation opt-in. Keep the bare form mapped to provider for compatibility, allowing the default to change to strict in a future major release. Preserve the mode when forwarding flags to child test processes, and keep the parser's internal mode storage out of process.allowedNodeEnvironmentFlags. Document the callback scope limitations and cover provider compatibility, validation, synchronous crypto, WebCrypto, Workers, and opt-in event publication. Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: #65645 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent d5d203a commit 1309975

11 files changed

Lines changed: 278 additions & 13 deletions

File tree

doc/api/cli.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1597,15 +1597,32 @@ added: v12.12.0
15971597

15981598
Disable loading native addons that are not [context-aware][].
15991599

1600-
### `--force-fips`
1600+
### `--force-fips[=mode]`
16011601

16021602
<!-- YAML
16031603
added: v6.0.0
1604+
changes:
1605+
- version: REPLACEME
1606+
pr-url: https://github.com/nodejs/node/pull/65645
1607+
description: Added the optional `provider` and `strict` modes.
16041608
-->
16051609

16061610
Enable [FIPS mode][] at startup and prevent it from being disabled from script
16071611
code. The same OpenSSL requirements as [`--enable-fips`][] apply.
16081612

1613+
An optional mode can be specified using `--force-fips=mode`:
1614+
1615+
* `provider`: Preserve the OpenSSL FIPS provider's configured handling of
1616+
non-approved operations. This is the current default when the mode is
1617+
omitted.
1618+
* `strict`: Reject non-approved operations reported through the OpenSSL FIPS
1619+
indicator callback. This mode requires OpenSSL 3.4 or later.
1620+
1621+
The `strict` mode only covers operations reported through the callback for
1622+
OpenSSL's default library context. It does not cover native addons that use
1623+
another `OSSL_LIB_CTX` or another copy of `libcrypto`, nor operation-specific
1624+
indicators that do not invoke the callback.
1625+
16091626
### `--force-node-api-uncaught-exceptions-policy`
16101627

16111628
<!-- YAML

doc/api/crypto.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7278,7 +7278,9 @@ startup. The following controls are also available:
72787278
* [`--enable-fips`][] and [`--force-fips`][] enable the property query and
72797279
additionally require the configured provider named `fips` to initialize and
72807280
pass its self-test. Node.js exits if that check fails. `--force-fips` also
7281-
prevents FIPS mode from being disabled from script code.
7281+
prevents FIPS mode from being disabled from script code. With
7282+
`--force-fips=strict`, Node.js also rejects non-approved operations reported
7283+
through the OpenSSL FIPS indicator callback.
72827284
* [`crypto.setFips()`][] changes the FIPS/property-query state. On OpenSSL 3, it
72837285
does not install, load, initialize, or validate a provider. Implementations
72847286
fetched before the call are not changed.

doc/api/diagnostics_channel.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,8 +1583,7 @@ added: REPLACEME
15831583
* `operation` {string} The provider-defined operation type.
15841584
* `reason` {string} The provider-defined description of why the operation is
15851585
not approved.
1586-
* `blocked` {boolean} Whether a native indicator callback installed before
1587-
Node.js blocked the operation.
1586+
* `blocked` {boolean} Whether an indicator callback blocked the operation.
15881587
* `count` {number} The number of matching pending indicator invocations
15891588
represented by this message.
15901589
* `dropped` {number} The number of additional indicator invocations dropped
@@ -1602,7 +1601,10 @@ messages are published.
16021601

16031602
Subscribing to the channel is observation-only and never changes the result of
16041603
an operation. Node.js preserves the result from any native indicator callback
1605-
installed before Node.js initializes its crypto support.
1604+
installed before Node.js initializes its crypto support. When
1605+
[`--force-fips=strict`][] is used, Node.js rejects callback-indicated
1606+
non-approved operations whether or not indicator events are enabled or the
1607+
channel has subscribers.
16061608

16071609
Messages are published asynchronously on the main thread because OpenSSL
16081610
indicators can originate from Workers or other threads. Only subscriptions on
@@ -2028,6 +2030,7 @@ statement, since both are still in use while the event is being delivered; see
20282030
[TracingChannel Channels]: #tracingchannel-channels
20292031
[`'uncaughtException'`]: process.md#event-uncaughtexception
20302032
[`--enable-fips-indicator-events`]: cli.md#--enable-fips-indicator-events
2033+
[`--force-fips=strict`]: cli.md#--force-fips
20312034
[`BoundedChannel`]: #class-boundedchannel
20322035
[`DatabaseSync`]: sqlite.md#class-databasesync
20332036
[`TracingChannel`]: #class-tracingchannel

doc/node.1

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -847,9 +847,23 @@ Enable experimental support for the worker inspection with Chrome DevTools.
847847
.It Fl -force-context-aware
848848
Disable loading native addons that are not context-aware.
849849
.
850-
.It Fl -force-fips
850+
.It Fl -force-fips Ns = Ns Ar [mode]
851851
Enable FIPS mode at startup and prevent it from being disabled from script
852852
code. The same OpenSSL requirements as \fB--enable-fips\fR apply.
853+
An optional mode can be specified using \fB--force-fips=mode\fR:
854+
.Bl -bullet
855+
.It
856+
\fBprovider\fR: Preserve the OpenSSL FIPS provider's configured handling of
857+
non-approved operations. This is the current default when the mode is
858+
omitted.
859+
.It
860+
\fBstrict\fR: Reject non-approved operations reported through the OpenSSL FIPS
861+
indicator callback. This mode requires OpenSSL 3.4 or later.
862+
.El
863+
The \fBstrict\fR mode only covers operations reported through the callback for
864+
OpenSSL's default library context. It does not cover native addons that use
865+
another \fBOSSL_LIB_CTX\fR or another copy of \fBlibcrypto\fR, nor operation-specific
866+
indicators that do not invoke the callback.
853867
.
854868
.It Fl -force-node-api-uncaught-exceptions-policy
855869
Enforces \fBuncaughtException\fR event on Node-API asynchronous callbacks.

lib/internal/process/per_thread.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,9 @@ function buildAllowedFlags() {
392392

393393
const allowedNodeEnvironmentFlags = [];
394394
for (const { 0: name, 1: info } of options) {
395+
// Bracketed options are internal parser targets. They can be allowed in
396+
// NODE_OPTIONS so aliases expand to them, but are not public flags.
397+
if (name[0] === '[') continue;
395398
if (info.envVarSettings === kAllowedInEnvvar) {
396399
ArrayPrototypePush(allowedNodeEnvironmentFlags, name);
397400
if (info.type === kBoolean) {

src/crypto/crypto_util.cc

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ class FipsIndicatorState final {
254254
}
255255

256256
void Install() {
257+
reject_unapproved_.store(
258+
per_process::cli_options->force_fips_crypto &&
259+
per_process::cli_options->force_fips_crypto_policy == "strict",
260+
std::memory_order_release);
257261
std::call_once(install_once_, [this]() {
258262
OSSL_INDICATOR_get_callback(nullptr, &previous_callback_);
259263
OSSL_INDICATOR_set_callback(nullptr, OnOpenSSLIndicator);
@@ -314,9 +318,12 @@ class FipsIndicatorState final {
314318
previous_callback_ == nullptr
315319
? 1
316320
: previous_callback_(operation, reason, params);
317-
if (!active_.load(std::memory_order_acquire)) return previous_result;
321+
const int result = reject_unapproved_.load(std::memory_order_acquire)
322+
? 0
323+
: previous_result;
324+
if (!active_.load(std::memory_order_acquire)) return result;
318325

319-
const bool blocked = previous_result == 0;
326+
const bool blocked = result == 0;
320327
{
321328
Mutex::ScopedLock lock(mutex_);
322329
if (env_ != nullptr && active_.load(std::memory_order_relaxed)) {
@@ -349,7 +356,7 @@ class FipsIndicatorState final {
349356
}
350357
}
351358
}
352-
return previous_result;
359+
return result;
353360
}
354361

355362
void Drain(Environment* env) {
@@ -404,6 +411,7 @@ class FipsIndicatorState final {
404411

405412
std::once_flag install_once_;
406413
std::atomic<bool> active_{false};
414+
std::atomic<bool> reject_unapproved_{false};
407415
OSSL_INDICATOR_CALLBACK* previous_callback_ = nullptr;
408416
Mutex mutex_;
409417
Environment* env_ = nullptr;
@@ -419,7 +427,10 @@ class FipsIndicatorState final {
419427

420428
void InstallFipsIndicatorCallback() {
421429
#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(3, 4)
422-
if (per_process::cli_options->enable_fips_indicator_events) {
430+
const auto& options = per_process::cli_options;
431+
const bool strict = options->force_fips_crypto &&
432+
options->force_fips_crypto_policy == "strict";
433+
if (options->enable_fips_indicator_events || strict) {
423434
FipsIndicatorState::Get().Install();
424435
}
425436
#endif

src/node.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1247,13 +1247,13 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
12471247
OPENSSL_init();
12481248
}
12491249
#endif
1250-
crypto::InstallFipsIndicatorCallback();
12511250
if (auto fips_error = crypto::ProcessFipsOptions()) {
12521251
result->exit_code_ = ExitCode::kGenericUserError;
12531252
result->early_return_ = true;
12541253
result->errors_.emplace_back(std::move(*fips_error));
12551254
return result;
12561255
}
1256+
crypto::InstallFipsIndicatorCallback();
12571257

12581258
// Ensure CSPRNG is properly seeded.
12591259
CHECK(ncrypto::CSPRNG(nullptr, 0));

src/node_options.cc

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,21 @@ void PerProcessOptions::CheckOptions(std::vector<std::string>* errors,
8484
"used, not both");
8585
}
8686

87+
if (force_fips_crypto_policy != "provider" &&
88+
force_fips_crypto_policy != "strict") {
89+
errors->push_back(
90+
"invalid value for --force-fips; expected 'provider' or 'strict'");
91+
}
92+
8793
#if defined(OPENSSL_IS_BORINGSSL) || !OPENSSL_VERSION_PREREQ(3, 4)
8894
if (enable_fips_indicator_events) {
8995
errors->push_back(
9096
"--enable-fips-indicator-events requires OpenSSL 3.4 or later");
9197
}
98+
99+
if (force_fips_crypto && force_fips_crypto_policy == "strict") {
100+
errors->push_back("--force-fips=strict requires OpenSSL 3.4 or later");
101+
}
92102
#endif
93103

94104
// Any value less than 2 disables use of the secure heap.
@@ -1495,9 +1505,14 @@ PerProcessOptionsParser::PerProcessOptionsParser(
14951505
BOOL_FIELD(enable_fips_indicator_events),
14961506
kAllowedInEnvvar);
14971507
AddOption("--force-fips",
1498-
"force FIPS crypto (cannot be disabled)",
1508+
"force FIPS crypto (optional mode: provider or strict)",
14991509
BOOL_FIELD(force_fips_crypto),
15001510
kAllowedInEnvvar);
1511+
AddOption("[force_fips_crypto_policy]",
1512+
"",
1513+
&PerProcessOptions::force_fips_crypto_policy,
1514+
kAllowedInEnvvar);
1515+
AddAlias("--force-fips=", {"[force_fips_crypto_policy]", "--force-fips"});
15011516
#ifndef V8_ENABLE_SANDBOX
15021517
AddOption("--secure-heap",
15031518
"total size of the OpenSSL secure heap",
@@ -2097,6 +2112,12 @@ void GetOptionsAsFlags(const FunctionCallbackInfo<Value>& args) {
20972112
switch (option_info.type) {
20982113
case kBoolean: {
20992114
bool current_value = field->GetBool(opts);
2115+
#if HAVE_OPENSSL
2116+
if (option_name == "--force-fips" && current_value) {
2117+
flags.push_back(option_name + "=" + opts->force_fips_crypto_policy);
2118+
break;
2119+
}
2120+
#endif
21002121
// For boolean options with default_is_true, we want the opposite logic
21012122
if (option_info.default_is_true) {
21022123
if (!current_value) {

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ class PerProcessOptions : public Options {
411411
DEFINE_BOOL_FIELD(enable_fips_crypto) = false;
412412
DEFINE_BOOL_FIELD(enable_fips_indicator_events) = false;
413413
DEFINE_BOOL_FIELD(force_fips_crypto) = false;
414+
std::string force_fips_crypto_policy = "provider";
414415
#endif // HAVE_OPENSSL
415416
#if OPENSSL_VERSION_MAJOR >= 3
416417
DEFINE_BOOL_FIELD(openssl_legacy_provider) = false;
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
if (!common.hasCrypto) {
6+
common.skip('missing crypto');
7+
}
8+
9+
if (process.features.openssl_is_boringssl) {
10+
common.skip('BoringSSL does not support FIPS');
11+
}
12+
13+
const assert = require('node:assert');
14+
const diagnosticsChannel = require('node:diagnostics_channel');
15+
const { once } = require('node:events');
16+
const { createHmac, subtle } = require('node:crypto');
17+
const { Worker } = require('node:worker_threads');
18+
const {
19+
spawnSyncAndExitWithoutError,
20+
} = require('../common/child_process');
21+
const { hasFIPS, hasOpenSSL } = require('../common/crypto');
22+
23+
const channelName = 'crypto.fips.indicator';
24+
const mode = process.env.NODE_TEST_FIPS_FORCE_MODE;
25+
26+
if (!hasOpenSSL(3, 4)) {
27+
common.skip('OpenSSL 3.4 or later is required');
28+
} else if (!hasFIPS(3, 4)) {
29+
common.skip('an active OpenSSL 3.4+ FIPS provider is required');
30+
} else if (mode === 'provider') {
31+
assertSerializedMode(mode);
32+
assert.strictEqual(
33+
createHmac('sha256', Buffer.alloc(13)).digest().byteLength, 32);
34+
} else if (mode === 'strict') {
35+
assertSerializedMode(mode);
36+
const subscriber = common.mustNotCall();
37+
diagnosticsChannel.subscribe(channelName, subscriber);
38+
assert.throws(
39+
() => createHmac('sha256', Buffer.alloc(13)).digest(),
40+
{ code: /^ERR_OSSL_/ });
41+
setImmediate(common.mustCall(() => {
42+
assert.strictEqual(
43+
diagnosticsChannel.unsubscribe(channelName, subscriber), true);
44+
}));
45+
} else if (mode === 'strict-events') {
46+
assertSerializedMode('strict');
47+
testStrictEvents().then(common.mustCall());
48+
} else {
49+
runParent();
50+
}
51+
52+
function assertSerializedMode(expected) {
53+
const { getOptionsAsFlagsFromBinding } = require('internal/options');
54+
assert.ok(getOptionsAsFlagsFromBinding().includes(`--force-fips=${expected}`));
55+
}
56+
57+
function nextIndicator() {
58+
const keepAlive = setInterval(common.mustNotCall(), 10_000);
59+
const { promise, resolve } = Promise.withResolvers();
60+
const subscriber = common.mustCall((event, name) => {
61+
assert.strictEqual(name, channelName);
62+
clearInterval(keepAlive);
63+
assert.strictEqual(
64+
diagnosticsChannel.unsubscribe(channelName, subscriber), true);
65+
resolve(event);
66+
});
67+
diagnosticsChannel.subscribe(channelName, subscriber);
68+
return promise;
69+
}
70+
71+
function runParent() {
72+
try {
73+
createHmac('sha256', Buffer.alloc(13)).digest();
74+
} catch (error) {
75+
assert.match(error.code, /^ERR_OSSL_/);
76+
common.printSkipMessage(
77+
'the FIPS provider rejects unapproved operations before signaling');
78+
return;
79+
}
80+
81+
for (const [args, childMode] of [
82+
[['--force-fips'], 'provider'],
83+
[['--force-fips=provider'], 'provider'],
84+
[['--force-fips=strict'], 'strict'],
85+
[[
86+
'--force-fips=strict',
87+
'--enable-fips-indicator-events',
88+
], 'strict-events'],
89+
]) {
90+
spawnSyncAndExitWithoutError(
91+
process.execPath, [...args, '--expose-internals', __filename], {
92+
env: { ...process.env, NODE_TEST_FIPS_FORCE_MODE: childMode },
93+
});
94+
}
95+
}
96+
97+
async function testStrictEvents() {
98+
const key = Buffer.alloc(13);
99+
100+
assert.throws(
101+
() => createHmac('sha256', key).digest(),
102+
{ code: /^ERR_OSSL_/ });
103+
104+
let eventPromise = nextIndicator();
105+
assert.throws(
106+
() => createHmac('sha256', key).digest(),
107+
{ code: /^ERR_OSSL_/ });
108+
assert.deepStrictEqual(await eventPromise, {
109+
operation: 'HMAC',
110+
reason: 'keysize',
111+
blocked: true,
112+
count: 1,
113+
dropped: 0,
114+
});
115+
116+
const hmacKey = await subtle.importKey(
117+
'raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
118+
eventPromise = nextIndicator();
119+
await assert.rejects(
120+
subtle.sign('HMAC', hmacKey, Buffer.alloc(0)),
121+
{ name: 'OperationError' });
122+
const webCryptoEvent = await eventPromise;
123+
assert.strictEqual(webCryptoEvent.operation, 'HMAC');
124+
assert.strictEqual(webCryptoEvent.reason, 'keysize');
125+
assert.strictEqual(webCryptoEvent.blocked, true);
126+
127+
eventPromise = nextIndicator();
128+
const worker = new Worker(`
129+
'use strict';
130+
const { createHmac } = require('node:crypto');
131+
const { workerData } = require('node:worker_threads');
132+
createHmac('sha256', workerData.key).digest();
133+
`, {
134+
eval: true,
135+
workerData: { key },
136+
});
137+
const errorPromise = once(worker, 'error');
138+
const exitPromise = new Promise((resolve) => worker.on('exit', resolve));
139+
const [[error], workerEvent] = await Promise.all([
140+
errorPromise,
141+
eventPromise,
142+
]);
143+
assert.match(error.code, /^ERR_OSSL_/);
144+
assert.deepStrictEqual(workerEvent, {
145+
operation: 'HMAC',
146+
reason: 'keysize',
147+
blocked: true,
148+
count: 1,
149+
dropped: 0,
150+
});
151+
const exitCode = await exitPromise;
152+
assert.strictEqual(exitCode, 1);
153+
}

0 commit comments

Comments
 (0)