Skip to content

Commit 16e6c05

Browse files
ondrejmirtesclaude
andcommitted
Verify Composer's signature instead of pinning its version and checksum
The turbo container legs downloaded a pinned Composer (COMPOSER_VERSION and COMPOSER_PHAR_SHA256), which had fallen behind (2.9.2 while 2.10.3 is out) and needed a manual bump with a new checksum each time. download-composer.sh takes the newest Composer 2 release and verifies it the way `composer self-update` does: the release's composer.phar.sig must verify against Composer's "Tags" public key, committed as composer-tags.pub from https://composer.github.io/pubkeys.html, which is hosted apart from getcomposer.org. A compromised getcomposer.org still cannot supply a phar that passes, and there is nothing left to bump. A phar that fails the check is deleted instead of being left where the step would run it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017h1GfB7vfJPgwcTHD9Fth6
1 parent df607f6 commit 16e6c05

4 files changed

Lines changed: 77 additions & 29 deletions

File tree

‎.github/scripts/composer-tags.pub‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0Vi/2K6apCVj76nCnCl2
3+
MQUPdK+A9eqkYBacXo2wQBYmyVlXm2/n/ZsX6pCLYPQTHyr5jXbkQzBw8SKqPdlh
4+
vA7NpbMeNCz7wP/AobvUXM8xQuXKbMDTY2uZ4O7sM+PfGbptKPBGLe8Z8d2sUnTO
5+
bXtX6Lrj13wkRto7st/w/Yp33RHe9SlqkiiS4MsH1jBkcIkEHsRaveZzedUaxY0M
6+
mba0uPhGUInpPzEHwrYqBBEtWvP97t2vtfx8I5qv28kh0Y6t+jnjL1Urid2iuQZf
7+
noCMFIOu4vksK5HxJxxrN0GOmGmwVQjOOtxkwikNiotZGPR4KsVj8NnBrLX7oGuM
8+
nQvGciiu+KoC2r3HDBrpDeBVdOWxDzT5R4iI0KoLzFh2pKqwbY+obNPS2bj+2dgJ
9+
rV3V5Jjry42QOCBN3c88wU1PKftOLj2ECpewY6vnE478IipiEu7EAdK8Zwj2LmTr
10+
RKQUSa9k7ggBkYZWAeO/2Ag0ey3g2bg7eqk+sHEq5ynIXd5lhv6tC5PBdHlWipDK
11+
tl2IxiEnejnOmAzGVivE1YGduYBjN+mjxDVy8KGBrjnz1JPgAvgdwJ2dYw4Rsc/e
12+
TzCFWGk/HM6a4f0IzBWbJ5ot0PIi4amk07IotBXDWwqDiQTwyuGCym5EqWQ2BD95
13+
RGv89BPD+2DLnJysngsvVaUCAwEAAQ==
14+
-----END PUBLIC KEY-----
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/bin/sh
2+
# Downloads the newest Composer 2 release into ./composer.phar and verifies
3+
# it the way `composer self-update` does: the release's signature
4+
# (composer.phar.sig, RSA over SHA-384) must verify against Composer's "Tags"
5+
# public key, committed next to this script (composer-tags.pub, from
6+
# https://composer.github.io/pubkeys.html — hosted apart from
7+
# getcomposer.org). A compromised or impersonated getcomposer.org cannot
8+
# supply a phar that passes, and there is no version or checksum to bump.
9+
#
10+
# POSIX sh: it also runs in the Alpine build containers. Needs curl and a PHP
11+
# with the openssl extension, which Composer requires anyway.
12+
set -eu
13+
14+
KEY="$(dirname "$0")/composer-tags.pub"
15+
# the download path of the newest 2.x release, e.g. /download/2.10.3/composer.phar
16+
# shellcheck disable=SC2016 # PHP code, not shell expansions
17+
RELEASE=$(curl -fsSL --retry 3 https://getcomposer.org/versions | php -r '
18+
$path = json_decode(stream_get_contents(STDIN), true, 512, JSON_THROW_ON_ERROR)["2"][0]["path"] ?? "";
19+
if (preg_match("~^/download/2\.[0-9]+\.[0-9]+/composer\.phar$~", $path) !== 1) {
20+
fwrite(STDERR, "unexpected Composer 2 download path: $path\n");
21+
exit(1);
22+
}
23+
echo $path;
24+
')
25+
# verified under another name first, so a phar that fails is never left
26+
# where the caller runs it
27+
curl -fsSLo composer.phar.unverified --retry 3 "https://getcomposer.org$RELEASE"
28+
curl -fsSLo composer.phar.sig --retry 3 "https://getcomposer.org$RELEASE.sig"
29+
# shellcheck disable=SC2016 # PHP code, not shell expansions
30+
php -r '
31+
$signature = json_decode(file_get_contents("composer.phar.sig"), true, 512, JSON_THROW_ON_ERROR)["sha384"] ?? null;
32+
$decoded = is_string($signature) ? base64_decode($signature, true) : false;
33+
if ($decoded === false || openssl_verify(file_get_contents("composer.phar.unverified"), $decoded, file_get_contents($argv[1]), OPENSSL_ALGO_SHA384) !== 1) {
34+
fwrite(STDERR, "the Composer download does not match its signature\n");
35+
unlink("composer.phar.unverified");
36+
exit(1);
37+
}
38+
' "$KEY"
39+
rm composer.phar.sig
40+
mv composer.phar.unverified composer.phar
41+
echo "Composer $(php composer.phar --version --no-ansi 2>/dev/null | sed -n 's/^Composer version \([^ ]*\).*/\1/p'), signature verified"

‎.github/scripts/find-turbo-origins.sh‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ BUILD_INPUT_PATHS=(
4747
turbo-ext/bin/pgo-train.sh
4848
.github/turbo-build
4949
.github/scripts/install-alpine-php.sh
50+
.github/scripts/download-composer.sh
51+
.github/scripts/composer-tags.pub
5052
.github/scripts/install-php86-windows.sh
5153
.github/scripts/download-php-windows-devel.sh
5254
turbo-ext/bin/shared-core

‎.github/workflows/phar.yml‎

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,6 @@ env:
3030
# The php-parser version whose grammar tables and semantic actions the
3131
# native parser engine (turbo-ext/src/parser/) was ported against.
3232
SUPPORTED_PHP_PARSER_VERSION: "v5.9.0"
33-
# Composer for the turbo container legs, pinned by checksum. The distro
34-
# composers are unusable there: jammy's 2.2.6 fatals under PHP 8.5, and
35-
# Alpine's /usr/bin/composer is a shell wrapper hardwired to the
36-
# distro-default PHP.
37-
COMPOSER_VERSION: "2.9.2"
38-
COMPOSER_PHAR_SHA256: "471f2d857abf0ec18af7b055e61472214d91adb24f9bdbbb864c1c64faad7dd6"
3933
# setup-php installs an 8.6 nightly, which has no matching Windows devel pack.
4034
PHP86_WINDOWS_VERSION: "8.6.0RC2"
4135

@@ -520,14 +514,16 @@ jobs:
520514
with:
521515
ignore-cache: true
522516

523-
- name: "Install Composer dependencies (pinned composer)"
517+
- name: "Install Composer dependencies (verified composer)"
524518
if: env.ORIGIN_RUN_ID == '' && matrix.target.family != 'macos'
525-
# checksum-pinned composer, run under the matrix PHP so platform
526-
# requirements are validated against the interpreter the build
527-
# trains on
519+
# signature-verified composer (.github/scripts/download-composer.sh;
520+
# the distro composers are unusable here: jammy's 2.2.6 fatals under
521+
# PHP 8.5, and Alpine's /usr/bin/composer is a shell wrapper
522+
# hardwired to the distro-default PHP), run under the matrix PHP so
523+
# platform requirements are validated against the interpreter the
524+
# build trains on
528525
run: |
529-
curl -fsSLo composer.phar "https://getcomposer.org/download/$COMPOSER_VERSION/composer.phar"
530-
echo "$COMPOSER_PHAR_SHA256 composer.phar" | sha256sum -c -
526+
sh .github/scripts/download-composer.sh
531527
php composer.phar install --no-interaction --no-progress
532528
rm composer.phar
533529
@@ -648,7 +644,7 @@ jobs:
648644
run: |
649645
docker run -d --name alpine-build \
650646
-v "$PWD:/work" -w /work \
651-
-e PHP_MINOR -e PHP_ZTS -e COMPOSER_VERSION -e COMPOSER_PHAR_SHA256 \
647+
-e PHP_MINOR -e PHP_ZTS \
652648
alpine:3.24 sleep 7200
653649
654650
- name: "Install PHP and build tools"
@@ -663,12 +659,11 @@ jobs:
663659
run: docker exec alpine-build git config --global --add safe.directory /work
664660

665661
# The training run of `make pgo` runs bin/phpstan on the checkout.
666-
- name: "Install Composer dependencies (pinned composer)"
662+
- name: "Install Composer dependencies (verified composer)"
667663
if: env.ORIGIN_RUN_ID == ''
668664
run: |
669665
docker exec -i alpine-build sh -e <<'EOF'
670-
curl -fsSLo composer.phar "https://getcomposer.org/download/$COMPOSER_VERSION/composer.phar"
671-
echo "$COMPOSER_PHAR_SHA256 composer.phar" | sha256sum -c -
666+
sh .github/scripts/download-composer.sh
672667
COMPOSER_HOME="$(mktemp -d)" php composer.phar install --no-interaction --no-progress
673668
rm composer.phar
674669
EOF
@@ -844,11 +839,10 @@ jobs:
844839
with:
845840
ignore-cache: true
846841

847-
- name: "Install Composer dependencies (pinned composer)"
842+
- name: "Install Composer dependencies (verified composer)"
848843
if: matrix.variant == 'pie' && matrix.target.family != 'macos'
849844
run: |
850-
curl -fsSLo composer.phar "https://getcomposer.org/download/$COMPOSER_VERSION/composer.phar"
851-
echo "$COMPOSER_PHAR_SHA256 composer.phar" | sha256sum -c -
845+
sh .github/scripts/download-composer.sh
852846
php composer.phar install --no-interaction --no-progress
853847
rm composer.phar
854848
@@ -961,7 +955,7 @@ jobs:
961955
run: |
962956
docker run -d --name alpine-build \
963957
-v "$PWD:/work" -w /work \
964-
-e PHP_MINOR -e PHP_ZTS -e COMPOSER_VERSION -e COMPOSER_PHAR_SHA256 \
958+
-e PHP_MINOR -e PHP_ZTS \
965959
"$ALPINE_IMAGE" sleep 7200
966960
967961
- name: "Install PHP and build tools"
@@ -986,8 +980,7 @@ jobs:
986980
if: matrix.variant == 'pie'
987981
run: |
988982
docker exec -i -w /work alpine-build sh -e <<'EOF'
989-
curl -fsSLo composer.phar "https://getcomposer.org/download/$COMPOSER_VERSION/composer.phar"
990-
echo "$COMPOSER_PHAR_SHA256 composer.phar" | sha256sum -c -
983+
sh .github/scripts/download-composer.sh
991984
php composer.phar install --no-interaction --no-progress
992985
rm composer.phar
993986
cd turbo-ext
@@ -1874,11 +1867,10 @@ jobs:
18741867
with:
18751868
ignore-cache: true
18761869

1877-
- name: "Install Composer dependencies (pinned composer)"
1870+
- name: "Install Composer dependencies (verified composer)"
18781871
if: ${{ matrix.container }}
18791872
run: |
1880-
curl -fsSLo composer.phar "https://getcomposer.org/download/$COMPOSER_VERSION/composer.phar"
1881-
echo "$COMPOSER_PHAR_SHA256 composer.phar" | sha256sum -c -
1873+
sh .github/scripts/download-composer.sh
18821874
php composer.phar install --no-interaction --no-progress
18831875
rm composer.phar
18841876
@@ -1988,7 +1980,7 @@ jobs:
19881980
run: |
19891981
docker run -d --name alpine-test \
19901982
-v "$PWD:/work" -w /work \
1991-
-e PHP_MINOR -e PHP_ZTS -e COMPOSER_VERSION -e COMPOSER_PHAR_SHA256 \
1983+
-e PHP_MINOR -e PHP_ZTS \
19921984
"$ALPINE_IMAGE" sleep 7200
19931985
19941986
- name: "Install PHP and tools"
@@ -1997,11 +1989,10 @@ jobs:
19971989
- name: "Trust the checkout despite the container/host uid mismatch"
19981990
run: docker exec alpine-test git config --global --add safe.directory /work
19991991

2000-
- name: "Install Composer dependencies (pinned composer)"
1992+
- name: "Install Composer dependencies (verified composer)"
20011993
run: |
20021994
docker exec -i alpine-test sh -e <<'EOF'
2003-
curl -fsSLo composer.phar "https://getcomposer.org/download/$COMPOSER_VERSION/composer.phar"
2004-
echo "$COMPOSER_PHAR_SHA256 composer.phar" | sha256sum -c -
1995+
sh .github/scripts/download-composer.sh
20051996
COMPOSER_HOME="$(mktemp -d)" php composer.phar install --no-interaction --no-progress
20061997
rm composer.phar
20071998
EOF

0 commit comments

Comments
 (0)