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 target, + Local unused, + Local context, + void* priv) { + Environment* env = Environment::GetCurrent(context); + SetMethod( + context, + target, + "isCodeIntegrityForcedByOS", + IsCodeIntegrityForcedByOS); + + SetMethod( + context, + target, + "isFileTrustedBySystemCodeIntegrityPolicy", + IsFileTrustedBySystemCodeIntegrityPolicy); +} + +void RegisterExternalReferences(ExternalReferenceRegistry* registry) { +// BindingData::RegisterExternalReferences(registry); + + registry->Register(IsCodeIntegrityForcedByOS); + registry->Register(IsFileTrustedBySystemCodeIntegrityPolicy); +} + +} // namespace codeintegrity +} // namespace node +NODE_BINDING_CONTEXT_AWARE_INTERNAL(code_integrity, + node::codeintegrity::Initialize) +NODE_BINDING_EXTERNAL_REFERENCE(code_integrity, + node::codeintegrity::RegisterExternalReferences) diff --git a/src/node_code_integrity.h b/src/node_code_integrity.h new file mode 100644 index 00000000000000..8cb196854b7dcd --- /dev/null +++ b/src/node_code_integrity.h @@ -0,0 +1,75 @@ +#ifndef SRC_NODE_CODE_INTEGRITY_H_ +#define SRC_NODE_CODE_INTEGRITY_H_ + +#ifdef _WIN32 + +#include + +/* from winternl.h */ +#if !defined(__UNICODE_STRING_DEFINED) && defined(__MINGW32__) +#define __UNICODE_STRING_DEFINED +#endif +typedef struct _UNICODE_STRING { + USHORT Length; + USHORT MaximumLength; + PWSTR Buffer; +} UNICODE_STRING, *PUNICODE_STRING; + +typedef const UNICODE_STRING* PCUNICODE_STRING; + +// {0xb5367df1,0xcbac,0x11cf,{0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92}} +#define WLDP_HOST_OTHER \ + {0x626cbec3, 0xe1fa, 0x4227, \ + {0x98, 0x0, 0xed, 0x21, 0x2, 0x74, 0xcf, 0x7c}}; + +// +// Enumeration types for WldpCanExecuteFile +// +typedef enum WLDP_EXECUTION_POLICY { + WLDP_EXECUTION_POLICY_BLOCKED, + WLDP_EXECUTION_POLICY_ALLOWED, + WLDP_EXECUTION_POLICY_REQUIRE_SANDBOX, +} WLDP_EXECUTION_POLICY; + +typedef enum WLDP_EXECUTION_EVALUATION_OPTIONS { + WLDP_EXECUTION_EVALUATION_OPTION_NONE = 0x0, + WLDP_EXECUTION_EVALUATION_OPTION_EXECUTE_IN_INTERACTIVE_SESSION = 0x1, +} WLDP_EXECUTION_EVALUATION_OPTIONS; + +typedef HRESULT(WINAPI* pfnWldpCanExecuteFileFromDetachedSignature)( + _In_ REFGUID host, + _In_ WLDP_EXECUTION_EVALUATION_OPTIONS options, + _In_ HANDLE contentFileHandle, + _In_ HANDLE signatureFileHandle, + _In_opt_ PCWSTR auditInfo, + _Out_ WLDP_EXECUTION_POLICY* result); + +typedef HRESULT(WINAPI* pfnWldpGetApplicationSettingBoolean)( + _In_ PCWSTR id, + _In_ PCWSTR setting, + _Out_ BOOL* result); + +typedef enum WLDP_SECURE_SETTING_VALUE_TYPE { + WLDP_SECURE_SETTING_VALUE_TYPE_BOOLEAN = 0, + WLDP_SECURE_SETTING_VALUE_TYPE_ULONG, + WLDP_SECURE_SETTING_VALUE_TYPE_BINARY, + WLDP_SECURE_SETTING_VALUE_TYPE_STRING +} WLDP_SECURE_SETTING_VALUE_TYPE, *PWLDP_SECURE_SETTING_VALUE_TYPE; + +typedef HRESULT(WINAPI* pfnWldpQuerySecurityPolicy)( + _In_ const UNICODE_STRING * providerName, + _In_ const UNICODE_STRING * keyName, + _In_ const UNICODE_STRING * valueName, + _Out_ PWLDP_SECURE_SETTING_VALUE_TYPE valueType, + _Out_writes_bytes_opt_(*valueSize) PVOID valueAddress, + _Inout_ PULONG valueSize); + +#ifndef DECLARE_CONST_UNICODE_STRING +#define DECLARE_CONST_UNICODE_STRING(_var, _string) \ +const WCHAR _var ## _buffer[] = _string; \ +const UNICODE_STRING _var = \ +{ sizeof(_string) - sizeof(WCHAR), sizeof(_string), (PWCH) _var ## _buffer } +#endif + +#endif // _WIN32 +#endif // SRC_NODE_CODE_INTEGRITY_H_ diff --git a/src/node_external_reference.h b/src/node_external_reference.h index b952bb7fc5adf8..f9b75d07fa2422 100644 --- a/src/node_external_reference.h +++ b/src/node_external_reference.h @@ -99,6 +99,7 @@ class ExternalReferenceRegistry { V(buffer) \ V(builtins) \ V(cares_wrap) \ + V(code_integrity) \ V(contextify) \ V(credentials) \ V(encoding_binding) \ diff --git a/src/node_options.cc b/src/node_options.cc index 7b5152172c5ce7..ff9fd0cf0d2113 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -439,6 +439,15 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { &EnvironmentOptions::experimental_policy_integrity, kAllowedInEnvvar); Implies("--policy-integrity", "[has_policy_integrity_string]"); + AddOption("[has_policy_signature]", + "", + &EnvironmentOptions::has_policy_signature); + AddOption("--policy-signature", + "ensure the security policy is signed " + "by a trusted signer", + &EnvironmentOptions::experimental_policy_signature, + kAllowedInEnvvar); + Implies("--policy-signature", "[has_policy_signature]"); AddOption("--allow-fs-read", "allow permissions to read the filesystem", &EnvironmentOptions::allow_fs_read, diff --git a/src/node_options.h b/src/node_options.h index a0b56ebacb436c..700ce9ae7a2079 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -119,7 +119,9 @@ class EnvironmentOptions : public Options { std::string type; // Value of --experimental-default-type std::string experimental_policy; std::string experimental_policy_integrity; + std::string experimental_policy_signature; bool has_policy_integrity_string = false; + bool has_policy_signature = false; bool experimental_permission = false; std::vector allow_fs_read; std::vector allow_fs_write; diff --git a/tsconfig.json b/tsconfig.json index 3f5a3067ced063..9d280df245cf60 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,6 +34,7 @@ "buffer": ["./lib/buffer.js"], "child_process": ["./lib/child_process.js"], "cluster": ["./lib/cluster.js"], + "codeintegrity": ["./lib/codeintegrity.js"], "console": ["./lib/console.js"], "constants": ["./lib/constants.js"], "crypto": ["./lib/crypto.js"],