Repository navigation
feat(legacy): let Schannel verify certificates on Windows - #259
Merged
Merged
Conversation
The embedded PHP's curl is now built with Schannel (upsun/cli-php-builds), which verifies certificates with Windows: the certificate store (including roots installed for TLS inspection, and roots Windows downloads on demand), the disallowed certificate trust list, and revocation. The wrapper no longer writes a CA bundle (the shipped curl bundle plus the store's roots) or sets openssl.cafile: PHP passes that to curl, and Schannel would then verify against the file instead of the store. The bundle missed trust decisions Windows makes outside the ROOT store, such as the eMudhra intermediates distrusted only through the disallowed CTL. This removes the CA bundle code, the embedded curl.se bundle and its updates in update-php.sh, and file.WriteIfChanged, which only the bundle used. The Windows test now checks that curl is built with Schannel, that an installed root is trusted with best-effort revocation checks, and that a CA file would replace the store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… on Windows PHP streams use OpenSSL, which cannot use the Windows certificate store, so the two remaining HTTPS requests made with them now use Guzzle and curl: - The activity log stream, with a new HttpStream class, which reads a response as it arrives through Guzzle's CurlMultiHandler, waiting for a limited time, as the non-blocking stream did. - The credential helper download. Config::getGuzzleOptions() replaces getStreamContextOptions(), and holds the TLS, proxy and user agent options shared by every HTTP client. With curl built against Schannel, it sets no CA file, so that Windows verifies certificates, and sets CURLSSLOPT_REVOKE_BEST_EFFORT: Schannel's default strict revocation checks reject roots without revocation information, such as many installed to inspect TLS traffic. The API client library's Connector only passes "verify" to Guzzle, so a subclass adds a "curl" option, applied to API requests with a middleware and to OAuth 2.0 token requests with the provider's HTTP client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # internal/legacy/ca_bundle_windows.go # internal/legacy/ca_bundle_windows_test.go # legacy/src/Service/ActivityMonitor.php # legacy/src/Service/Api.php
Build 2 of PHP 8.5.12 builds curl with Schannel on Windows (upsun/cli-php-builds#4). No source archives changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pjcdawkins
marked this pull request as ready for review
October 21, 2026 16:42
Contributor
There was a problem hiding this comment.
Warning
Changes suggested — 🟡 1 warning
🔍 Full review · 25 files reviewed
Verification
Platformsh\Cli\Connectorpushes its curl middleware after the library's OAuth middleware, so OAuth retries and redirects still go through it, and per-requestcurloptions take precedence because of+.- The base
Connector::$configisprotected, and the library'sgetOAuth2Provider()passes the same debug, verify and proxy keys that the override reuses. - No remaining code in the repo references
caBundle,WriteIfChanged,settings(),warnings(),cacert.pem(outside box.json) orgetStreamContextOptions. - The only Guzzle clients in legacy/src are the connector,
getExternalHttpClient, the credential-helper download and the OAuth provider, and all of them now get the options fromgetGuzzleOptions(); no remotefile_get_contentsremains. Api::getCurlOptionssets CAINFO/CAPATH only whenverifyis a string, so on Schannel curl falls back to the Windows store.
New PHPUnit tests (ConfigTest::testGetGuzzleOptions and ConnectorTest, using a built-in-server trait) run in the CI legacy-php job on Linux, where Schannel is not used. The Schannel behaviour is covered only by the reworked TestWindowsCertStoreTrust in the windows-cert-store job, which calls PHP curl directly rather than the legacy CLI's own code. Nothing tests the HTTPS-proxy path.
Review details
- Commit: 66ff4b2
- Model: claude-opus-5-5
Review 1 of 10 for this pull request · View the full run
… too curl reads the TLS options for an HTTPS proxy from CURLOPT_PROXY_SSL_OPTIONS, so with Schannel a proxy whose certificate chains to a root without revocation information (as many TLS inspection roots are) failed the strict default check. It now gets CURLSSLOPT_REVOKE_BEST_EFFORT as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pjcdawkins
added a commit
that referenced
this pull request
Oct 21, 2026
* fix(legacy): use SSL_CERT_FILE on Windows, as Go does Since #259, the embedded PHP on Windows verifies certificates with Schannel against the Windows certificate store, and ignored SSL_CERT_FILE. Go (since 1.27) still uses it on Windows, so with it set, Go and PHP commands would trust different certificates, as before #141. The wrapper now passes SSL_CERT_FILE to PHP as openssl.cafile, which PHP gives to every curl handle as CURLOPT_CAINFO. Schannel then verifies against that file instead of the store, as v5.15.0 did (through Composer\CaBundle) and as Go does. Without SSL_CERT_FILE, nothing is passed and the store is used. An unusable file makes requests fail, as in Go. SSL_CERT_DIR is still not supported on Windows, as Schannel cannot use a directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(legacy): let PHP read SSL_CERT_FILE from the environment The wrapper now sets openssl.cafile to "${SSL_CERT_FILE}", which PHP expands itself, instead of quoting and escaping the path. A path with a literal "${...}" was otherwise expanded by PHP's ini parser. The ini escaping helper is removed, and the test checks paths with "~", backslashes, "$" and quotes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(legacy): verify an HTTPS proxy with SSL_CERT_FILE on Windows too PHP passes openssl.cafile to curl as CURLOPT_CAINFO, which only covers servers: curl verifies an HTTPS proxy with CURLOPT_PROXY_CAINFO, without falling back to CAINFO. So with SSL_CERT_FILE set, Schannel still verified a proxy against the Windows store, while Go uses the file for both. With Schannel, getGuzzleOptions() now also sets CURLOPT_PROXY_CAINFO from openssl.cafile when it is set. The Windows test checks both cases through an HTTPS proxy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On Windows, the CLI gave the embedded PHP a CA bundle: the curl.se bundle plus the roots in the Windows certificate store. That copy misses trust decisions Windows makes outside the ROOT store. For example, Microsoft distrusted the compromised eMudhra intermediates only through its auto-updated disallowed certificate trust list, which is why #256 blocklists three roots.
With curl built with Schannel (upsun/cli-php-builds#4) and no CA file, Windows verifies certificates itself: its certificate store (including roots installed for TLS inspection, see #110), the disallowed list, and revocation.
openssl.cafile: PHP passes it to curl, and Schannel would then use the file instead of the store. The bundle code, the embedded curl.se bundle and its updates inupdate-php.share removed.Config::getGuzzleOptions()holds the TLS, proxy and user agent options, which also set the activity log stream's curl options.verify: trueandCURLSSLOPT_REVOKE_BEST_EFFORT. Schannel's default strict revocation checks reject roots without revocation information, which is common for TLS inspection roots.SSL_CERT_FILEis no longer used on Windows.Connectorsubclass applies the curl options to OAuth 2.0 token requests, which the API client library makes with a separate HTTP client.Tested on Windows 11 with a Schannel build from upsun/cli-php-builds#4: the eMudhra test sites are rejected (
CRYPT_E_REVOKED) for API and token requests, and a root without revocation information installed in the machine store is trusted.This pins PHP 8.5.12 build 2, the first Schannel build. An automatic
update-phppull request for the same build is redundant with this one.🤖 Generated with Claude Code