Skip to content

Commit bdaee42

Browse files
committed
🔒 fix(security): make the published cosign verify command actually verify
The identity regexp published in docs/RELEASE.md, docs/security/security-architecture.md and ADR-0709 was lowercase (`platformrelay/kollect`), but GitHub's OIDC SAN preserves the repository's canonical casing (`PlatformRelay/Kollect`) and `--certificate-identity-regexp` is a case-sensitive Go RE2 pattern. The command therefore could not verify any Kollect release, for anyone, since the first signed release. Proven, not assumed: run against a real published signature (charts/kollect:0.19.0@sha256:7812957c), the old value FAILS and the new one PASSES, with wrong-org, wrong-repo and wrong-issuer negative controls all still rejecting. Why it went unnoticed: .github/release-notes-install.md builds the same command from ${GITHUB_REPOSITORY}, which GitHub expands with canonical casing. Verifying from the release page worked; verifying from the docs never did. The new value is case-tolerant on both path segments and escapes the host dots, matching the convention already proven in the assent repo: ^https://github\.com/[Pp]latform[Rr]elay/[Kk]ollect/.+ Also removes the hardcoded ERE copy of this value in the migration gate. That copy was a second source of truth full of regex metacharacters, and it went on asserting the broken lowercase identity while agreeing with itself. It is replaced by logged_f(), a fixed-string twin of logged() fed from the variable the gate already cross-checks against docs/RELEASE.md, so the value can no longer drift between what is pinned and what is run. Mutation-proven: pointing the real cosign call at the old value while leaving the pinned variable correct now reds the gate with the drifted command printed. ADR-0709 keeps its original analysis paragraph and gains a dated amendment: that paragraph's central claim ("every documented verification command matches the signer") was false, and executing its own V1 gate is what proved it.
1 parent a4effa9 commit bdaee42

5 files changed

Lines changed: 30 additions & 8 deletions

File tree

docs/RELEASE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,12 +313,12 @@ PIPELINE_DIGEST="$(crane digest ghcr.io/${REPO}/kollect-pipeline:${TAG#v})"
313313
314314
cosign verify \
315315
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
316-
--certificate-identity-regexp '^https://github.com/platformrelay/kollect/.+' \
316+
--certificate-identity-regexp '^https://github\.com/[Pp]latform[Rr]elay/[Kk]ollect/.+' \
317317
"ghcr.io/${REPO}/kollect@${OP_DIGEST}"
318318
319319
cosign verify \
320320
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
321-
--certificate-identity-regexp '^https://github.com/platformrelay/kollect/.+' \
321+
--certificate-identity-regexp '^https://github\.com/[Pp]latform[Rr]elay/[Kk]ollect/.+' \
322322
"ghcr.io/${REPO}/kollect-pipeline@${PIPELINE_DIGEST}"
323323
```
324324

@@ -346,7 +346,7 @@ cd /tmp/kollect-verify
346346
cosign verify-blob \
347347
--bundle "kollect-${VERSION}.tgz.sigstore.json" \
348348
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
349-
--certificate-identity-regexp '^https://github.com/platformrelay/kollect/.+' \
349+
--certificate-identity-regexp '^https://github\.com/[Pp]latform[Rr]elay/[Kk]ollect/.+' \
350350
"kollect-${VERSION}.tgz"
351351
```
352352

docs/adr/0709-chart-image-oci-path-separation.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,22 @@ workflow path: `cosign verify` at `docs/RELEASE.md:311,316`,
216216
`cosign verify-blob` at `docs/RELEASE.md:343`. The identity pattern is a repository prefix, so any
217217
signature produced under `github.com/platformrelay/kollect/…` satisfies it.
218218
219+
> **Amendment, 2026-09-05 (SEC-VERIFYCASE-01).** The paragraph above is left as written because it
220+
> records the analysis this decision was made on, but its central factual claim was **wrong**, and
221+
> executing the runbook is what proved it. The published identity regexp was lowercase
222+
> (`platformrelay/kollect`) while GitHub's OIDC SAN preserves the repository's canonical casing
223+
> (`PlatformRelay/Kollect`), and `--certificate-identity-regexp` is a **case-sensitive** Go RE2
224+
> pattern. So "every documented verification command matches the signer" was false: none of them
225+
> did, for any release, for anyone — the V1 gate below reported FAILED for this reason and not
226+
> because the copy was bad, confirmed by running the same command against the **untouched original
227+
> path**. What kept it hidden is that `.github/release-notes-install.md` builds the command from
228+
> `${GITHUB_REPOSITORY}`, which GitHub expands with canonical casing; verifying from the *release
229+
> page* worked, verifying from the *docs* never did. The published value is now
230+
> `^https://github\.com/[Pp]latform[Rr]elay/[Kk]ollect/.+` — case-tolerant on both path segments and
231+
> with the host dots escaped — proven against a real published signature, with wrong-org, wrong-repo
232+
> and wrong-issuer negative controls. The reasoning that follows is unaffected: the pattern is still
233+
> a repository prefix and still pins no workflow path.
234+
219235
That makes the fallback viable — but **only from a workflow**. A keyless re-sign has to run in
220236
GitHub Actions with `id-token: write`, because that is what mints a Fulcio certificate whose SAN is
221237
a `github.com/platformrelay/kollect/…` workflow URI. A maintainer re-signing interactively from a
@@ -266,7 +282,7 @@ crane digest ghcr.io/platformrelay/charts/kollect:0.14.0
266282
# 2. the published verify command must pass VERBATIM against the new path
267283
cosign verify \
268284
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
269-
--certificate-identity-regexp '^https://github.com/platformrelay/kollect/.+' \
285+
--certificate-identity-regexp '^https://github\.com/[Pp]latform[Rr]elay/[Kk]ollect/.+' \
270286
ghcr.io/platformrelay/charts/kollect:0.14.0
271287
```
272288

docs/security/security-architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ helm template kollect ./charts/kollect > /tmp/kollect-rendered.yaml
309309
# Release identity (replace the example tag/digest)
310310
cosign verify \
311311
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
312-
--certificate-identity-regexp '^https://github.com/platformrelay/kollect/.+' \
312+
--certificate-identity-regexp '^https://github\.com/[Pp]latform[Rr]elay/[Kk]ollect/.+' \
313313
ghcr.io/platformrelay/kollect@sha256:REPLACE_ME
314314
```
315315

hack/migrate-chart-path.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ DST_PACKAGE_NAME_ENC="charts%2Fkollect"
5959
# Used VERBATIM. A looser regexp here would certify signatures that a user following the
6060
# published command would reject, which is the opposite of what this gate is for.
6161
OIDC_ISSUER="https://token.actions.githubusercontent.com"
62-
IDENTITY_REGEXP='^https://github.com/platformrelay/kollect/.+'
62+
IDENTITY_REGEXP='^https://github\.com/[Pp]latform[Rr]elay/[Kk]ollect/.+'
6363

6464
# The V1 gate chart. One chart is copied and fully verified before any other is touched,
6565
# so that a signature-portability failure costs one copy rather than six.

hack/test/dist_chart_path_migration_test.sh

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ pass "phases are called in order (0 -> 1 -> 2 -> 3 -> 4 -> handoff) and the bulk
299299
# would certify chart signatures as good and the hub's readers would still see them fail.
300300
# The expected value is cross-checked against docs/RELEASE.md so the two cannot drift
301301
# apart silently in either direction.
302-
IDENTITY_REGEXP='^https://github.com/platformrelay/kollect/.+'
302+
IDENTITY_REGEXP='^https://github\.com/[Pp]latform[Rr]elay/[Kk]ollect/.+'
303303
OIDC_ISSUER='https://token.actions.githubusercontent.com'
304304

305305
grep -Fq -- "--certificate-identity-regexp '${IDENTITY_REGEXP}'" "${SCRIPT}" ||
@@ -758,6 +758,12 @@ run_migrate() {
758758
set -e
759759
}
760760
logged() { grep -Eq "$2" "$1/log"; }
761+
# Fixed-string twin. Use this whenever the needle is a VALUE the gate already pins
762+
# (an identity regexp, an issuer URL): the value is full of ERE metacharacters, so a
763+
# hand-escaped copy of it is a second source of truth that drifts silently. SEC-VERIFYCASE-01
764+
# is exactly that failure -- a hardcoded copy here went on asserting the lowercase identity
765+
# that cosign could never match, and agreed with itself while the real command was broken.
766+
logged_f() { grep -Fq "$2" "$1/log"; }
761767

762768
# --- 8a. A bare invocation must not write. ---------------------------------
763769
# Kills three mutations at once: APPLY defaulting to 1; run_mutating executing regardless
@@ -803,7 +809,7 @@ for v in 0.14.0 0.15.0 0.16.0 0.17.0 0.18.0 0.19.0; do
803809
fail "--apply copied ${v} without running 'cosign verify' against ${DST}:${v}. Recorded verifies:"$'\n'"$(grep -E '^cosign verify' "${scene}/log" || echo '(none)')"
804810
done
805811
# ...and with the published flag shape, on the real call rather than in the source text.
806-
logged "${scene}" '^cosign verify --certificate-oidc-issuer https://token\.actions\.githubusercontent\.com --certificate-identity-regexp \^https://github\.com/platformrelay/kollect/\.\+ ' ||
812+
logged_f "${scene}" "cosign verify --certificate-oidc-issuer ${OIDC_ISSUER} --certificate-identity-regexp ${IDENTITY_REGEXP} " ||
807813
fail "the recorded 'cosign verify' does not carry the published issuer/identity flags. Recorded:"$'\n'"$(grep -E '^cosign verify' "${scene}/log" | head -1)"
808814

809815
# THE transposition assertion. `cosign copy DST SRC` would push the chart-only path back

0 commit comments

Comments
 (0)