From 197071734179c73f318aea77be90c4e391a173f4 Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Thu, 30 Jul 2026 01:25:00 +0100 Subject: [PATCH 1/4] feat(windows): build curl against OpenSSL instead of Schannel Schannel verifies against a CA file instead of the Windows certificate store, and refuses a file larger than 1 MiB. The CLI has to pass a CA file, because the openssl extension cannot read the store at all, so a root certificate installed by an organization is never trusted, and requests fail with cURL error 60. curl built against OpenSSL loads a CA file and the Windows stores together, and has no size limit. OpenSSL is already built here for the openssl extension. static-php-cli chose Schannel in its pull request 674, which also removed OpenSSL from curl's Windows dependencies, so both are changed back. The patch script fails the build if static-php-cli no longer looks the way it expects, rather than quietly producing a Schannel build, and a new step checks the backend of the binary that was built. Related to https://github.com/upsun/cli/issues/110 Written by Claude Code. --- .github/workflows/build-php.yml | 21 +++++++ README.md | 16 ++++++ scripts/patch-spc-windows-curl.php | 91 ++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 scripts/patch-spc-windows-curl.php diff --git a/.github/workflows/build-php.yml b/.github/workflows/build-php.yml index b5f6cf8..4deac03 100644 --- a/.github/workflows/build-php.yml +++ b/.github/workflows/build-php.yml @@ -222,6 +222,9 @@ jobs: needs: resolve-version runs-on: windows-latest steps: + - name: Check out repository code + uses: actions/checkout@v7 + - name: Clone static-php-cli run: | git clone --depth 1 --branch ${{ env.SPC_VERSION }} https://github.com/crazywhalecc/static-php-cli.git spc @@ -238,6 +241,12 @@ jobs: cd spc composer install --no-dev --classmap-authoritative + # Schannel cannot use a CA file and the Windows certificate store + # together, which the CLI needs. This must run before the download step, + # which reads the dependencies it changes. + - name: Build curl against OpenSSL, not Schannel + run: php scripts/patch-spc-windows-curl.php spc + - name: Setup build environment run: | cd spc @@ -259,6 +268,18 @@ jobs: $extensions = $extensions.Trim(',') ./bin/spc build "$extensions" --build-cli ${{ inputs.debug && '--debug' || '' }} + # The CLI depends on the SSL backend: only OpenSSL can use a CA file and + # the Windows certificate store together. + - name: Check the SSL backend + run: | + $curl = & ./spc/buildroot/bin/php.exe -n -r "echo curl_version()['ssl_version'];" + $openssl = & ./spc/buildroot/bin/php.exe -n -r "echo OPENSSL_VERSION_TEXT;" + Write-Host "curl: $curl" + Write-Host "openssl extension: $openssl" + if ($curl -notlike 'OpenSSL/*') { + throw "expected curl to be built against OpenSSL, got '$curl'" + } + - name: Upload artifact uses: actions/upload-artifact@v7 with: diff --git a/README.md b/README.md index dc9ab35..f1dd270 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,22 @@ The workflow will: - **Linux/macOS**: static-php-cli automatically uses the system CA bundle - **Windows**: Requires explicit `cacert.pem` configuration (handled by the CLI) +### Windows SSL backend + +static-php-cli builds curl with Schannel on Windows. These builds change that to +OpenSSL, which is already built for the `openssl` extension, using +`scripts/patch-spc-windows-curl.php`. + +Schannel verifies against a CA file *instead of* the Windows certificate store, +and refuses a file larger than 1 MiB. The CLI has to pass a CA file, because the +`openssl` extension cannot read the store at all, so with Schannel a root +certificate installed by an organization is never trusted. curl built against +OpenSSL loads a CA file and the Windows stores together, and has no size limit. + +The patch fails the build if static-php-cli changes in a way it does not expect, +rather than quietly producing a Schannel build. See +[upsun/cli#110](https://github.com/upsun/cli/issues/110). + ## License The build scripts in this repository are MIT licensed. PHP binaries are subject to the [PHP License](https://www.php.net/license/). diff --git a/scripts/patch-spc-windows-curl.php b/scripts/patch-spc-windows-curl.php new file mode 100644 index 0000000..e535a6f --- /dev/null +++ b/scripts/patch-spc-windows-curl.php @@ -0,0 +1,91 @@ + "'-DUSE_WINDOWS_SSPI=OFF '", + "'-DCURL_USE_SCHANNEL=ON '" => "'-DCURL_USE_SCHANNEL=OFF '", + "'-DCURL_USE_OPENSSL=OFF '" => "'-DCURL_USE_OPENSSL=ON '", +]; +foreach ($options as $from => $to) { + if (substr_count($source, $to) === 1 && !str_contains($source, $from)) { + echo "already set: $to\n"; + continue; + } + if (substr_count($source, $from) !== 1) { + fail("expected exactly one $from in $curlFile: check the static-php-cli version"); + } + $source = str_replace($from, $to, $source); + echo "set $to\n"; +} +writeFileOrFail($curlFile, $source); From b40cf0658bbf2b88b4238e087aa78f82ca704029 Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Thu, 30 Jul 2026 01:27:58 +0100 Subject: [PATCH 2/4] fix(windows): pin the runner image to one with Visual Studio 2022 The windows-latest image now ships Visual Studio 2026, and static-php-cli only looks for 2022 or 2019, so its doctor check fails before anything is built. Nothing had been built since April, so this went unnoticed. Written by Claude Code. --- .github/workflows/build-php.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-php.yml b/.github/workflows/build-php.yml index 4deac03..a7d3f13 100644 --- a/.github/workflows/build-php.yml +++ b/.github/workflows/build-php.yml @@ -220,7 +220,9 @@ jobs: build-windows-amd64: name: Windows x64 needs: resolve-version - runs-on: windows-latest + # Not windows-latest: that image now ships Visual Studio 2026, and + # static-php-cli only looks for 2022 or 2019, so its doctor check fails. + runs-on: windows-2025 steps: - name: Check out repository code uses: actions/checkout@v7 From 0b18728107b0193f656ea569682edad6af5ec569 Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Thu, 30 Jul 2026 01:29:55 +0100 Subject: [PATCH 3/4] fix(windows): use the windows-2022 runner, which has Visual Studio 2022 The windows-2025 image ships Visual Studio 2026 as well, so pinning it was not enough. Written by Claude Code. --- .github/workflows/build-php.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-php.yml b/.github/workflows/build-php.yml index a7d3f13..d86a30e 100644 --- a/.github/workflows/build-php.yml +++ b/.github/workflows/build-php.yml @@ -220,9 +220,10 @@ jobs: build-windows-amd64: name: Windows x64 needs: resolve-version - # Not windows-latest: that image now ships Visual Studio 2026, and - # static-php-cli only looks for 2022 or 2019, so its doctor check fails. - runs-on: windows-2025 + # Not windows-latest or windows-2025: those images now ship Visual Studio + # 2026, and static-php-cli only looks for 2022 or 2019, so its doctor check + # fails before anything is built. + runs-on: windows-2022 steps: - name: Check out repository code uses: actions/checkout@v7 From dceeeb1b65475c9d442fb463490c61dd83629003 Mon Sep 17 00:00:00 2001 From: Patrick Dawkins Date: Thu, 30 Jul 2026 01:50:12 +0100 Subject: [PATCH 4/4] feat(windows): use the certificate store when no CA file is set Schannel fell back to the Windows certificate store when curl was given no CA file. OpenSSL does not, and CURL_CA_NATIVE is off by default, so without this a request with no CA file would have no trust anchors at all. curl only auto-enables the store when no CA file is set, so this does not change the case where the CLI passes its bundle. Written by Claude Code. --- scripts/patch-spc-windows-curl.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/scripts/patch-spc-windows-curl.php b/scripts/patch-spc-windows-curl.php index e535a6f..b2d8b65 100644 --- a/scripts/patch-spc-windows-curl.php +++ b/scripts/patch-spc-windows-curl.php @@ -88,4 +88,20 @@ function writeFileOrFail(string $path, string $contents): void $source = str_replace($from, $to, $source); echo "set $to\n"; } + +// Use the Windows certificate store when no CA file is set, which is what +// Schannel did. Only then: given a CA file, curl uses that alone unless asked +// for both. This keeps verification working if the CLI cannot write its bundle. +$nativeCA = "'-DCURL_CA_NATIVE=ON '"; +$anchor = "'-DCURL_ENABLE_SSL=ON ' ."; +if (str_contains($source, $nativeCA)) { + echo "already set: $nativeCA\n"; +} else { + if (substr_count($source, $anchor) !== 1) { + fail("expected exactly one $anchor in $curlFile: check the static-php-cli version"); + } + $source = str_replace($anchor, $nativeCA . " .\n " . $anchor, $source); + echo "set $nativeCA\n"; +} + writeFileOrFail($curlFile, $source);