diff --git a/doc/api/errors.md b/doc/api/errors.md
index e91d4a8304ba15..62c48551569950 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -2214,6 +2214,15 @@ documentation for [policy][] manifests for more information.
An attempt was made to load a policy manifest, but the manifest was unable to
be parsed. See the documentation for [policy][] manifests for more information.
+
+
+### `ERR_MANIFEST_SYSTEM_CI_VIOLATION`
+
+The manifest does not match the signature provided or the signature
+does not conform to system code integrity policy.
+
+See the documentation for [policy][] manifests for more information.
+
### `ERR_MANIFEST_TDZ`
diff --git a/doc/api/permissions.md b/doc/api/permissions.md
index 9c2c713ab10faf..84b740744f94d9 100644
--- a/doc/api/permissions.md
+++ b/doc/api/permissions.md
@@ -70,7 +70,11 @@ The policy manifest will be used to enforce constraints on code loaded by
Node.js.
To mitigate tampering with policy files on disk, an integrity for
-the policy file itself may be provided via `--policy-integrity`.
+the policy file itself may be provided in two ways.
+One, via `--policy-integrity` using a subresource integrity string.
+Or, via `--policy-signature` using a PKCS7 detached signature created with
+a signing key trusted by system code integrity policy.
+
This allows running `node` and asserting the policy file contents
even if the file is changed on disk.
@@ -78,6 +82,10 @@ even if the file is changed on disk.
node --experimental-policy=policy.json --policy-integrity="sha384-SggXRQHwCG8g+DktYYzxkXRIkTiEYWBHqev0xnpCxYlqMBufKZHAHQM3/boDaI/0" app.js
```
+```bash
+node --experimental-policy=policy.json --policy-signature=policy.json.p7s app.js
+```
+
#### Features
##### Error behavior
diff --git a/lib/codeintegrity.js b/lib/codeintegrity.js
new file mode 100644
index 00000000000000..a5cc7123daf32e
--- /dev/null
+++ b/lib/codeintegrity.js
@@ -0,0 +1,11 @@
+'use strict';
+
+const {
+ isCodeIntegrityForcedByOS,
+ isFileTrustedBySystemCodeIntegrityPolicy,
+} = internalBinding('code_integrity');
+
+module.exports = {
+ isCodeIntegrityForcedByOS,
+ isFileTrustedBySystemCodeIntegrityPolicy,
+};
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index ef4295f9eaaaef..1736337880e391 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -1588,6 +1588,9 @@ E('ERR_MANIFEST_INVALID_RESOURCE_FIELD',
E('ERR_MANIFEST_INVALID_SPECIFIER',
'Manifest resource %s has invalid dependency mapping %s',
TypeError);
+E('ERR_MANIFEST_SYSTEM_CI_VIOLATION',
+ 'The provided manifest does not pass system code integrity validation. "%s"',
+ Error);
E('ERR_MANIFEST_TDZ', 'Manifest initialization has not yet run', Error);
E('ERR_MANIFEST_UNKNOWN_ONERROR',
'Manifest specified unknown error behavior "%s".',
diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js
index b7cf9390c9845c..230c75f8682ee5 100644
--- a/lib/internal/process/pre_execution.js
+++ b/lib/internal/process/pre_execution.js
@@ -37,6 +37,7 @@ const {
const {
ERR_INVALID_THIS,
ERR_MANIFEST_ASSERT_INTEGRITY,
+ ERR_MANIFEST_SYSTEM_CI_VIOLATION,
ERR_NO_CRYPTO,
ERR_MISSING_OPTION,
ERR_ACCESS_DENIED,
@@ -632,21 +633,55 @@ function initializePermission() {
}
function readPolicyFromDisk() {
+ const ci = require('codeintegrity');
const experimentalPolicy = getOptionValue('--experimental-policy');
- if (experimentalPolicy) {
+
+ const forceCI = ci.isCodeIntegrityForcedByOS();
+ if (forceCI && !experimentalPolicy) {
+ throw new ERR_MANIFEST_SYSTEM_CI_VIOLATION(
+ 'Code integrity is forced by system policy. ' +
+ 'A Policy manifest (--experimental-policy) and signature (--policy-signature) are required');
+ }
+
+ if (experimentalPolicy || forceCI) {
process.emitWarning('Policies are experimental.',
'ExperimentalWarning');
- const { pathToFileURL, URL } = require('internal/url');
+ const { pathToFileURL, URL, fileURLToPath } = require('internal/url');
// URL here as it is slightly different parsing
// no bare specifiers for now
- let manifestURL;
- if (require('path').isAbsolute(experimentalPolicy)) {
- manifestURL = pathToFileURL(experimentalPolicy);
- } else {
+
+ function makeAbsoluteUrl(f) {
+ if (require('path').isAbsolute(f)) {
+ return new URL(`file://${f}`);
+ }
+
const cwdURL = pathToFileURL(process.cwd());
cwdURL.pathname += '/';
- manifestURL = new URL(experimentalPolicy, cwdURL);
+ return new URL(f, cwdURL);
+ }
+
+ const manifestURL = makeAbsoluteUrl(experimentalPolicy);
+ const policySignature = getOptionValue('--policy-signature');
+ if (forceCI && !policySignature) {
+ throw new ERR_MANIFEST_SYSTEM_CI_VIOLATION(
+ 'Code integrity is forced by system policy. ' +
+ 'A Policy manifest (--experimental-policy) and signature (--policy-signature) are required');
}
+
+ if (policySignature) {
+ const policySignatureUrl = makeAbsoluteUrl(policySignature);
+
+ const isPolicyTrusted = ci.isFileTrustedBySystemCodeIntegrityPolicy(
+ fileURLToPath(manifestURL),
+ fileURLToPath(policySignatureUrl),
+ );
+
+ if (!isPolicyTrusted) {
+ throw new ERR_MANIFEST_SYSTEM_CI_VIOLATION(
+ 'The --policy-signature provided is not valid or does not meet system code integrity requirements');
+ }
+ }
+
const fs = require('fs');
const src = fs.readFileSync(manifestURL, 'utf8');
const experimentalPolicyIntegrity = getOptionValue('--policy-integrity');
@@ -681,6 +716,7 @@ function readPolicyFromDisk() {
}
}
+
function initializeCJSLoader() {
const { initializeCJS } = require('internal/modules/cjs/loader');
initializeCJS();
diff --git a/node.gyp b/node.gyp
index a25253e38870dd..e3457ae6c55b9d 100644
--- a/node.gyp
+++ b/node.gyp
@@ -96,6 +96,7 @@
'src/node_blob.cc',
'src/node_buffer.cc',
'src/node_builtins.cc',
+ 'src/node_code_integrity.cc',
'src/node_config.cc',
'src/node_constants.cc',
'src/node_contextify.cc',
@@ -214,6 +215,7 @@
'src/node_blob.h',
'src/node_buffer.h',
'src/node_builtins.h',
+ 'src/node_code_integrity.h',
'src/node_constants.h',
'src/node_context_data.h',
'src/node_contextify.h',
diff --git a/src/node_binding.cc b/src/node_binding.cc
index 8013d9a0bbf48b..01d20714f9bcc4 100644
--- a/src/node_binding.cc
+++ b/src/node_binding.cc
@@ -33,6 +33,7 @@
V(buffer) \
V(builtins) \
V(cares_wrap) \
+ V(code_integrity) \
V(config) \
V(constants) \
V(contextify) \
diff --git a/src/node_code_integrity.cc b/src/node_code_integrity.cc
new file mode 100644
index 00000000000000..11af52c031f07a
--- /dev/null
+++ b/src/node_code_integrity.cc
@@ -0,0 +1,206 @@
+#include "node_code_integrity.h"
+#include "v8.h"
+#include "node.h"
+#include "env-inl.h"
+#include "node_external_reference.h"
+
+namespace node {
+
+using v8::Boolean;
+using v8::Context;
+using v8::FunctionCallbackInfo;
+using v8::Local;
+using v8::Object;
+using v8::Value;
+
+
+namespace codeintegrity {
+
+static void IsCodeIntegrityForcedByOS(const FunctionCallbackInfo& args) {
+ Environment* env = Environment::GetCurrent(args);
+ int ret = 0;
+
+#ifdef _WIN32
+ HRESULT hr = E_FAIL;
+
+ HMODULE wldp_module = LoadLibraryExA(
+ "wldp.dll",
+ nullptr,
+ LOAD_LIBRARY_SEARCH_SYSTEM32);
+
+ if (wldp_module == nullptr) {
+ // this case only happens on Windows versions that don't support
+ // code integrity policies.
+ args.GetReturnValue().Set(Boolean::New(env->isolate(), false));
+ return;
+ }
+
+ pfnWldpGetApplicationSettingBoolean WldpGetApplicationSettingBoolean =
+ (pfnWldpGetApplicationSettingBoolean)GetProcAddress(
+ wldp_module,
+ "WldpGetApplicationSettingBoolean");
+
+ if (WldpGetApplicationSettingBoolean != nullptr) {
+ HRESULT hr = WldpGetApplicationSettingBoolean(
+ L"nodejs",
+ L"EnforceCodeIntegrity",
+ &ret);
+
+ if (SUCCEEDED(hr)) {
+ args.GetReturnValue().Set(
+ Boolean::New(env->isolate(), static_cast(ret)));
+ return;
+ } else if (hr != 0x80070490) { // E_NOTFOUND
+ args.GetReturnValue().Set(Boolean::New(env->isolate(), false));
+ return;
+ }
+ }
+
+ // WldpGetApplicationSettingBoolean is the preferred way for applications to
+ // query security policy values. However, this method only exists on Windows
+ // versions going back to circa Win10 2023H2. In order to support systems
+ // older than that (down to Win10RS2), we can use the deprecated
+ // WldpQuerySecurityPolicy
+ pfnWldpQuerySecurityPolicy WldpQuerySecurityPolicy =
+ (pfnWldpQuerySecurityPolicy)GetProcAddress(
+ wldp_module,
+ "WldpQuerySecurityPolicy");
+
+ if (WldpQuerySecurityPolicy != nullptr) {
+ DECLARE_CONST_UNICODE_STRING(providerName, L"nodejs");
+ DECLARE_CONST_UNICODE_STRING(keyName, L"Settings");
+ DECLARE_CONST_UNICODE_STRING(valueName, L"EnforceCodeIntegrity");
+ WLDP_SECURE_SETTING_VALUE_TYPE valueType =
+ WLDP_SECURE_SETTING_VALUE_TYPE_BOOLEAN;
+ ULONG valueSize = sizeof(int);
+ hr = WldpQuerySecurityPolicy(
+ &providerName,
+ &keyName,
+ &valueName,
+ &valueType,
+ &ret,
+ &valueSize);
+ if (FAILED(hr)) {
+ args.GetReturnValue().Set(Boolean::New(env->isolate(), false));
+ return;
+ }
+ }
+#endif // _WIN32
+ args.GetReturnValue().Set(Boolean::New(env->isolate(), ret));
+}
+
+static void IsFileTrustedBySystemCodeIntegrityPolicy(
+ const FunctionCallbackInfo& args) {
+#ifdef _WIN32
+ Environment* env = Environment::GetCurrent(args);
+
+ CHECK_EQ(args.Length(), 2);
+ CHECK(args[0]->IsString());
+ CHECK(args[1]->IsString());
+
+ BufferValue manifestPath(env->isolate(), args[0]);
+ if (*manifestPath == nullptr) {
+ return env->ThrowError("Manifest path cannot be empty");
+ }
+
+ BufferValue signaturePath(env->isolate(), args[1]);
+ if (*signaturePath == nullptr) {
+ return env->ThrowError("Signature path cannot be empty");
+ }
+
+ HANDLE hNodePolicyFile = CreateFileA(
+ *manifestPath,
+ GENERIC_READ,
+ FILE_SHARE_READ,
+ nullptr,
+ OPEN_EXISTING,
+ FILE_ATTRIBUTE_NORMAL,
+ nullptr);
+
+ if (hNodePolicyFile == INVALID_HANDLE_VALUE || hNodePolicyFile == nullptr) {
+ return env->ThrowError("invalid manifest path");
+ }
+
+ HANDLE hSignatureFile = CreateFileA(
+ *signaturePath,
+ GENERIC_READ,
+ FILE_SHARE_READ,
+ nullptr,
+ OPEN_EXISTING,
+ FILE_ATTRIBUTE_NORMAL,
+ nullptr);
+
+ if (hSignatureFile == INVALID_HANDLE_VALUE || hSignatureFile == nullptr) {
+ return env->ThrowError("invalid signature path");
+ }
+
+ HMODULE wldp_module = LoadLibraryExA(
+ "wldp.dll",
+ nullptr,
+ LOAD_LIBRARY_SEARCH_SYSTEM32);
+
+ if (wldp_module == nullptr) {
+ return env->ThrowError("Unable to load wldp.dll");
+ }
+
+ pfnWldpCanExecuteFileFromDetachedSignature
+ WldpCanExecuteFileFromDetachedSignature =
+ (pfnWldpCanExecuteFileFromDetachedSignature)GetProcAddress(
+ wldp_module,
+ "WldpCanExecuteFileFromDetachedSignature");
+
+ if (WldpCanExecuteFileFromDetachedSignature == nullptr) {
+ return env->ThrowError(
+ "Cannot find proc WldpCanExecuteFileFromDetachedSignature");
+ }
+
+ const GUID wldp_host_other = WLDP_HOST_OTHER;
+ WLDP_EXECUTION_POLICY result;
+ HRESULT hr = WldpCanExecuteFileFromDetachedSignature(
+ wldp_host_other,
+ WLDP_EXECUTION_EVALUATION_OPTION_NONE,
+ hNodePolicyFile,
+ hSignatureFile,
+ nullptr,
+ &result);
+
+ if (FAILED(hr)) {
+ return env->ThrowError("WldpCanExecuteFileFromDetachedSignature failed");
+ }
+
+ bool isPolicyTrusted = (result == WLDP_EXECUTION_POLICY_ALLOWED);
+ args.GetReturnValue().Set(isPolicyTrusted);
+#endif // _WIN32
+}
+
+void Initialize(Local