From a0c4af6deedd040f18e515f252d10a28047ed886 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 823d87e7f204..63d25260a1d8 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -451,18 +451,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 929ac99d47ee5e86db817128b662b226e77eeb88 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 605acf75afb6..bd5c98bc9e6f 100644 --- a/test/fixtures/keys/Makefile +++ b/test/fixtures/keys/Makefile @@ -57,6 +57,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 \ @@ -422,6 +426,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 bca0c136e883c4025fb655708f6a77d36943b0de 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 63d25260a1d8..9e38f1f8634c 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -451,17 +451,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); }