Skip to content

Commit

Permalink
crypto: support --use-system-ca on Windows
Browse files Browse the repository at this point in the history
This patch adds support for --use-system-ca on Windows. Currently
this only supports Root CA certificates under the current user,
with support for other certificates (matching Chromium's policy)
as WIP.
  • Loading branch information
joyeecheung committed Jan 30, 2025
1 parent 8fdaad7 commit dc737dd
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 51 deletions.
33 changes: 32 additions & 1 deletion doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -2866,7 +2866,37 @@ The following values are valid for `mode`:
Node.js uses the trusted CA certificates present in the system store along with
the `--use-bundled-ca`, `--use-openssl-ca` options.

This option is available to macOS only.
This option is only supported on Windows and macOS, and the certificate trust policy
is planned to follow [Chromium's policy for locally trusted certificates][]:

On macOS, the following certifcates are trusted:

* Default and System Keychains
* Trust:
* Any certificate where the “When using this certificate” flag is set to “Always Trust” or
* Any certificate where the “Secure Sockets Layer (SSL)” flag is set to “Always Trust.”
* Distrust:
* Any certificate where the “When using this certificate” flag is set to “Never Trust” or
* Any certificate where the “Secure Sockets Layer (SSL)” flag is set to “Never Trust.”

On Windows, the following certificates are currently trusted (unlike
Chromium's policy, distrust is not currently supported):

* Local Machine (accessed via `certlm.msc`)
* Trust:
* Trusted Root Certification Authorities
* Trusted People
* Enterprise Trust -> Enterprise -> Trusted Root Certification Authorities
* Enterprise Trust -> Enterprise -> Trusted People
* Enterprise Trust -> Group Policy -> Trusted Root Certification Authorities
* Enterprise Trust -> Group Policy -> Trusted People
* Current User (accessed via `certmgr.msc`)
* Trust:
* Trusted Root Certification Authorities
* Enterprise Trust -> Group Policy -> Trusted Root Certification Authorities

On any supported system, Node.js would check that the certificate's key usage and extended key
usage are consistent with TLS use cases before using it for server authentication.

### `--v8-options`

Expand Down Expand Up @@ -3688,6 +3718,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
[CommonJS]: modules.md
[CommonJS module]: modules.md
[Chromium's policy for locally trusted certificates]: https://chromium.googlesource.com/chromium/src/+/main/net/data/ssl/chrome_root_store/faq.md#does-the-chrome-certificate-verifier-consider-local-trust-decisions
[DEP0025 warning]: deprecations.md#dep0025-requirenodesys
[ECMAScript module]: esm.md#modules-ecmascript-modules
[EventSource Web API]: https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
Expand Down
155 changes: 153 additions & 2 deletions src/crypto/crypto_context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
#include <Security/Security.h>
#endif

#ifdef _WIN32
#include <Windows.h>
#include <wincrypt.h>
#endif

namespace node {

using ncrypto::BignumPointer;
Expand Down Expand Up @@ -285,13 +290,15 @@ void X509VectorToPEMVector(const std::vector<X509Pointer>& src,
}
}

#ifdef __APPLE__
// This code is loosely based on
// The following code is loosely based on
// https://github.com/chromium/chromium/blob/54bd8e3/net/cert/internal/trust_store_mac.cc
// and
// https://github.com/chromium/chromium/blob/0192587/net/cert/internal/trust_store_win.cc
// Copyright 2015 The Chromium Authors
// Licensed under a BSD-style license
// See https://chromium.googlesource.com/chromium/src/+/HEAD/LICENSE for
// details.
#ifdef __APPLE__
TrustStatus IsTrustDictionaryTrustedForPolicy(CFDictionaryRef trust_dict,
bool is_self_issued) {
// Trust settings may be scoped to a single application
Expand Down Expand Up @@ -524,11 +531,155 @@ void ReadMacOSKeychainCertificates(
}
#endif // __APPLE__

#ifdef _WIN32

// Returns true if the cert can be used for server authentication, based on
// certificate properties.
//
// While there are a variety of certificate properties that can affect how
// trust is computed, the main property is CERT_ENHKEY_USAGE_PROP_ID, which
// is intersected with the certificate's EKU extension (if present).
// The intersection is documented in the Remarks section of
// CertGetEnhancedKeyUsage, and is as follows:
// - No EKU property, and no EKU extension = Trusted for all purpose
// - Either an EKU property, or EKU extension, but not both = Trusted only
// for the listed purposes
// - Both an EKU property and an EKU extension = Trusted for the set
// intersection of the listed purposes
// CertGetEnhancedKeyUsage handles this logic, and if an empty set is
// returned, the distinction between the first and third case can be
// determined by GetLastError() returning CRYPT_E_NOT_FOUND.
//
// See:
// https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certgetenhancedkeyusage
//
// If we run into any errors reading the certificate properties, we fail
// closed.
bool IsCertTrustedForServerAuth(PCCERT_CONTEXT cert) {
DWORD usage_size = 0;

if (!CertGetEnhancedKeyUsage(cert, 0, nullptr, &usage_size)) {
return false;
}

std::vector<BYTE> usage_bytes(usage_size);
CERT_ENHKEY_USAGE* usage =
reinterpret_cast<CERT_ENHKEY_USAGE*>(usage_bytes.data());
if (!CertGetEnhancedKeyUsage(cert, 0, usage, &usage_size)) {
return false;
}

if (usage->cUsageIdentifier == 0) {
// check GetLastError
HRESULT error_code = GetLastError();

switch (error_code) {
case CRYPT_E_NOT_FOUND:
return true;
case S_OK:
return false;
default:
return false;
}
}

// SAFETY: `usage->rgpszUsageIdentifier` is an array of LPSTR (pointer to null
// terminated string) of length `usage->cUsageIdentifier`.
for (DWORD i = 0; i < usage->cUsageIdentifier; ++i) {
std::string_view eku(usage->rgpszUsageIdentifier[i]);
if ((eku == szOID_PKIX_KP_SERVER_AUTH) ||
(eku == szOID_ANY_ENHANCED_KEY_USAGE)) {
return true;
}
}

return false;
}

void GatherCertsForLocation(std::vector<X509Pointer>* vector,
DWORD location,
LPCWSTR store_name) {
if (!(location == CERT_SYSTEM_STORE_LOCAL_MACHINE ||
location == CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY ||
location == CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE ||
location == CERT_SYSTEM_STORE_CURRENT_USER ||
location == CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY)) {
return;
}

DWORD flags =
location | CERT_STORE_OPEN_EXISTING_FLAG | CERT_STORE_READONLY_FLAG;

HCERTSTORE opened_store(
CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, NULL, flags, store_name));
if (!opened_store) {
return;
}

auto cleanup = OnScopeLeave(
[opened_store]() { CHECK_EQ(CertCloseStore(opened_store, 0), TRUE); });

PCCERT_CONTEXT cert_from_store = nullptr;
while ((cert_from_store = CertEnumCertificatesInStore(
opened_store, cert_from_store)) != nullptr) {
if (!IsCertTrustedForServerAuth(cert_from_store)) {
continue;
}
const unsigned char* cert_data =
reinterpret_cast<const unsigned char*>(cert_from_store->pbCertEncoded);
const size_t cert_size = cert_from_store->cbCertEncoded;

vector->emplace_back(d2i_X509(nullptr, &cert_data, cert_size));
}
}

void ReadWindowsCertificates(
std::vector<std::string>* system_root_certificates) {
std::vector<X509Pointer> system_root_certificates_X509;
// TODO(joyeecheung): match Chromium's policy, collect more certificates
// from user-added CAs and support disallowed (revoked) certificates.

// Grab the user-added roots.
GatherCertsForLocation(
&system_root_certificates_X509, CERT_SYSTEM_STORE_LOCAL_MACHINE, L"ROOT");
GatherCertsForLocation(&system_root_certificates_X509,
CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY,
L"ROOT");
GatherCertsForLocation(&system_root_certificates_X509,
CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE,
L"ROOT");
GatherCertsForLocation(
&system_root_certificates_X509, CERT_SYSTEM_STORE_CURRENT_USER, L"ROOT");
GatherCertsForLocation(&system_root_certificates_X509,
CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY,
L"ROOT");

// Grab the user-added trusted server certs. Trusted end-entity certs are
// only allowed for server auth in the "local machine" store, but not in the
// "current user" store.
GatherCertsForLocation(&system_root_certificates_X509,
CERT_SYSTEM_STORE_LOCAL_MACHINE,
L"TrustedPeople");
GatherCertsForLocation(&system_root_certificates_X509,
CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY,
L"TrustedPeople");
GatherCertsForLocation(&system_root_certificates_X509,
CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE,
L"TrustedPeople");

X509VectorToPEMVector(system_root_certificates_X509,
system_root_certificates);
}
#endif

void ReadSystemStoreCertificates(
std::vector<std::string>* system_root_certificates) {
#ifdef __APPLE__
ReadMacOSKeychainCertificates(system_root_certificates);
#endif
#ifdef _WIN32
ReadWindowsCertificates(system_root_certificates);
#endif
}

std::vector<std::string> getCombinedRootCertificates() {
Expand Down
Binary file added test/fixtures/keys/fake-startcom-root-cert.cer
Binary file not shown.
2 changes: 1 addition & 1 deletion test/parallel/parallel.status
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ test-fs-read-stream-concurrent-reads: PASS, FLAKY
test-snapshot-incompatible: SKIP

# Requires manual setup for certificates to be trusted by the system
test-native-certs-macos: SKIP
test-native-certs: SKIP

[$system==win32]
# https://github.com/nodejs/node/issues/54808
Expand Down
47 changes: 0 additions & 47 deletions test/parallel/test-native-certs-macos.mjs

This file was deleted.

68 changes: 68 additions & 0 deletions test/parallel/test-native-certs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Flags: --use-system-ca

import * as common from '../common/index.mjs';
import assert from 'node:assert/strict';
import https from 'node:https';
import fixtures from '../common/fixtures.js';
import { it, beforeEach, afterEach, describe } from 'node:test';
import { once } from 'events';

if (!common.isMacOS && !common.isWindows) {
common.skip('--use-system-ca is only supported on macOS and Windows');
}

if (!common.hasCrypto) {
common.skip('requires crypto');
}

// To run this test, the system needs to be configured to trust
// the CA certificate first (which needs an interactive GUI approval, e.g. TouchID):
// On macOS:
// 1. To add the certificate:
// $ security add-trusted-cert \
// -k /Users/$USER/Library/Keychains/login.keychain-db \
// test/fixtures/keys/fake-startcom-root-cert.pem
// 2. To remove the certificate:
// $ security delete-certificate -c 'StartCom Certification Authority' \
// -t /Users/$USER/Library/Keychains/login.keychain-db
//
// On Windows:
// 1. To add the certificate in PowerShell (remember the thumbprint printed):
// $ Import-Certificate -FilePath .\test\fixtures\keys\fake-startcom-root-cert.cer \
// -CertStoreLocation Cert:\CurrentUser\Root
// 2. To remove the certificate by the thumbprint:
// $ $thumbprint = (Get-ChildItem -Path Cert:\CurrentUser\Root | \
// Where-Object { $_.Subject -match "StartCom Certification Authority" }).Thumbprint
// $ Remove-Item -Path "Cert:\CurrentUser\Root\$thumbprint"
const handleRequest = (req, res) => {
const path = req.url;
switch (path) {
case '/hello-world':
res.writeHead(200);
res.end('hello world\n');
break;
default:
assert(false, `Unexpected path: ${path}`);
}
};

describe('use-system-ca', function() {
let server;

beforeEach(async function() {
server = https.createServer({
key: fixtures.readKey('agent8-key.pem'),
cert: fixtures.readKey('agent8-cert.pem'),
}, handleRequest);
server.listen(0);
await once(server, 'listening');
});

it('can connect successfully with a trusted certificate', async function() {
await fetch(`https://localhost:${server.address().port}/hello-world`);
});

afterEach(async function() {
server?.close();
});
});

0 comments on commit dc737dd

Please sign in to comment.