From 82cd4a6801cd72a721af45c4a2c6642eb9e22838 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 4 Sep 2026 09:22:14 +0200 Subject: [PATCH 1/3] crypto: avoid network access loading system CAs Use the purpose-neutral Basic X.509 policy when enumerating macOS system certificates. Verify that enumeration does not fetch AIA or OCSP resources. Assisted-by: AI coding assistant Signed-off-by: Christof Marti --- src/crypto/crypto_context.cc | 18 +- .../test-macos-cert-network-fetch.mjs | 238 ++++++++++++++++++ 2 files changed, 244 insertions(+), 12 deletions(-) create mode 100644 test/system-ca/test-macos-cert-network-fetch.mjs diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index 9dc68f4d9d2d..82297629912c 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -450,18 +450,12 @@ bool IsCertificateTrustValid(SecCertificateRef ref) { CFArrayCreateMutable(nullptr, 1, &kCFTypeArrayCallBacks); CFArraySetValueAtIndex(subj_certs, 0, ref); - // SecTrustEvaluateWithError is used to check whether an individual - // certificate is trusted by the system — not to validate it for a - // specific role (server, intermediate, etc.). We just need a minimal - // policy that guarantees the certificate can be chained to a known - // trust anchor while filtering out irrelevant certificates. - // - // Refs - // https://github.com/apple-oss-distributions/Security/blob/db15acbe6a7f257a859ad9a3bb86097bfe0679d9/OSX/sec/Security/SecPolicy.c#L1855-L1890 - // SecPolicyCreateSSL (both mark EKU optional): - // server=true -> BasicX509 + serverAuth + anyExtendedKeyUsage + SGC - // server=false -> BasicX509 + clientAuth + anyExtendedKeyUsage - SecPolicyRef policy = SecPolicyCreateSSL(false, nullptr); + // Enumerating trust anchors must not constrain them to a particular + // application policy. The actual TLS handshake applies the appropriate EKU + // checks when validating the peer certificate. Basic X.509 policy also + // disables network access, avoiding AIA and revocation requests during + // certificate enumeration. + SecPolicyRef policy = SecPolicyCreateBasicX509(); OSStatus ortn = SecTrustCreateWithCertificates(subj_certs, policy, &sec_trust); bool result = false; diff --git a/test/system-ca/test-macos-cert-network-fetch.mjs b/test/system-ca/test-macos-cert-network-fetch.mjs new file mode 100644 index 000000000000..111bc7f4e97d --- /dev/null +++ b/test/system-ca/test-macos-cert-network-fetch.mjs @@ -0,0 +1,238 @@ +import * as common from '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import assert from 'node:assert/strict'; +import { execFile, execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import http from 'node:http'; +import { once } from 'node:events'; +import { promisify } from 'node:util'; +import { test } from 'node:test'; +import tmpdir from '../common/tmpdir.js'; +import cryptoFixtures from '../common/crypto.js'; + +const { opensslCli } = cryptoFixtures; + +if (!common.hasCrypto) { + common.skip('requires crypto'); +} + +if (process.platform !== 'darwin') { + common.skip('macOS-specific test'); +} + +if (!opensslCli) { + common.skip('missing openssl-cli'); +} + +function isCertInKeychain(cn) { + try { + execFileSync('security', ['find-certificate', '-c', cn], { stdio: 'pipe' }); + return true; + } catch { + return false; + } +} + +if (!isCertInKeychain('StartCom Certification Authority')) { + common.skip( + 'fake-startcom-root-cert.pem not found in system CA store. ' + + 'Please follow setup instructions in test/system-ca/README.md', + ); +} + +const execFileAsync = promisify(execFile); + +async function run(file, args) { + return execFileAsync(file, args, { encoding: 'utf8' }); +} + +async function runToCompletion(file, args) { + await new Promise((resolve) => execFile(file, args, resolve)); +} + +async function generateCertificates(port) { + const intermediateKey = tmpdir.resolve('intermediate-key.pem'); + const intermediateCsr = tmpdir.resolve('intermediate.csr'); + const intermediateCert = tmpdir.resolve('intermediate-cert.pem'); + const intermediateDer = tmpdir.resolve('intermediate-cert.der'); + const intermediateConfig = tmpdir.resolve('intermediate.cnf'); + const leafKey = tmpdir.resolve('leaf-key.pem'); + const leafCsr = tmpdir.resolve('leaf.csr'); + const leafCert = tmpdir.resolve('leaf-cert.pem'); + const leafConfig = tmpdir.resolve('leaf.cnf'); + const rootCert = fixtures.path('keys', 'fake-startcom-root-cert.pem'); + const rootKey = fixtures.path('keys', 'fake-startcom-root-key.pem'); + + fs.writeFileSync(intermediateConfig, ` +[v3_ca] +basicConstraints = critical,CA:TRUE,pathlen:0 +keyUsage = critical,keyCertSign,cRLSign +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer +`); + fs.writeFileSync(leafConfig, ` +[v3_leaf] +basicConstraints = critical,CA:FALSE +keyUsage = critical,digitalSignature,keyEncipherment +extendedKeyUsage = serverAuth,clientAuth +subjectAltName = DNS:localhost,IP:127.0.0.1 +authorityInfoAccess = caIssuers;URI:http://127.0.0.1:${port}/intermediate.der,\\ + OCSP;URI:http://127.0.0.1:${port}/ocsp +`); + + await run(opensslCli, [ + 'req', '-new', '-newkey', 'rsa:2048', '-noenc', + '-keyout', intermediateKey, + '-out', intermediateCsr, + '-subj', '/CN=NodeJS Test AIA Intermediate', + ]); + await run(opensslCli, [ + 'x509', '-req', + '-in', intermediateCsr, + '-CA', rootCert, + '-CAkey', rootKey, + '-set_serial', `0x${Date.now().toString(16)}01`, + '-out', intermediateCert, + '-days', '1', + '-extfile', intermediateConfig, + '-extensions', 'v3_ca', + ]); + await run(opensslCli, [ + 'x509', '-in', intermediateCert, '-outform', 'DER', '-out', intermediateDer, + ]); + await run(opensslCli, [ + 'req', '-new', '-newkey', 'rsa:2048', '-noenc', + '-keyout', leafKey, + '-out', leafCsr, + '-subj', '/CN=NodeJS Test AIA Leaf', + ]); + await run(opensslCli, [ + 'x509', '-req', + '-in', leafCsr, + '-CA', intermediateCert, + '-CAkey', intermediateKey, + '-set_serial', `0x${Date.now().toString(16)}02`, + '-out', leafCert, + '-days', '1', + '-extfile', leafConfig, + '-extensions', 'v3_leaf', + ]); + + return { + intermediateCert, + intermediateDer, + leafCert, + rootCert, + }; +} + +function parseKeychainSearchList(stdout) { + return stdout.trim().split(/\r?\n/) + .map((line) => line.trim().replace(/^"|"$/g, '')) + .filter(Boolean); +} + +test('system CA enumeration does not fetch AIA or OCSP', { + timeout: 30_000, +}, async (t) => { + tmpdir.refresh(); + + const requests = []; + let intermediate; + const server = http.createServer((req, res) => { + requests.push({ method: req.method, url: req.url }); + if (req.url === '/intermediate.der') { + res.writeHead(200, { 'Content-Type': 'application/pkix-cert' }); + res.end(intermediate); + } else if (req.url?.startsWith('/ocsp')) { + res.writeHead(500); + res.end(); + } else { + res.writeHead(404); + res.end(); + } + }); + server.listen(0, '127.0.0.1'); + await once(server, 'listening'); + t.after(() => new Promise((resolve) => server.close(resolve))); + + const address = server.address(); + assert.notStrictEqual(address, null); + assert.notStrictEqual(typeof address, 'string'); + const certificates = await generateCertificates(address.port); + intermediate = fs.readFileSync(certificates.intermediateDer); + + const keychain = tmpdir.resolve('node-system-ca-test.keychain-db'); + const password = 'node-test'; + const { stdout } = await run('/usr/bin/security', [ + 'list-keychains', '-d', 'user', + ]); + const originalKeychains = parseKeychainSearchList(stdout); + + await run('/usr/bin/security', [ + 'create-keychain', '-p', password, keychain, + ]); + t.after(async () => { + await run('/usr/bin/security', [ + 'list-keychains', '-d', 'user', '-s', ...originalKeychains, + ]); + await run('/usr/bin/security', ['delete-keychain', keychain]); + }); + await run('/usr/bin/security', [ + 'unlock-keychain', '-p', password, keychain, + ]); + await run('/usr/bin/security', [ + 'set-keychain-settings', '-lut', '3600', keychain, + ]); + await run('/usr/bin/security', [ + 'add-certificates', '-k', keychain, certificates.leafCert, + ]); + await run('/usr/bin/security', [ + 'list-keychains', '-d', 'user', '-s', ...originalKeychains, keychain, + ]); + + await run(process.execPath, [ + '-e', 'require("node:tls").getCACertificates("system")', + ]); + const enumerationRequests = [...requests]; + requests.length = 0; + + // Node's TLS validation uses OpenSSL and does not fetch AIA or OCSP itself. + // Use macOS trust evaluation as a control to show that the generated + // certificate can trigger both types of network request. + await runToCompletion('/usr/bin/security', [ + 'verify-cert', + '-c', certificates.leafCert, + '-r', certificates.rootCert, + '-p', 'ssl', + '-n', 'localhost', + ]); + const validationFetchedAia = requests.some( + ({ url }) => url === '/intermediate.der', + ); + + requests.length = 0; + await runToCompletion('/usr/bin/security', [ + 'verify-cert', + '-c', certificates.leafCert, + '-c', certificates.intermediateCert, + '-r', certificates.rootCert, + '-p', 'ssl', + '-n', 'localhost', + '-R', 'ocsp', + '-R', 'require', + ]); + const validationRequestedOcsp = requests.some( + ({ url }) => url?.startsWith('/ocsp'), + ); + + assert.deepStrictEqual({ + enumerationRequests, + validationFetchedAia, + validationRequestedOcsp, + }, { + enumerationRequests: [], + validationFetchedAia: true, + validationRequestedOcsp: true, + }); +}); From a5c25cf19e17ae8b31556b771dd7d9cf6e69a0cf Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 4 Sep 2026 09:22:15 +0200 Subject: [PATCH 2/3] test: pin system CA network certificates Replace runtime certificate generation with checked-in fixtures and Makefile targets for the macOS system CA network test. Assisted-by: AI coding assistant Signed-off-by: Christof Marti --- test/fixtures/keys/Makefile | 54 +++++++ .../system-ca-network-intermediate-cert.pem | 24 +++ .../system-ca-network-intermediate-key.pem | 28 ++++ .../keys/system-ca-network-leaf-cert.pem | 23 +++ .../keys/system-ca-network-leaf-key.pem | 28 ++++ test/fixtures/keys/system-ca-network.cnf | 13 ++ .../test-macos-cert-network-fetch.mjs | 142 ++++-------------- 7 files changed, 203 insertions(+), 109 deletions(-) create mode 100644 test/fixtures/keys/system-ca-network-intermediate-cert.pem create mode 100644 test/fixtures/keys/system-ca-network-intermediate-key.pem create mode 100644 test/fixtures/keys/system-ca-network-leaf-cert.pem create mode 100644 test/fixtures/keys/system-ca-network-leaf-key.pem create mode 100644 test/fixtures/keys/system-ca-network.cnf diff --git a/test/fixtures/keys/Makefile b/test/fixtures/keys/Makefile index 3ef61d00afff..17f509a2b1e4 100644 --- a/test/fixtures/keys/Makefile +++ b/test/fixtures/keys/Makefile @@ -51,6 +51,10 @@ all: \ non-trusted-intermediate-ca-key.pem \ non-trusted-leaf-from-intermediate-cert.pem \ non-trusted-leaf-from-intermediate-key.pem \ + system-ca-network-intermediate-cert.pem \ + system-ca-network-intermediate-key.pem \ + system-ca-network-leaf-cert.pem \ + system-ca-network-leaf-key.pem \ rsa_private.pem \ rsa_private_encrypted.pem \ rsa_private_pkcs8.pem \ @@ -415,6 +419,56 @@ non-trusted-leaf-from-intermediate-cert.pem: non-trusted-leaf-from-intermediate- rm -f non-trusted-leaf-from-intermediate-cert.csr +system-ca-network-intermediate-key.pem: + openssl genrsa -out system-ca-network-intermediate-key.pem 2048 + +system-ca-network-intermediate-cert.pem: \ + system-ca-network-intermediate-key.pem \ + system-ca-network.cnf \ + fake-startcom-root-cert.pem \ + fake-startcom-root-key.pem + openssl req -new \ + -sha256 \ + -key system-ca-network-intermediate-key.pem \ + -subj "/CN=NodeJS-System-CA-Network-Intermediate" \ + -out system-ca-network-intermediate-cert.csr + openssl x509 -req \ + -in system-ca-network-intermediate-cert.csr \ + -CA fake-startcom-root-cert.pem \ + -CAkey fake-startcom-root-key.pem \ + -set_serial 0x4e6f64654e657403 \ + -out system-ca-network-intermediate-cert.pem \ + -days 99999 \ + -extfile system-ca-network.cnf \ + -extensions v3_ca \ + -sha256 + rm -f system-ca-network-intermediate-cert.csr + +system-ca-network-leaf-key.pem: + openssl genrsa -out system-ca-network-leaf-key.pem 2048 + +system-ca-network-leaf-cert.pem: \ + system-ca-network-leaf-key.pem \ + system-ca-network-intermediate-cert.pem \ + system-ca-network-intermediate-key.pem \ + system-ca-network.cnf + openssl req -new \ + -sha256 \ + -key system-ca-network-leaf-key.pem \ + -subj "/CN=NodeJS-System-CA-Network-Leaf" \ + -out system-ca-network-leaf-cert.csr + openssl x509 -req \ + -in system-ca-network-leaf-cert.csr \ + -CA system-ca-network-intermediate-cert.pem \ + -CAkey system-ca-network-intermediate-key.pem \ + -set_serial 0x4e6f64654e657405 \ + -out system-ca-network-leaf-cert.pem \ + -days 99999 \ + -extfile system-ca-network.cnf \ + -extensions v3_leaf \ + -sha256 + rm -f system-ca-network-leaf-cert.csr + # # agent1 is signed by ca1. # diff --git a/test/fixtures/keys/system-ca-network-intermediate-cert.pem b/test/fixtures/keys/system-ca-network-intermediate-cert.pem new file mode 100644 index 000000000000..21c208f65d9f --- /dev/null +++ b/test/fixtures/keys/system-ca-network-intermediate-cert.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIITm9kZU5ldAMwDQYJKoZIhvcNAQELBQAwfTELMAkGA1UE +BhMCSUwxFjAUBgNVBAoMDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsMIlNlY3VyZSBE +aWdpdGFsIENlcnRpZmljYXRlIFNpZ25pbmcxKTAnBgNVBAMMIFN0YXJ0Q29tIENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MCAXDTI2MDkwNDA3MTI1MloYDzIzMDAwNjE5 +MDcxMjUyWjAwMS4wLAYDVQQDDCVOb2RlSlMtU3lzdGVtLUNBLU5ldHdvcmstSW50 +ZXJtZWRpYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvtvTTze+ +GOXD+rl3wxwLgGR0J5e/0qV4k9CupE5Agp4jw7clbM7KVXQrEsgGP3bR2SAlBsOH +VTkQy+YQnbYEpDUkKKwQZVlVSwpW/7zm72+HMAt89ZlKYZeRi1NZ3YqeLWtPOAGW +diX7IkQGw1Q8FcGjQC/06UqEA5s6lrOY1YdxRGWY0DlF9EQqJK1Xrgl6KtvdV/oU +sui0LARvvm+EpWUeebm4iZSA7Jar9M/6KysoeY1Y8kjqFSqZU9QP0sFSpctKL37c +L5Go8KCI9tvUUFwjfseOTSDyKH/s8671m+RO9lJtlJ26vDG8kY1jNG6OZHdsljIR +m5fCJG9QlF+3rQIDAQABo4HjMIHgMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0P +AQH/BAQDAgEGMB0GA1UdDgQWBBT/bdAcwGLQOOUrDT28Nv51jdnBrzCBmgYDVR0j +BIGSMIGPoYGBpH8wfTELMAkGA1UEBhMCSUwxFjAUBgNVBAoMDVN0YXJ0Q29tIEx0 +ZC4xKzApBgNVBAsMIlNlY3VyZSBEaWdpdGFsIENlcnRpZmljYXRlIFNpZ25pbmcx +KTAnBgNVBAMMIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggkAgg9v +TE81yAowDQYJKoZIhvcNAQELBQADggEBAHzyyAD0a2lVNkJOk09iwlllCO8kNv7D +BAbZt2byd9CAFZ5j9XMJMibSjOlgA0jK7wMO26rSoVvNjNZNdT+KzLsa6WxPcaXm +37gG7Oj29JvB0uMsAczIyoRvuDzLIYSgZNHFm+SRsHkaQdrjLNXlfc/SmckET6EF +KdmnGILQL1PA/ZC7MEvn9O+u+QsrkVVfziTzPrAJQmUaXhZZW+yoOvHof55mhbNV +wO2bFIvn+OI6HDkQJjcrTdQmM7em++sPSkEO4rUSlQVfgsP/6zpX2WmTyOQZURjq +USuBhVTqEU2QaVMdxRNwRPtgPHoXeBKXnKTRwU9svShFD9SMDyPC4mY= +-----END CERTIFICATE----- diff --git a/test/fixtures/keys/system-ca-network-intermediate-key.pem b/test/fixtures/keys/system-ca-network-intermediate-key.pem new file mode 100644 index 000000000000..9de47899ee13 --- /dev/null +++ b/test/fixtures/keys/system-ca-network-intermediate-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC+29NPN74Y5cP6 +uXfDHAuAZHQnl7/SpXiT0K6kTkCCniPDtyVszspVdCsSyAY/dtHZICUGw4dVORDL +5hCdtgSkNSQorBBlWVVLClb/vObvb4cwC3z1mUphl5GLU1ndip4ta084AZZ2Jfsi +RAbDVDwVwaNAL/TpSoQDmzqWs5jVh3FEZZjQOUX0RCokrVeuCXoq291X+hSy6LQs +BG++b4SlZR55ubiJlIDslqv0z/orKyh5jVjySOoVKplT1A/SwVKly0ovftwvkajw +oIj229RQXCN+x45NIPIof+zzrvWb5E72Um2Unbq8MbyRjWM0bo5kd2yWMhGbl8Ik +b1CUX7etAgMBAAECggEAHOZ1DiEK9kKuhSP59E+mFzQ+2XLzTz6SrlFSbJtTzeOL +11o/fsUmYr+zOQYUWh5QIuBo8uf/MWKxfiqReAwkkkfguI7rcalQDsubRmDQmE+L +STVoH9ltJ693kMEXkcIodwixh3HIGo5gQ31JB+FUsteuNG0+mOfZayFhgwnhee2j +li4pxdX87xP56gs5jSLx+5XAFkzJFCjxhovhenDIzC7/8NzCHWXouvZou08h69mn +CBUfZrjZfr1TJTjJo4bthxDHlZF6YUGUv9dLquhH/6+sCXsq9QZoh7gZs7/wfb/p +58gzJ1S9Us+ngcVDt9lkba/0B/13dXNhYgfAkF/mYQKBgQD+pez5+zRtNHU9pDv2 +yMzUXm2lDBMghanXbOJFfaqYYoaBttjhdMUl5EPKebx+EAEQzX/ws/MH1X63q1KK +ZdhJ81Gu0QTg710OjUPFSnBX6t6gdSbmG00iVPzZYyTzYcacsKhqzpDs6iDBMfHE +VtqzQJlZbvOZMQUlx2+FWknZ4QKBgQC/3zU/QwWw5emPIvITZjhyN59qjbnJc84v +GK0cpLY2bxpuguW3YSrnnxN5U9Epd4IdJddP21I/TtnZA9+FgfIelFLbPTkSZA61 +JC269JgKDAXeEflVssGeBbwdbeP2RzxvgvyRgYfA5s0OtcSwb8VkM85zA+peNYAI +9pbZtwUPTQKBgDYTTDCFnMt38VGmt6kfWQfcCZKb7s/gMoHxuFcjaX+0pHWVO5cc +8eSrBY5QratjgKZtzTZzMC/AsYkVU5DaMcT6a6Ug89VEX1GAxBx4Q0UYNjCC0EDb +GVUqEQuk1yrr335UsV8FAJee3FfKYFneS9D27dmlCwvD8iDkqhT6aGZhAoGBAKey +rxdbvN2gFULFiE7sSOmLNAotVPR+Avem2abvT1LESJXOUni0tIs8vgiM+YMEsPRI +3qctSfWLNVw3xmUp7jRL7x8vo3IvgatQs6dzoG8nkvlFPGdqlU9IM+KE6XmXSXst +QRLr43r2PsNcm0yaZCvwG8Dq6yzsFbu/ypj/Ha0BAoGBAIYajJc1dPYP167VjV1b +AgUD+/JpbsLj+GknOv4zrBZdipSpGzPbanHwRGRtWA2x67iA1jv+F5iL4wykUzZB +4FB0wB4occkOraXEeEwqm5/BdBiKTqEGNf5wGv67ciNOWkdamWj1DE7PQ2RpgjiT +7ChZGDlBcpH46Ttp+ndJPKzX +-----END PRIVATE KEY----- diff --git a/test/fixtures/keys/system-ca-network-leaf-cert.pem b/test/fixtures/keys/system-ca-network-leaf-cert.pem new file mode 100644 index 000000000000..85647008ae59 --- /dev/null +++ b/test/fixtures/keys/system-ca-network-leaf-cert.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID6TCCAtGgAwIBAgIITm9kZU5ldAUwDQYJKoZIhvcNAQELBQAwMDEuMCwGA1UE +AwwlTm9kZUpTLVN5c3RlbS1DQS1OZXR3b3JrLUludGVybWVkaWF0ZTAgFw0yNjA5 +MDQwNzEyNTJaGA8yMzAwMDYxOTA3MTI1MlowKDEmMCQGA1UEAwwdTm9kZUpTLVN5 +c3RlbS1DQS1OZXR3b3JrLUxlYWYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCWJUja7Z1+Q98Id7vyqBuT7/qCRY4CeoD65mC0CtfHEM4fcPZQ/+6+48iQ +V4GRcDjLReuJKf8FsPpfiSCccnfOqpmer9dduWn6mibKCZ9NoV/YFYg3hf9FuNXj +1p9AhMhGNXCiElVWhjUgn7gO39g3jhGKq7IzlnIfS0nvEeJC2prxjr1hJ7x1gTIo +xlBNJuK2OLWz80tcFUrEl6TB14RvEwXU8XOkbdwkK0iWpWpuaAUl647X8UGPqwgW +rb1je3A+DIPtS370IXEyZtdXzHuWbwI93X77HmN8nYTzzCDo7DMlNHLARHfDTX0M +YctPsCqmrZLdZbvqeJ8FsbhKfWVBAgMBAAGjggELMIIBBzAMBgNVHRMBAf8EAjAA +MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw +GgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMGwGCCsGAQUFBwEBBGAwXjAzBggr +BgEFBQcwAoYnaHR0cDovLzEyNy4wLjAuMToxMjM0Ny9pbnRlcm1lZGlhdGUuZGVy +MCcGCCsGAQUFBzABhhtodHRwOi8vMTI3LjAuMC4xOjEyMzQ3L29jc3AwHQYDVR0O +BBYEFEaI/ENl5rvz8Bly69p3n1Gany4hMB8GA1UdIwQYMBaAFP9t0BzAYtA45SsN +Pbw2/nWN2cGvMA0GCSqGSIb3DQEBCwUAA4IBAQAe7BCQEuqEl3gnWfM47UmTyb1y +mvJYZmEpChwZaGYrJKfKunt5X0dATzdKPwi8ouXZI+4chLY45/tul7B1BpkqdlY+ +XzaHOYi8oJPyvQKf/r+6V7nWomnfTHuAkrT6RmjgTFYJwblv+N26hOnnewkTLQQI +y8yBvm1BH5GppQpfVG2DhhDImJzur2+4s59Oh4v1aMZHelZyN6wnjAz+CSU9HJxO +G4z4AWiGRdxQKL07d6in8ugvl8cyVuIhFtMbS1SAh4rQXqiFMsBbX3Exbqhlgm+F +p51htAhfXe3QagafY8G+ffVbPxoLO+D4+HPTiGVv5SWyrT4Fh0CbpxuggE+z +-----END CERTIFICATE----- diff --git a/test/fixtures/keys/system-ca-network-leaf-key.pem b/test/fixtures/keys/system-ca-network-leaf-key.pem new file mode 100644 index 000000000000..55891784d8fd --- /dev/null +++ b/test/fixtures/keys/system-ca-network-leaf-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCWJUja7Z1+Q98I +d7vyqBuT7/qCRY4CeoD65mC0CtfHEM4fcPZQ/+6+48iQV4GRcDjLReuJKf8FsPpf +iSCccnfOqpmer9dduWn6mibKCZ9NoV/YFYg3hf9FuNXj1p9AhMhGNXCiElVWhjUg +n7gO39g3jhGKq7IzlnIfS0nvEeJC2prxjr1hJ7x1gTIoxlBNJuK2OLWz80tcFUrE +l6TB14RvEwXU8XOkbdwkK0iWpWpuaAUl647X8UGPqwgWrb1je3A+DIPtS370IXEy +ZtdXzHuWbwI93X77HmN8nYTzzCDo7DMlNHLARHfDTX0MYctPsCqmrZLdZbvqeJ8F +sbhKfWVBAgMBAAECggEAAtw9+ZG6RNwFJTOmvHYwZE6XjqDFlk2AJId8AyILv0qW +KqorvxpJEZDkUr+gA8XIBzNTS98lcRXQhz5lBSX3G1RLrea6qxnrp28FWyb4xnFT +EppCaJBgbxMY3AjMKt38tL2y4huITagr3e29A2eHRnOp6OLhd7MfhHIaO/gyv8RD +369o6sxt6RS0N/oGWioeNpIO6XFVZLCfzvDXUbVPPO4KLrqi9cicfKUgT+xBDAx8 +D/a4vXqGIdEMuczVCZMubI0omqSbMf3OO0P/lMIhnNAtXRG4DTnvTssl9nDTfZgI +QUwISjq3w3t5+hk7gqBxvNrRK7WzQHA0IUhWozURgQKBgQDR0v0Rh3PcGKTvoh5x +os9Wbn4jo5cgU0rbxIG0p3OZY1hbLOUB+GYUvnH/ob+keB+0z3XgGvxBYHg1YDmX +rEtrTHeDDL0lXYslj1GVGMrkraHgQbZ5D0nKtuenHKDDjcwot2TEPseiK4qz8LMP +l1Cyx8PRvpUeYVFwzzJiPTfMMQKBgQC3MCUUEtblfJ56xiuLYLyO6DllKJuPmseq +c0p6mgFNUPpPdVY4yb9XmD6S3Zx5ua9ToQnJWmDVDgvbDEDrS+dUqKvVjPTn+QJP +G7Scaet4ku4b4rW13KjqOzX3wrVFsIWsCk4TITeHvJ60sM+Esbfe8pYXIbZGXN9M +QUTP1v62EQKBgQCa74pRTE0AQ+5eyL5kOM2TTAm2tVCd3QNU3njMdi3FIPeYc8Ey +wEJYpPw2wDBr+QuojtzCvApzGBILj99XITGbrJ4/VcPBeJ6MAJVEXEdNruzuVHey +UtixaHgMDPalD+qmUi2FP7pYBIDqOB1wsNI1AY+WLH2eLkxd2ZGhuwSdsQKBgFPa +Tu6sXP/5FlYexinrowRLWpJqjyKh+AmW8p3CRxxMF/gtPsWJ2/NNtj7mAE1b6NyA +F4m6vB5qapTfPAw413BSMS30ctqIij0J8peyiF3N6/6JvSG9Gvj1F5QM5Ys77UQ+ +B1UMXG3esksFsEgOTZtZMcju3o/AzVoLemMlB7zxAoGBAMnu67XJ2wjix6A+q/tB +RGGcmqFiOhERhkHj1uh0K0Mdlx2vRcrfVb7PLfbeUSQy0VrZWsUjP5FuExmNunbg +M2jg2NHQ4dEWyV6/UBZ/lcA048aLZPoKglAVU7s75SRFkYYGdo+elz+cXjsdyURr +cwAPnlqqpcReBDRTGtgSreNI +-----END PRIVATE KEY----- diff --git a/test/fixtures/keys/system-ca-network.cnf b/test/fixtures/keys/system-ca-network.cnf new file mode 100644 index 000000000000..dbf0f67cae19 --- /dev/null +++ b/test/fixtures/keys/system-ca-network.cnf @@ -0,0 +1,13 @@ +[v3_ca] +basicConstraints = critical,CA:TRUE,pathlen:0 +keyUsage = critical,keyCertSign,cRLSign +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer + +[v3_leaf] +basicConstraints = critical,CA:FALSE +keyUsage = critical,digitalSignature,keyEncipherment +extendedKeyUsage = serverAuth,clientAuth +subjectAltName = DNS:localhost,IP:127.0.0.1 +authorityInfoAccess = caIssuers;URI:http://127.0.0.1:12347/intermediate.der,\ + OCSP;URI:http://127.0.0.1:12347/ocsp diff --git a/test/system-ca/test-macos-cert-network-fetch.mjs b/test/system-ca/test-macos-cert-network-fetch.mjs index 111bc7f4e97d..4d029421d5af 100644 --- a/test/system-ca/test-macos-cert-network-fetch.mjs +++ b/test/system-ca/test-macos-cert-network-fetch.mjs @@ -1,16 +1,12 @@ import * as common from '../common/index.mjs'; import * as fixtures from '../common/fixtures.mjs'; import assert from 'node:assert/strict'; +import { X509Certificate } from 'node:crypto'; import { execFile, execFileSync } from 'node:child_process'; -import fs from 'node:fs'; import http from 'node:http'; import { once } from 'node:events'; import { promisify } from 'node:util'; import { test } from 'node:test'; -import tmpdir from '../common/tmpdir.js'; -import cryptoFixtures from '../common/crypto.js'; - -const { opensslCli } = cryptoFixtures; if (!common.hasCrypto) { common.skip('requires crypto'); @@ -20,10 +16,6 @@ if (process.platform !== 'darwin') { common.skip('macOS-specific test'); } -if (!opensslCli) { - common.skip('missing openssl-cli'); -} - function isCertInKeychain(cn) { try { execFileSync('security', ['find-certificate', '-c', cn], { stdio: 'pipe' }); @@ -41,6 +33,7 @@ if (!isCertInKeychain('StartCom Certification Authority')) { } const execFileAsync = promisify(execFile); +const responderPort = 12347; async function run(file, args) { return execFileAsync(file, args, { encoding: 'utf8' }); @@ -50,82 +43,6 @@ async function runToCompletion(file, args) { await new Promise((resolve) => execFile(file, args, resolve)); } -async function generateCertificates(port) { - const intermediateKey = tmpdir.resolve('intermediate-key.pem'); - const intermediateCsr = tmpdir.resolve('intermediate.csr'); - const intermediateCert = tmpdir.resolve('intermediate-cert.pem'); - const intermediateDer = tmpdir.resolve('intermediate-cert.der'); - const intermediateConfig = tmpdir.resolve('intermediate.cnf'); - const leafKey = tmpdir.resolve('leaf-key.pem'); - const leafCsr = tmpdir.resolve('leaf.csr'); - const leafCert = tmpdir.resolve('leaf-cert.pem'); - const leafConfig = tmpdir.resolve('leaf.cnf'); - const rootCert = fixtures.path('keys', 'fake-startcom-root-cert.pem'); - const rootKey = fixtures.path('keys', 'fake-startcom-root-key.pem'); - - fs.writeFileSync(intermediateConfig, ` -[v3_ca] -basicConstraints = critical,CA:TRUE,pathlen:0 -keyUsage = critical,keyCertSign,cRLSign -subjectKeyIdentifier = hash -authorityKeyIdentifier = keyid,issuer -`); - fs.writeFileSync(leafConfig, ` -[v3_leaf] -basicConstraints = critical,CA:FALSE -keyUsage = critical,digitalSignature,keyEncipherment -extendedKeyUsage = serverAuth,clientAuth -subjectAltName = DNS:localhost,IP:127.0.0.1 -authorityInfoAccess = caIssuers;URI:http://127.0.0.1:${port}/intermediate.der,\\ - OCSP;URI:http://127.0.0.1:${port}/ocsp -`); - - await run(opensslCli, [ - 'req', '-new', '-newkey', 'rsa:2048', '-noenc', - '-keyout', intermediateKey, - '-out', intermediateCsr, - '-subj', '/CN=NodeJS Test AIA Intermediate', - ]); - await run(opensslCli, [ - 'x509', '-req', - '-in', intermediateCsr, - '-CA', rootCert, - '-CAkey', rootKey, - '-set_serial', `0x${Date.now().toString(16)}01`, - '-out', intermediateCert, - '-days', '1', - '-extfile', intermediateConfig, - '-extensions', 'v3_ca', - ]); - await run(opensslCli, [ - 'x509', '-in', intermediateCert, '-outform', 'DER', '-out', intermediateDer, - ]); - await run(opensslCli, [ - 'req', '-new', '-newkey', 'rsa:2048', '-noenc', - '-keyout', leafKey, - '-out', leafCsr, - '-subj', '/CN=NodeJS Test AIA Leaf', - ]); - await run(opensslCli, [ - 'x509', '-req', - '-in', leafCsr, - '-CA', intermediateCert, - '-CAkey', intermediateKey, - '-set_serial', `0x${Date.now().toString(16)}02`, - '-out', leafCert, - '-days', '1', - '-extfile', leafConfig, - '-extensions', 'v3_leaf', - ]); - - return { - intermediateCert, - intermediateDer, - leafCert, - rootCert, - }; -} - function parseKeychainSearchList(stdout) { return stdout.trim().split(/\r?\n/) .map((line) => line.trim().replace(/^"|"$/g, '')) @@ -135,34 +52,36 @@ function parseKeychainSearchList(stdout) { test('system CA enumeration does not fetch AIA or OCSP', { timeout: 30_000, }, async (t) => { - tmpdir.refresh(); - const requests = []; - let intermediate; + const leafCert = fixtures.path('keys', 'system-ca-network-leaf-cert.pem'); + const rootCert = fixtures.path('keys', 'fake-startcom-root-cert.pem'); + const infoAccess = new X509Certificate( + fixtures.readKey('system-ca-network-leaf-cert.pem'), + ).infoAccess; + assert.match( + infoAccess, + /CA Issuers - URI:http:\/\/127\.0\.0\.1:12347\/intermediate\.der/, + ); + assert.match(infoAccess, /OCSP - URI:http:\/\/127\.0\.0\.1:12347\/ocsp/); + const server = http.createServer((req, res) => { requests.push({ method: req.method, url: req.url }); if (req.url === '/intermediate.der') { - res.writeHead(200, { 'Content-Type': 'application/pkix-cert' }); - res.end(intermediate); + res.writeHead(404, { 'Cache-Control': 'no-store' }); + res.end(); } else if (req.url?.startsWith('/ocsp')) { - res.writeHead(500); + res.writeHead(500, { 'Cache-Control': 'no-store' }); res.end(); } else { res.writeHead(404); res.end(); } }); - server.listen(0, '127.0.0.1'); + server.listen(responderPort, '127.0.0.1'); await once(server, 'listening'); t.after(() => new Promise((resolve) => server.close(resolve))); - const address = server.address(); - assert.notStrictEqual(address, null); - assert.notStrictEqual(typeof address, 'string'); - const certificates = await generateCertificates(address.port); - intermediate = fs.readFileSync(certificates.intermediateDer); - - const keychain = tmpdir.resolve('node-system-ca-test.keychain-db'); + const keychain = '/tmp/node-system-ca-network-test.keychain-db'; const password = 'node-test'; const { stdout } = await run('/usr/bin/security', [ 'list-keychains', '-d', 'user', @@ -185,7 +104,7 @@ test('system CA enumeration does not fetch AIA or OCSP', { 'set-keychain-settings', '-lut', '3600', keychain, ]); await run('/usr/bin/security', [ - 'add-certificates', '-k', keychain, certificates.leafCert, + 'add-certificates', '-k', keychain, leafCert, ]); await run('/usr/bin/security', [ 'list-keychains', '-d', 'user', '-s', ...originalKeychains, keychain, @@ -202,10 +121,12 @@ test('system CA enumeration does not fetch AIA or OCSP', { // certificate can trigger both types of network request. await runToCompletion('/usr/bin/security', [ 'verify-cert', - '-c', certificates.leafCert, - '-r', certificates.rootCert, - '-p', 'ssl', - '-n', 'localhost', + '-c', leafCert, + '-r', rootCert, + '-p', 'basic', + '-R', 'ocsp', + '-R', 'online', + '-R', 'require', ]); const validationFetchedAia = requests.some( ({ url }) => url === '/intermediate.der', @@ -214,12 +135,15 @@ test('system CA enumeration does not fetch AIA or OCSP', { requests.length = 0; await runToCompletion('/usr/bin/security', [ 'verify-cert', - '-c', certificates.leafCert, - '-c', certificates.intermediateCert, - '-r', certificates.rootCert, - '-p', 'ssl', - '-n', 'localhost', + '-c', leafCert, + '-c', fixtures.path( + 'keys', + 'system-ca-network-intermediate-cert.pem', + ), + '-r', rootCert, + '-p', 'basic', '-R', 'ocsp', + '-R', 'online', '-R', 'require', ]); const validationRequestedOcsp = requests.some( From 027ea517361ab0cd0680c5d5b0ba677facb248e1 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 4 Sep 2026 10:57:00 +0200 Subject: [PATCH 3/3] crypto: preserve system CA certificate filtering Keep the existing SSL client policy and explicitly disable network access. This avoids AIA and revocation requests without changing which certificates pass the fallback evaluation. Assisted-by: AI coding assistant Signed-off-by: Christof Marti --- src/crypto/crypto_context.cc | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index 82297629912c..dcde175f8b16 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -450,17 +450,18 @@ bool IsCertificateTrustValid(SecCertificateRef ref) { CFArrayCreateMutable(nullptr, 1, &kCFTypeArrayCallBacks); CFArraySetValueAtIndex(subj_certs, 0, ref); - // Enumerating trust anchors must not constrain them to a particular - // application policy. The actual TLS handshake applies the appropriate EKU - // checks when validating the peer certificate. Basic X.509 policy also - // disables network access, avoiding AIA and revocation requests during + // Use the SSL client policy to preserve the existing certificate filtering, + // but disable network access to avoid AIA and revocation requests during // certificate enumeration. - SecPolicyRef policy = SecPolicyCreateBasicX509(); + SecPolicyRef policy = SecPolicyCreateSSL(false, nullptr); OSStatus ortn = SecTrustCreateWithCertificates(subj_certs, policy, &sec_trust); bool result = false; if (ortn) { /* should never happen */ + } else if (SecTrustSetNetworkFetchAllowed(sec_trust, false) != + errSecSuccess) { + /* should never happen */ } else { result = SecTrustEvaluateWithError(sec_trust, nullptr); }