Skip to content

Commit

Permalink
implement detached signature verification of policy manifests on Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
rdw-msft committed Feb 16, 2024
1 parent 8a41d9b commit 49495e2
Show file tree
Hide file tree
Showing 13 changed files with 372 additions and 8 deletions.
9 changes: 9 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a id="ERR_MANIFEST_SYSTEM_CI_VIOLATION"></a>

### `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.

<a id="ERR_MANIFEST_TDZ"></a>

### `ERR_MANIFEST_TDZ`
Expand Down
10 changes: 9 additions & 1 deletion doc/api/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,22 @@ 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.

```bash
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
Expand Down
11 changes: 11 additions & 0 deletions lib/codeintegrity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';

const {
isCodeIntegrityForcedByOS,
isFileTrustedBySystemCodeIntegrityPolicy,
} = internalBinding('code_integrity');

module.exports = {
isCodeIntegrityForcedByOS,
isFileTrustedBySystemCodeIntegrityPolicy,
};
3 changes: 3 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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".',
Expand Down
50 changes: 43 additions & 7 deletions lib/internal/process/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -681,6 +716,7 @@ function readPolicyFromDisk() {
}
}


function initializeCJSLoader() {
const { initializeCJS } = require('internal/modules/cjs/loader');
initializeCJS();
Expand Down
2 changes: 2 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/node_binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
V(buffer) \
V(builtins) \
V(cares_wrap) \
V(code_integrity) \
V(config) \
V(constants) \
V(contextify) \
Expand Down
206 changes: 206 additions & 0 deletions src/node_code_integrity.cc
Original file line number Diff line number Diff line change
@@ -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<Value>& 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<bool>(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<Value>& 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<Object> target,
Local<Value> unused,
Local<Context> 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)
Loading

0 comments on commit 49495e2

Please sign in to comment.