From e0bfb91e73c769ce8207ddab988a01be1435a944 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Thu, 3 Sep 2026 17:26:17 -0500 Subject: [PATCH 1/4] docs(backlog,adr): record #332's shipped mechanism, raise the sigstore version discrepancy (BACKLOG #332) Docs only. No build change. The #332 row said "not started". The mechanism shipped in a9354808e (2026-08-29). Verified at 46ea10a7 on a clean tree, reading the working tree rather than piping git show: group at pyproject.toml:294, tracked lock (193 hashes), export + diff gate at security.yml:107-108, resync at dependabot-lock-resync.yml:140,152,156, --require-hashes install at release.yml:391, guard re-pointed at tests/test_ci_venv_pinning.py:204 and :994. Steps 2, 3 and 5 are done. Raised, NOT resolved: the row records an owner ruling of 2026-08-22 pinning sigstore==4.4.0, "NOT 4.5.0"; the shipped code pins 4.5.0 (pyproject.toml:295) and release.yml:385-390 argues the cooldown objection is spent. Both citations are live and they disagree. It needs an owner ruling, so both are recorded with the timeline and left alone. Also recorded: step 4's contamination risk did not materialise (measured with the CI-pinned uv==0.12.0, control-first); step 6 not started; and one new residual -- nothing pins release-tools.lock INTO the DEP-1 set, since the lockstep test is satisfied by set equality. ADR 0034's sigstore residual said the lock omits sigstore. That is now false, so the row is marked superseded rather than deleted. Co-Authored-By: Claude Opus 5 --- docs/BACKLOG.md | 34 +++++++++++++++++-- ...is-triage-policy-accepted-risk-register.md | 2 +- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index a19e6b606..fe1a9035b 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -3362,18 +3362,42 @@ This is **wider than the disclosure describes.** [`CONFIGURATION.md:718`](CONFIG ## 332. Release signing toolchain is unhashed +> ๐Ÿšง **Status 2026-09-03 โ€” THE MECHANISM IS BUILT AND ON `main`. The "not started" line below is stale; read this banner first.** Landed in commit `a9354808e` (2026-08-29), *"the clean half of PR 531"*, which cited this item. **Verified independently at `46ea10a7` (= `origin/main`) on a clean tree, reading the working tree rather than piping `git show`:** the PEP 735 group is at `pyproject.toml:294-296`; `ci/locks/release-tools.lock` is **tracked** (`git ls-tree HEAD ci/locks/` lists three locks, not two) and carries 193 `--hash=` lines; the seventh export and its `git diff --exit-code` gate are at `security.yml:107-108`; the resync's export, `--quiet` short-circuit and `git add` are at `dependabot-lock-resync.yml:140,152,156`; and `release.yml:391` installs it with `python -m pip install --require-hashes -r ci/locks/release-tools.lock`. The guard was re-pointed, not deleted, exactly as this item's step 5 required: `sigstore` left `RELEASE_PINNED_TOOLS` (`tests/test_ci_venv_pinning.py:204`) with a comment saying it MOVED, and `test_the_release_signing_toolchain_is_installed_from_a_hashed_lock` (`:994`) replaced it with three stronger assertions. **So steps 2, 3 and 5 are done.** +> +> **ONE OPEN QUESTION, AND IT NEEDS AN OWNER RULING โ€” do not resolve it by editing either side.** This row records **OWNER RULING 2026-08-22: pin `sigstore==4.4.0` โ€ฆ NOT 4.5.0**. The shipped code pins **`sigstore==4.5.0`** (`pyproject.toml:295`, `ci/locks/release-tools.lock:238`) and `release.yml:385-390` argues the case for it: the 5-day `dependabot.yml` cooldown that motivated the original 4.4.0 choice closed 2026-08-02, so *"the objection is SPENT, not overridden."* **Both citations are live and they disagree.** Two facts a ruling should have, neither of which decides it: (1) the landing commit is dated **2026-08-29, seven days AFTER the ruling**, and the ruling itself was made **twenty days after** the cooldown closed โ€” so a spent cooldown is not new information relative to the ruling; (2) **the revert is measurably cheap.** Measured 2026-09-03 with the CI-pinned `uv==0.12.0`, control-first (re-export of the unchanged tree gave DIFFS=0, so the instrument matches CI's): setting the group to `sigstore==4.4.0` moves `uv lock` by one line (`sigstore v4.5.0 -> v4.4.0`), leaves **all six** pre-existing DEP-1 artifacts byte-identical, and moves `release-tools.lock` by three lines, because 4.4.0 and 4.5.0 share a transitive closure. **The step-4 contamination risk did not materialise in either direction, so the `semgrep`-style excluded-by-decision call is not reached and is not the reason this is open.** The question is purely which version the owner wants. +> > ๐Ÿ”ข **Re-scored 2026-08-20 -> P2.** Value **6/10** ยท Difficulty **5/10** ยท _quick win_. Build-time only with no engine, store or PHI surface and no pull-request reachability, which holds value below the defect band; what keeps it at 6 is that code executing at :255 holds the OIDC identity that signs at :258, so a compromise yields a backdoored wheel carrying valid Sigstore and SLSA evidence. Difficulty 5 is the proven ADR 0034 mechanism repeated as a seventh lock across a six-place lockstep, with a real chance the re-resolve contaminates the existing locks and forces the excluded-by-decision call semgrep got. _(was 6/10 ยท 5/10.)_ > -> **Filed 2026-08-01 โ€” not started.** Arbitrary code from any of ~30 floating transitives at `.github/workflows/release.yml:255` runs with the OIDC identity that then signs the wheel, writes the SLSA attestation and publishes to PyPI โ€” a backdoored artifact carrying a *valid* Sigstore bundle and valid provenance โ€” and no Dependabot ecosystem parses an inline `pip install X==Y`, so the pin rots with no trigger and no owner (the two siblings at `:104` and `:207`, the latter a `~=` range, float identically); the ADR 0034 hashed-lock mechanism is proven and running for `ci-scanners`/`ci-quality`, but `sigstore` is absent from every lock (`grep -c sigstore uv.lock` โ†’ 0), adding a seventh is a six-place lockstep edit, the resolve contamination may force the same excluded-by-decision call semgrep got, and no PR leg ever executes this path. +> **Filed 2026-08-01 โ€” SUPERSEDED, see the status banner above; the fix has since shipped and the line numbers in this paragraph are pre-fix.** Arbitrary code from any of ~30 floating transitives at `.github/workflows/release.yml:255` runs with the OIDC identity that then signs the wheel, writes the SLSA attestation and publishes to PyPI โ€” a backdoored artifact carrying a *valid* Sigstore bundle and valid provenance โ€” and no Dependabot ecosystem parses an inline `pip install X==Y`, so the pin rots with no trigger and no owner (the two siblings at `:104` and `:207`, the latter a `~=` range, float identically); the ADR 0034 hashed-lock mechanism is proven and running for `ci-scanners`/`ci-quality`, but `sigstore` is absent from every lock (`grep -c sigstore uv.lock` โ†’ 0), adding a seventh is a six-place lockstep edit, the resolve contamination may force the same excluded-by-decision call semgrep got, and no PR leg ever executes this path. > Verdict: build > Closing-act: code > **OWNER RULING 2026-08-22: pin `sigstore==4.4.0` and hash-lock it. NOT 4.5.0.** > *The ruling was made today and lived only in the owner queue record; this row is the surface that > governs `release.yml:255`, so it belongs here.* +> +> **WHY THIS ITEM IS STILL OPEN, in one place:** the version question above, and **step 6** (`build` and +> `cyclonedx-bom` into the same group), which was never started. Step 6's constraint is unchanged and +> load-bearing: `cyclonedx-bom` is half of a byte-identical pair โ€” `release.yml:326` and +> `security.yml:367` both read `python -m pip install "pip==26.1.2" "cyclonedx-bom~=7.3.1"`, and +> `test_sbom_install_is_byte_identical_in_release_and_security` (`tests/test_ci_venv_pinning.py:943`) +> requires them to stay identical, so **both halves must move in one commit or that test reds**. +> `build` remains inline at `release.yml:104`, `:521` and `:647`. **One smaller residual found while +> verifying:** nothing pins `ci/locks/release-tools.lock` *into* the DEP-1 set. +> `tests/test_dep1_lock_resync_lockstep.py` derives the export set dynamically and compares the gate to +> the resync, so it is satisfied by set **equality** โ€” drop the lock from both workflows and every test +> there stays green while the lock silently stops being re-derived, diffed and re-synced. +> `test_constraints_lock_is_in_the_set` exists for exactly that class after the #1193 incident; the +> signing lock has no equivalent pin, and `test_the_release_signing_toolchain_is_installed_from_a_hashed_lock` +> does not cover it (a lock unrefreshed for a year still exists, still pins, still hashes, still +> installs). That is the *"hash-pinned toolchain rotting into a pinned-but-unpatched one"* posture ADR +> 0034 calls worse than floating. **Cluster:** Security / Supply chain. **Priority:** P2. **Verdict:** build. **Severity:** medium. -**What:** `.github/workflows/release.yml:253-255` says so in its own comment and then does it: +**What** *(this section describes the PRE-FIX state and is kept as the filing record; every negative +claim in it โ€” no lock, two lock files, two groups, no `release-tools` group โ€” was made false by +`a9354808e`. See the status banner for what is on `main` now.)*: `.github/workflows/release.yml:253-255` +says so in its own comment and then does it: ``` # NOTE: this pins the TOP only; sigstore's ~30 transitive deps still float at signing time. @@ -3406,7 +3430,11 @@ What makes it worth fixing anyway is the **blast radius if it lands**: arbitrary Honestly bounded: **this is build-time only.** No PHI path, no running-engine surface, no operator-reachable behaviour. It does not touch the store, the API, or any connector. And per ADR 0034 ยง3 (`:162-170`) the fix moves the OPEN Scorecard count only for the lines it actually converts to `--require-hashes`; the two genuinely-open `PinnedDependenciesID` alerts named at `:336-341` are the SBOM scratch-venv pair, not this one. -**Proposed:** repeat the mechanism ADR 0034's 2026-07-29 amendment already proved, and handle the version question the residual row raises rather than stepping over it. +**Proposed** *(step status as of 2026-09-03: **2, 3, 5 DONE** in `a9354808e`; **4 measured, no +contamination, both ways**; **1 OPEN โ€” the owner ruling and the shipped pin disagree**; **6 NOT +STARTED**. Step 1's own warning โ€” "this is a recorded owner decision; do not invert it silently" โ€” is +the one that was not honoured, so it is restated in the banner rather than left here to be re-read +after the fact.)*: repeat the mechanism ADR 0034's 2026-07-29 amendment already proved, and handle the version question the residual row raises rather than stepping over it. 1. **Resolve the 4.4.0-vs-4.5.0 decision first, explicitly.** ADR 0034:350 keeps `sigstore` out of the lock *because* routing it through would resolve **4.5.0**, which was `<48 h` old against `.github/dependabot.yml`'s `cooldown: default-days: 5` (:26-30). That was written 2026-07-29. By the ADR's own arithmetic the window closes around 2026-08-01/02 โ€” i.e. now โ€” but **confirm the actual PyPI publish date before acting**; this item does not verify it, and the whole rationale hangs on it. If the window has closed, the objection is spent and the residual row should be amended, not quietly contradicted. This is a recorded owner decision; do not invert it silently. 2. **Add a PEP 735 `release-tools` group** to `pyproject.toml` alongside `ci-scanners`/`ci-quality`, non-default (ADR 0034:283-287, decision 2 โ€” an extra becomes a real install target; a default group lands in the release SBOM and in what `pip-audit` audits as runtime). diff --git a/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md b/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md index bf2aab564..545a86606 100644 --- a/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md +++ b/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md @@ -355,7 +355,7 @@ implied: | Residual | Why it is not fixed here | |---|---| -| **`release.yml`'s `sigstore==4.4.0`** | **Not a gap โ€” an owner decision this change must not invert.** Routing `sigstore` through the lock **would** resolve it to **4.5.0** โ€” stated counterfactually because that is what it is: `sigstore` is deliberately **absent** from `uv.lock` and from all six exports (0 hits), and the 4.5.0 figure was measured in a scratch resolve. A reader who greps `uv.lock`, finds nothing, and concludes this row is stale would re-open the very decision it exists to preserve. ADR 0034 pins 4.4.0 because `.github/dependabot.yml` sets `cooldown.default-days: 5` and 4.5.0 was <48 h old. Routing `sigstore` through the lock would pin the *signing* toolchain fresher than the repo's own update policy allows โ€” inverting that policy at the highest-privilege point in the build (the job holding `contents`/`id-token`/`attestations: write`). Left exactly as it is. Re-evaluate when 4.5.0 ages out. | +| **`release.yml`'s `sigstore==4.4.0`** | **SUPERSEDED BY THE SHIPPED CODE โ€” do not act on this row; its factual premise is false as written. Row kept rather than deleted because a reader arrives here by grep, and a missing row reads as "never considered".** It said `sigstore` was deliberately **absent** from `uv.lock` and from all six exports (0 hits). **Verified false 2026-09-03 at `46ea10a7`:** `sigstore` is in `uv.lock`, `ci/locks/release-tools.lock` is a tracked seventh export, and `release.yml:391` installs it with `--require-hashes`. The mechanism half of this row is spent. **The version half is now an OPEN QUESTION and is NOT resolved here:** this row and BACKLOG #332 record an **owner ruling of 2026-08-22 pinning `sigstore==4.4.0`, "NOT 4.5.0"**, while the shipped `pyproject.toml:295` pins **4.5.0** and `release.yml:385-390` argues the cooldown objection is spent. Both citations are live and they disagree. **It needs an owner ruling; do not resolve it by editing either side.** See BACKLOG #332. | | **The `uv` bootstrap** (`security.yml`, `python -m pip install --upgrade pip "uv==0.12.0"`) | **Permanently circular: you cannot hash-lock `uv` with `uv`.** That install produces every lock this repo commits. `uv` stays an inline `==` pin, and `pip` remains the sole registered *name* in `SECURITY_YML_ACCEPTED_UNPINNED`. Note it is also the pip that runs the **six exports and the diff gate** โ€” the `--require-hashes` install two steps later *downgrades* pip to the locked version afterwards, so the DEP-1 step's own posture is unchanged by this work. *Cheap out-of-band fix that removes it entirely:* `astral-sh/setup-uv@c771a70eโ€ฆ` is already SHA-pinned and used in 9 places (`ci.yml` ร—6, `quality-advisory.yml` ร—2, the resync ร—1); swapping it in deletes the install. Separate change. | | **`security.yml`'s unpinned `pip` in the `semgrep` step** โ€” `python -m pip install --upgrade pip "semgrep==1.172.0"` | **The SECOND surviving bootstrap, named because an undercounted inventory is how a real finding goes invisible.** The semgrep row below explains only the `[otel]` conflict that keeps *semgrep* inline; this row records that the same line is also an **unpinned `pip` fetch**. So two `--upgrade pip` bootstraps remain in the file, not one โ€” now asserted as an exact count by `test_security_yml_pip_bootstrap_count_is_exact`, since `SECURITY_YML_ACCEPTED_UNPINNED` registers the *name* `pip` and cannot tell two accepted bootstraps from twenty. **Mitigation WITHDRAWN 2026-08-04 (BACKLOG #334) โ€” it rested on a false premise.** This row previously read *"Mitigating: `semgrep` is not a required context (`tests/test_required_contexts.py`), so this one does not sit on the merge path."* That is false in the repo's own records: `semgrep (project SAST rules)` is at `.github/required-contexts.txt:78`, and `tests/test_security_posture.py`'s `_BLOCKING_SECURITY_JOBS` names `semgrep` and asserts that membership. `tests/test_required_contexts.py` never claimed the opposite โ€” it pins the required *set*, which contains it; the citation was to a file that says the reverse of what it was cited for. So this bootstrap **does** sit on the merge path, and #334 widened that same step's scan from a two-directory allow-list to the whole repo, which *increases* what rides on it. Re-accepted with that known, on the `uv` row's grounds (a bootstrap `pip` cannot hash-lock itself). It disappears whenever the semgrep row's `[tool.uv] conflicts` recipe is taken. | | **`quality-advisory.yml`'s `pipx install ruff`** | **Outside the guard's regex and outside Scorecard's.** `test_ci_venv_pinning.py`'s `_PIP_INSTALL` matches `pip`/`pip3`/`python -m pip` only, so the unpinned fallback branch is invisible to every existing guard โ€” and because it is not a `pip install`, **no alert exists to close**. `pipx` has no `--require-hashes`, so fixing it means changing the install mechanism, not the pin. Recorded, not done. | From 127587adf6c02981951255e1fb7557e0d83ec1fa Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Thu, 3 Sep 2026 17:42:53 -0500 Subject: [PATCH 2/4] fix(release): hold the signing toolchain at sigstore 4.4.0, per owner ruling (BACKLOG #332) Owner ruling 2026-08-22, re-affirmed 2026-09-03 with the full history in hand: pin sigstore==4.4.0, NOT 4.5.0. a9354808e (2026-08-29) landed the hashed-lock mechanism but authored the pin as 4.5.0, reasoning that dependabot.yml's 5-day cooldown had expired. That argument was published on PR 531 at 2026-08-26T23:46:18Z and RETRACTED BY ITS OWN AUTHOR at 2026-08-27T02:45:36Z, two days before it shipped. The retraction's own words: "I checked whether the ADR's premise had expired. I never checked whether the question had since been answered." The cooldown never reached the ruling in either direction. It expired 2026-08-02; the first ruling came twenty days later, and the 2026-09-03 re-affirmation had the expiry in the packet. Neither version has a patch line, so patch availability does not discriminate. Measured against PyPI 2026-09-03: 4.x is 4.0.0, 4.1.0, 4.2.0, 4.3.0, 4.4.0, 4.5.0 and nothing else. The 3.x series did ship patches, so a future 4.4.1 is possible. Re-export measured with the CI-pinned uv==0.12.0, control first (the unchanged tree re-exported byte-identically, so the instrument matches CI's): uv.lock moves one line, release-tools.lock three, and all six pre-existing DEP-1 artifacts stay byte-identical. No contamination. Co-Authored-By: Claude Opus 5 --- ci/locks/release-tools.lock | 6 +++--- pyproject.toml | 25 +++++++++++++++++-------- uv.lock | 8 ++++---- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/ci/locks/release-tools.lock b/ci/locks/release-tools.lock index da25a39f2..41fa999b9 100644 --- a/ci/locks/release-tools.lock +++ b/ci/locks/release-tools.lock @@ -235,9 +235,9 @@ securesystemslib==1.4.0 \ --hash=sha256:a0743a3d978cf26e98a70a57e3fbd5a18e0a74c20cabe615f6a55b02ef0272b3 \ --hash=sha256:faea87be0f9c4b4277a5fa1b54bf9bfd807be9a94ab11be6c557dc8b75c43285 # via tuf -sigstore==4.5.0 \ - --hash=sha256:020d3e07f622b2916bf453e66ff6ff0711e1fdc5ab69e8bd8902f71d9fcb316f \ - --hash=sha256:f045b207f2e12605cf775ec38e89c5eda625d71ffa7830477db65e47ec2bc8b2 +sigstore==4.4.0 \ + --hash=sha256:20ffe791c1fa33ce62148c0291b46280d29c1910964d9afac419e9b1a8afc56b \ + --hash=sha256:80c36d08b02479e2a282d0bea93de68fe0d43a93b0d58e4d9ac6bb9f8425957c sigstore-models==0.0.6 \ --hash=sha256:5201a68f4d7d0f8bec1e2f4378eb646b084c52609a4e31db8c385095fff68b2e \ --hash=sha256:c766c09470c2a7e8a4a333c893f07e2001c56a3ff1757b1a246119f53169a849 diff --git a/pyproject.toml b/pyproject.toml index 0ce643456..5fb2f68ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -279,20 +279,29 @@ ci-scanners = [ # The RELEASE SIGNING toolchain (BACKLOG #332). Exact `==`, and the version is the contract for the # same reason the scanners' are: this runs in the job holding `id-token: write`, and whatever executes # there signs the wheel, writes the SLSA attestation and publishes to PyPI. -# sigstore 4.5.0, not the 4.4.0 currently pinned inline at `release.yml:255`. That pin chose the -# OLDER release deliberately -- `dependabot.yml`'s 5-day supply-chain cooldown, and 4.5.0 -# was then <48h old, so hard-pinning the SIGNING toolchain to a fresher artifact than the -# repo's own update policy allows would have inverted that policy at the highest-privilege -# point in the pipeline. MEASURED: 4.5.0 published 2026-07-28T07:34:00Z, so the cooldown -# closed 2026-08-02 -- the objection is SPENT, not overridden, and `release.yml`'s own -# comment says "Re-evaluate to 4.5.0 once it has aged past the window." +# sigstore 4.4.0, BY OWNER RULING -- given 2026-08-22 and RE-AFFIRMED 2026-09-03 after the full +# history below was put to the owner. "NOT 4.5.0." Recorded at BACKLOG #332. This line is +# the contract (see the paragraph above); do not move it as a side effect of another +# change, and do not let a bot move it -- `.github/dependabot.yml` carries a matching +# `ignore` entry whose only purpose is to keep this pin where the owner put it. +# DO NOT RE-DERIVE THE COOLDOWN ARGUMENT. It has now been derived twice, and it has never +# reached the ruling. `dependabot.yml`'s 5-day cooldown motivated the ORIGINAL 4.4.0 +# choice and expired 2026-08-02 (4.5.0 published 2026-07-28T07:34:00Z). The first ruling +# came twenty days AFTER that expiry, and the 2026-09-03 re-affirmation came with the +# expiry stated in the packet. So "the cooldown is spent" is not news to this pin; it is +# the argument both rulings were made in spite of. +# NEITHER VERSION HAS A PATCH LINE, so patch availability does not discriminate between +# them. Measured against PyPI 2026-09-03: sigstore 4.x is 4.0.0, 4.1.0, 4.2.0, 4.3.0, +# 4.4.0 (2026-07-06), 4.5.0 (2026-07-28) and nothing else -- zero patch releases across +# the whole 4.x series. (The 3.x series did ship patches, e.g. 3.5.1 and 3.6.7, so a +# future 4.4.1 is possible and the `ignore` entry is scoped NOT to block one.) # The point of the group is NOT the version. An inline `pip install X==Y` pins only the TOP package -- # ~30 transitives still float, unhashed, resolved at signing time -- and NO Dependabot ecosystem parses # an inline install inside a workflow `run:` block, so the pin has no updater, no trigger and no owner. # Routing it through `uv.lock` hashes the transitives AND puts it under the `uv` ecosystem Dependabot # already watches. Non-default, like its siblings, so it stays out of the runtime exports and the SBOM. release-tools = [ - "sigstore==4.5.0", + "sigstore==4.4.0", ] # The ADVISORY measurement tools (quality-advisory.yml). Exact where something PARSES the tool's # output, a floor where nothing does: diff --git a/uv.lock b/uv.lock index 934f26208..5577f9992 100644 --- a/uv.lock +++ b/uv.lock @@ -1220,7 +1220,7 @@ ci-scanners = [ { name = "pip-audit", specifier = "==2.10.1" }, { name = "zizmor", specifier = "==1.29.0" }, ] -release-tools = [{ name = "sigstore", specifier = "==4.5.0" }] +release-tools = [{ name = "sigstore", specifier = "==4.4.0" }] [[package]] name = "msgpack" @@ -2167,7 +2167,7 @@ wheels = [ [[package]] name = "sigstore" -version = "4.5.0" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, @@ -2185,9 +2185,9 @@ dependencies = [ { name = "sigstore-rekor-types" }, { name = "tuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/e0/279419065e2d7102413605b3456122adbbccbc42e010b499c7b882fc01f8/sigstore-4.5.0.tar.gz", hash = "sha256:020d3e07f622b2916bf453e66ff6ff0711e1fdc5ab69e8bd8902f71d9fcb316f", size = 90969, upload-time = "2026-07-28T07:34:01.717Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/a9/7f7625225c6e7041ab4460bfc5b30a6ebc40bcf6487ee28d5864149124c4/sigstore-4.4.0.tar.gz", hash = "sha256:20ffe791c1fa33ce62148c0291b46280d29c1910964d9afac419e9b1a8afc56b", size = 90986, upload-time = "2026-07-06T13:07:49.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/7f/51dc313c06dd3ec6c5b308e334492cfdea0b02c9184a0da7b2db8c2a30a1/sigstore-4.5.0-py3-none-any.whl", hash = "sha256:f045b207f2e12605cf775ec38e89c5eda625d71ffa7830477db65e47ec2bc8b2", size = 111724, upload-time = "2026-07-28T07:34:00.211Z" }, + { url = "https://files.pythonhosted.org/packages/ad/37/7922b125ede9cbee53569adb992f6f86aefab009105f3b0e77bc5225636d/sigstore-4.4.0-py3-none-any.whl", hash = "sha256:80c36d08b02479e2a282d0bea93de68fe0d43a93b0d58e4d9ac6bb9f8425957c", size = 111705, upload-time = "2026-07-06T13:07:47.946Z" }, ] [[package]] From 144abab09abd8a2797dcbb1653731cccadb2122c Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Thu, 3 Sep 2026 17:49:49 -0500 Subject: [PATCH 3/4] fix(ci): make the sigstore 4.4.0 ruling stick, and audit the lock that backs it (BACKLOG #332) Two changes that only work together. 1. dependabot.yml gains `ignore: sigstore >=4.5.0`, so a routine weekly PR cannot re-propose the version the owner twice declined. Without it the ruling survives only until the next bot PR is merged, which is how it was undone the first time. 2. security.yml's audit step gains `pip-audit -r ci/locks/release-tools.lock`. This resolves the stated conflict at dependabot.yml's "NOT IGNORED, deliberately" policy rather than stepping over it. That policy keeps the `==` pins moving so an unattended pin cannot rot into "pinned, stale, unpatched". Its reason is about versions NOBODY HAS DECIDED, so it does not reach a version under an owner ruling -- there the weekly PR does not refresh neglect, it re-litigates a decision. The carve-out is recorded at the policy, with the condition for lifting it. The STALENESS half of the policy still binds, so it is honoured rather than waived. An `ignore` also suppresses the security track for the named range, which the file already accepts for annotated-types and ruff on the grounds that pip-audit still detects a withheld fix and reds a required gate. That backstop did not exist here: a9354808e added release-tools.lock to the export and byte-diff set but not to the audit set, leaving the release signing closure the only committed lock nothing audited. Change 2 closes that. Each side's comment names the other, so removing one without the other is caught by reading either. Scoped to >=4.5.0, not to the package. sigstore 4.x has shipped zero patch releases but 3.x did, so a 4.4.1 is possible; this range leaves the 4.4.x patch track open. Also corrected: the exported-artifact count in the same file said SIX; it has been seven since a9354808e. Co-Authored-By: Claude Opus 5 --- .github/dependabot.yml | 32 +++++++++++++++++++++++++++++++- .github/workflows/security.yml | 15 +++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b22989abc..70ce8240f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,7 +7,8 @@ updates: # pyproject.toml + uv.lock and REGENERATES uv.lock IN its PRs (version updates GA 2025-03, # security updates GA 2025-12) โ€” the old "pip" ecosystem updated requirements/pyproject but NOT # uv.lock. It still does not re-derive the EXPORTED locks (requirements.lock + docker/locks/* + - # constraints.lock + ci/locks/* โ€” SIX artifacts), which the DEP-1 gate in security.yml byte-diffs; + # constraints.lock + ci/locks/* โ€” SEVEN artifacts, since a9354808e added ci/locks/release-tools.lock), + # which the DEP-1 gate in security.yml byte-diffs; # .github/workflows/dependabot-lock-resync.yml re-exports those on the Dependabot branch so the # gate stays green. # @@ -56,11 +57,40 @@ updates: # `versioning-strategy: lockfile-only`. Both would freeze the hash-pinned CI toolchain, which ADR # 0034 ยง3 wants moving THROUGH this machinery โ€” a pinned toolchain outside it rots into # "pinned, stale, unpatched โ€” worse posture than floating". + # + # ONE CARVE-OUT, and it is the `sigstore` entry below (BACKLOG #332). The rule above is about + # versions NOBODY HAS DECIDED: it keeps the toolchain moving because leaving a pin unattended is + # how it rots. `sigstore`'s version is not unattended โ€” it is an OWNER RULING, given 2026-08-22 + # and re-affirmed 2026-09-03, pinning 4.4.0 and naming 4.5.0 as the version not to take. So the + # policy's own reason does not reach it, and the failure mode inverts: a routine weekly PR here + # does not keep a neglected pin fresh, it re-proposes a version the owner has twice declined, + # and merging it silently reverses a decision. That is not hypothetical -- it already happened + # once, in a9354808e, which shipped 4.5.0 two days AFTER its own author had retracted the + # argument for it (PR 531, comment of 2026-08-27T02:45:36Z). Lift this entry when the owner + # rules differently in writing, and not before. + # + # THE STALENESS HALF OF THE RULE STILL BINDS, and is honoured by a different mechanism rather + # than waived. The trade-off note above applies unchanged (an `ignore` suppresses the SECURITY + # track for the named range too), so this entry is only acceptable while something else can see + # an advisory in the pinned closure. Until this commit NOTHING could: security.yml's audit step + # covered `requirements.lock`, `ci-scanners.lock` and `ci-quality.lock` but NOT + # `ci/locks/release-tools.lock`, which a9354808e added to the export and byte-diff set without + # adding to the audit set. That gap is closed in the same commit as this entry, so the + # annotated-types / ruff bargain โ€” detection untouched, a withheld fix reds a required gate + # within ~24h, a human lifts the entry โ€” now holds here too. If that audit line is ever removed, + # REMOVE THIS ENTRY WITH IT. + # + # SCOPED TO `>=4.5.0`, NOT TO THE PACKAGE. sigstore 4.x has shipped zero patch releases (4.0.0, + # 4.1.0, 4.2.0, 4.3.0, 4.4.0, 4.5.0; measured against PyPI 2026-09-03), but the 3.x series did + # ship them (3.5.1, 3.6.7), so a 4.4.1 is possible. This range leaves the 4.4.x patch track open + # and blocks only the minor bump the ruling declined. ignore: - dependency-name: "annotated-types" versions: [">=0.8.0"] - dependency-name: "ruff" versions: [">=0.16.0"] + - dependency-name: "sigstore" + versions: [">=4.5.0"] groups: # Version-update grouping (applies-to defaults to version-updates). python-deps: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 6144c9335..d2bec17ae 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -143,6 +143,21 @@ jobs: # the advisory half not block, move the ci-quality line alone to its own step. pip-audit -r ci/locks/ci-scanners.lock --desc pip-audit -r ci/locks/ci-quality.lock --desc + # THE SIGNING TOOLCHAIN, and it is the one that most needs this (BACKLOG #332). a9354808e + # added ci/locks/release-tools.lock to the export set and the byte-diff gate above but NOT + # here, so from 2026-08-29 until this line landed the release signing closure was the only + # committed lock nothing audited -- hash-pinned, therefore sticky, therefore exactly the + # "pinned, stale, unpatched is worse than floating" case the two lines above exist to stop. + # It is also load-bearing for .github/dependabot.yml's `sigstore` ignore entry: that entry + # suppresses the security track for >=4.5.0, and is only acceptable while this audit can + # still see an advisory in the pinned closure. REMOVE THIS LINE AND YOU MUST REMOVE THAT + # ENTRY -- the comment there says so from the other side. + # + # Same REQUIRED-context consequence as the two above, and a better-earned one: a CVE in + # sigstore's closure reds the merge gate. That closure signs the wheel, the sdist, the SBOM + # and the VEX with the job's OIDC identity, so blocking is the wanted behaviour, not a cost + # reluctantly accepted for an advisory tool. `--ignore-vuln ` remains the escape hatch. + pip-audit -r ci/locks/release-tools.lock --desc # ANTI-SLOPSQUAT. Deliberately a STEP in this already-REQUIRED job rather than a new context: it # blocks today with no branch-protection change, the same reasoning as the ledger-gate backstop # in ci.yml. (A hard-failing job that is NOT a required context does not stop auto-merge โ€” it From 3c3f778ae5b42c8977811f1a5e7809cc3b8233a6 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Thu, 3 Sep 2026 18:01:11 -0500 Subject: [PATCH 4/4] docs(backlog,adr,release): correct every record that argued for sigstore 4.5.0 (BACKLOG #332) The pin moved back to 4.4.0 in the two commits before this one. This commit fixes the records that still argue the other way, so a reader does not find the case for 4.5.0 sitting unmarked beside the 4.4.0 pin. release.yml's "ON THE VERSION" comment restated the cooldown argument. It now records the ruling, names the retraction, and says plainly not to re-derive the argument. ADR 0034 had two false assertions, both that sigstore is absent from every lock: the section 3 register row at :206 and the residuals row at :358, read at 46ea10a7. Both are marked in place rather than deleted, per that table's own rule. A 2026-09-03 amendment records the sequence, the discharged re-evaluation clause, and the pip-audit gap this work closed. The backlog row cited :350 for that residual in four places. :350 is a blank line and was wrong at every commit; the citations now name the row and give the line with the commit they were read at. The #332 row records the ruling, the retraction timeline, the packet measurement showing the ruling never rested on the cooldown, the PyPI measurement showing neither version has a patch line, and the audit gap. It stays OPEN: step 6 is now the only thing left in it. Co-Authored-By: Claude Opus 5 --- .github/workflows/release.yml | 24 ++++--- docs/BACKLOG.md | 33 ++++++--- ...is-triage-policy-accepted-risk-register.md | 68 ++++++++++++++++++- 3 files changed, 102 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6d1bebda7..11e232781 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -375,19 +375,23 @@ jobs: # resolved at signing time. # 2. NO Dependabot ecosystem parses an inline `pip install X==Y` inside a workflow `run:` # block (.github/dependabot.yml registers uv, github-actions, npm). So the pin had no - # updater, no trigger and no owner โ€” its own comment said "re-evaluate once it has aged - # past the window" and nothing would ever have prompted that. It was 19 days overdue when - # this landed. tests/test_ci_venv_pinning.py words the class: "a stale pin rots invisibly - # and a DELETED pin is invisible twice over." + # updater, no trigger and no owner: its own comment said "re-evaluate once it has aged + # past the window" and nothing would ever have prompted that. + # tests/test_ci_venv_pinning.py words the class: "a stale pin rots invisibly and a + # DELETED pin is invisible twice over." # Routing it through `uv.lock` fixes both: the transitives are hash-pinned, and the group is # under the `uv` ecosystem Dependabot already watches. # - # ON THE VERSION: the lock resolves sigstore 4.5.0, which the old inline pin deliberately - # avoided. That choice was CORRECT when made โ€” dependabot.yml sets a 5-day supply-chain - # cooldown and 4.5.0 was then <48h old, so pinning the SIGNING toolchain to a fresher artifact - # than the repo's own update policy allows would have inverted that policy at its - # highest-privilege point. MEASURED: 4.5.0 published 2026-07-28T07:34:00Z, so the window - # closed 2026-08-02. The objection is SPENT, not overridden. + # ON THE VERSION: the lock pins sigstore 4.4.0 BY OWNER RULING -- given 2026-08-22, and + # re-affirmed 2026-09-03 with the full history in hand. "NOT 4.5.0." The rationale lives + # ONCE, at the `release-tools` group in pyproject.toml; BACKLOG #332 carries the record. + # The one thing worth repeating HERE, because this is where the thought occurs to a reader + # standing at the install: DO NOT RE-DERIVE THE COOLDOWN ARGUMENT. dependabot.yml's 5-day + # window closed 2026-08-02 and BOTH rulings postdate it, so "the objection is spent" is not + # news to this pin -- it is what the rulings were made in spite of. That argument has been + # published and RETRACTED BY ITS OWN AUTHOR once already (PR 531, 2026-08-27), and shipped + # anyway two days later in a9354808e before being corrected back. dependabot.yml now + # carries a matching `ignore` entry so a bot cannot repeat it. python -m pip install --require-hashes -r ci/locks/release-tools.lock # Sign the wheel + sdist AND the SBOM + VEX, so an operator can verify the provenance of the # bill-of-materials and the exploitability assessment too โ€” not just the code artifacts (ADR 0149). diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index fe1a9035b..6d763970f 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -3364,7 +3364,17 @@ This is **wider than the disclosure describes.** [`CONFIGURATION.md:718`](CONFIG > ๐Ÿšง **Status 2026-09-03 โ€” THE MECHANISM IS BUILT AND ON `main`. The "not started" line below is stale; read this banner first.** Landed in commit `a9354808e` (2026-08-29), *"the clean half of PR 531"*, which cited this item. **Verified independently at `46ea10a7` (= `origin/main`) on a clean tree, reading the working tree rather than piping `git show`:** the PEP 735 group is at `pyproject.toml:294-296`; `ci/locks/release-tools.lock` is **tracked** (`git ls-tree HEAD ci/locks/` lists three locks, not two) and carries 193 `--hash=` lines; the seventh export and its `git diff --exit-code` gate are at `security.yml:107-108`; the resync's export, `--quiet` short-circuit and `git add` are at `dependabot-lock-resync.yml:140,152,156`; and `release.yml:391` installs it with `python -m pip install --require-hashes -r ci/locks/release-tools.lock`. The guard was re-pointed, not deleted, exactly as this item's step 5 required: `sigstore` left `RELEASE_PINNED_TOOLS` (`tests/test_ci_venv_pinning.py:204`) with a comment saying it MOVED, and `test_the_release_signing_toolchain_is_installed_from_a_hashed_lock` (`:994`) replaced it with three stronger assertions. **So steps 2, 3 and 5 are done.** > -> **ONE OPEN QUESTION, AND IT NEEDS AN OWNER RULING โ€” do not resolve it by editing either side.** This row records **OWNER RULING 2026-08-22: pin `sigstore==4.4.0` โ€ฆ NOT 4.5.0**. The shipped code pins **`sigstore==4.5.0`** (`pyproject.toml:295`, `ci/locks/release-tools.lock:238`) and `release.yml:385-390` argues the case for it: the 5-day `dependabot.yml` cooldown that motivated the original 4.4.0 choice closed 2026-08-02, so *"the objection is SPENT, not overridden."* **Both citations are live and they disagree.** Two facts a ruling should have, neither of which decides it: (1) the landing commit is dated **2026-08-29, seven days AFTER the ruling**, and the ruling itself was made **twenty days after** the cooldown closed โ€” so a spent cooldown is not new information relative to the ruling; (2) **the revert is measurably cheap.** Measured 2026-09-03 with the CI-pinned `uv==0.12.0`, control-first (re-export of the unchanged tree gave DIFFS=0, so the instrument matches CI's): setting the group to `sigstore==4.4.0` moves `uv lock` by one line (`sigstore v4.5.0 -> v4.4.0`), leaves **all six** pre-existing DEP-1 artifacts byte-identical, and moves `release-tools.lock` by three lines, because 4.4.0 and 4.5.0 share a transitive closure. **The step-4 contamination risk did not materialise in either direction, so the `semgrep`-style excluded-by-decision call is not reached and is not the reason this is open.** The question is purely which version the owner wants. +> **THE VERSION IS SETTLED: `sigstore==4.4.0`. OWNER RULING 2026-08-22, RE-AFFIRMED 2026-09-03** once the full history below was put to the owner, together with the instruction to make it stick. **This PR corrects the shipped pin from 4.5.0 back to 4.4.0** (`pyproject.toml`, `uv.lock`, `ci/locks/release-tools.lock`) and adds `ignore: sigstore >=4.5.0` to `.github/dependabot.yml` so a routine bot PR cannot undo it. **Do not re-open the version, and above all do not re-derive the cooldown argument.** +> +> **WHY THAT WARNING IS IN CAPITALS โ€” this was inverted once, by its own author, after he had withdrawn it.** The 4.5.0 case was published on PR 531 at **2026-08-26T23:46:18Z**, arguing that ADR 0034's *"re-evaluate once it ages out"* clause had come due. **He retracted it three hours later, at 2026-08-27T02:45:36Z**, in these words: *"I checked whether the ADR's premise had expired. I never checked whether the question had since been answered."* Commit `a9354808e` then shipped 4.5.0 on **2026-08-29, two days after the retraction**. So `release.yml`'s *"the objection is SPENT"* comment was never merely written against pre-ruling `main` โ€” **it restated a position its author had already withdrawn.** +> +> **The cooldown never reached the ruling, in either direction, which is what makes the ruling robust rather than lucky.** The 5-day window closed 2026-08-02; the first ruling came **twenty days later**, and the re-affirmation came with the expiry stated in the packet. Verified in the owner packet itself (the `#332` section of the liaison queue handoff, at commit `f3d3d1be6`; the packet is a private handoff record, so it is cited by commit rather than reproduced): **zero** occurrences of *cooldown*, *2026-08-02*, *5-day*, *ages out* or *publish date*, against controls that fired in the same 30-line window (`sigstore` 3, `4.4.0` 3, `4.5.0` 2). The axis actually put to the owner was that a group entry spelled `sigstore==4.4.0` hash-locks the recorded version **without inverting anything** โ€” so killing the cooldown premise does not touch the decision. +> +> **`4.4.0 has no patch line` was the strongest argument for overriding, and it is not a differentiator.** Measured against PyPI 2026-09-03: sigstore 4.x is 4.0.0, 4.1.0, 4.2.0, 4.3.0, 4.4.0 (2026-07-06), 4.5.0 (2026-07-28) and nothing else โ€” **zero patch releases across the whole series**, so 4.5.0 has no patch path either. The 3.x series did ship patches (3.5.1, 3.6.7), so a future 4.4.1 is possible; the `ignore` is scoped `>=4.5.0` to leave that track open. **Do not record that 4.4.0 is uniquely unpatchable.** +> +> **Contamination: none, measured twice.** With the CI-pinned `uv==0.12.0`, control first (the unchanged tree re-exported byte-identically, so the instrument matches CI's): `uv lock` moves one line, `release-tools.lock` three, and **all six** pre-existing DEP-1 artifacts stay byte-identical, because 4.4.0 and 4.5.0 share a transitive closure. **The step-4 `semgrep`-style excluded-by-decision call was never reached.** +> +> **A GAP FOUND WHILE DOING THIS, AND CLOSED HERE.** `a9354808e` added `ci/locks/release-tools.lock` to the export set and the byte-diff gate but **not** to `security.yml`'s audit step โ€” which ran `pip-audit` over `requirements.lock`, `ci-scanners.lock` and `ci-quality.lock` only. So from 2026-08-29 the release signing closure was **the one committed lock nothing audited**: hash-pinned, therefore sticky, therefore precisely the *"pinned, stale, unpatched is worse than floating"* posture ADR 0034 names. `pip-audit -r ci/locks/release-tools.lock --desc` now runs beside the other two. It is also **load-bearing for the `ignore` entry**, since an `ignore` suppresses the security track for its range; each side's comment names the other, so removing one without the other is caught by reading either. > > ๐Ÿ”ข **Re-scored 2026-08-20 -> P2.** Value **6/10** ยท Difficulty **5/10** ยท _quick win_. Build-time only with no engine, store or PHI surface and no pull-request reachability, which holds value below the defect band; what keeps it at 6 is that code executing at :255 holds the OIDC identity that signs at :258, so a compromise yields a backdoored wheel carrying valid Sigstore and SLSA evidence. Difficulty 5 is the proven ADR 0034 mechanism repeated as a seventh lock across a six-place lockstep, with a real chance the re-resolve contaminates the existing locks and forces the excluded-by-decision call semgrep got. _(was 6/10 ยท 5/10.)_ > @@ -3375,7 +3385,7 @@ This is **wider than the disclosure describes.** [`CONFIGURATION.md:718`](CONFIG > *The ruling was made today and lived only in the owner queue record; this row is the surface that > governs `release.yml:255`, so it belongs here.* > -> **WHY THIS ITEM IS STILL OPEN, in one place:** the version question above, and **step 6** (`build` and +> **WHY THIS ITEM IS STILL OPEN, in one place:** **step 6 only** โ€” the version is settled and steps 1-5 are done. Step 6 is (`build` and > `cyclonedx-bom` into the same group), which was never started. Step 6's constraint is unchanged and > load-bearing: `cyclonedx-bom` is half of a byte-identical pair โ€” `release.yml:326` and > `security.yml:367` both read `python -m pip install "pip==26.1.2" "cyclonedx-bom~=7.3.1"`, and @@ -3419,10 +3429,10 @@ The next command (`:258-259`) signs the wheel, sdist, SBOM and VEX; the SLSA att **Two things the finding as originally written understates, both verified here:** -1. **The top pin has no updater.** `.github/dependabot.yml` registers three ecosystems โ€” `uv` (:17), `github-actions` (:42), `npm` (:61). None parses an inline `pip install X==Y` inside a workflow `run:` block. `tests/test_ci_venv_pinning.py:26-32` already records the consequence: *"a stale pin rots invisibly and a DELETED pin is invisible twice over."* So ADR 0034's *"Re-evaluate when 4.5.0 ages out"* (`:350`) has no trigger and no owner โ€” it is a manual action nothing will ever prompt. +1. **The top pin has no updater.** `.github/dependabot.yml` registers three ecosystems โ€” `uv` (:17), `github-actions` (:42), `npm` (:61). None parses an inline `pip install X==Y` inside a workflow `run:` block. `tests/test_ci_venv_pinning.py:26-32` already records the consequence: *"a stale pin rots invisibly and a DELETED pin is invisible twice over."* So ADR 0034's *"Re-evaluate when 4.5.0 ages out"* โ€” the residuals-table row for `release.yml`'s `sigstore==4.4.0`, and its twin in the ยง3 register row for `release.yml` `pip install sigstore` (`:358` and `:206` respectively, read at `46ea10a7`; **this item originally cited `:350`, which is a blank line and was wrong at every commit**) โ€” has no trigger and no owner. **Discharged 2026-09-03:** the owner ruled the version, the clause must not be actioned again, and `.github/dependabot.yml` now carries `ignore: sigstore >=4.5.0` as the machine backstop. 2. **`sigstore` is not the only one.** Two sibling installs in the same privileged job float their transitives identically: `release.yml:104` `python -m pip install "pip==26.1.2" "build==1.5.0"` (the PEP 517 frontend that produces the published wheel) and `release.yml:207` `python -m pip install "pip==26.1.2" "cyclonedx-bom~=7.3.1"` โ€” the latter a `~=` *range*, into the main interpreter. Only the scratch venv at `:210` uses `--require-hashes`. Fixing `sigstore` alone narrows the window; it does not close the class. -**Correction to the audit's framing:** it cites ADR 0034's *"Option B โ€ฆ remains the only thing that closes the alert"* (`:206-208`), but the ADR's own 2026-07-29 amendment at `:263-265` states it **supersedes that sentence**. The mechanism is no longer hypothetical โ€” it was built for CI tooling and is running. What is open is that `sigstore` was deliberately left out of it, for a reason at `:350` that a naive "just add it to the lock" fix would silently invert (see **Proposed**). +**Correction to the audit's framing:** it cites ADR 0034's *"Option B โ€ฆ remains the only thing that closes the alert"* (`:206-208`), but the ADR's own 2026-07-29 amendment at `:263-265` states it **supersedes that sentence**. The mechanism is no longer hypothetical โ€” it was built for CI tooling and is running. What is open is that `sigstore` was deliberately left out of it, for a reason in the residuals-table row (`:358` at `46ea10a7`, **not `:350` as this item first wrote**) that a naive "just add it to the lock" fix would silently invert (see **Proposed**). *(Both halves are now settled: the lock landed, and the version is an owner ruling. The row's reason turned out to rest on a false counterfactual โ€” a group entry spelled `sigstore==4.4.0` hash-locks 4.4.0, so "hash-lock it" and "keep 4.4.0" were never exclusive.)* **Why:** exploitation is an **upstream** compromise, not a repo-local weakness โ€” an attacker needs to own one of ~30 PyPI packages in `sigstore`'s closure (or land a hijack/typosquat) during the window a maintainer pushes a tag. The release job is **not reachable from a pull request**; it is gated on a tag push or `workflow_dispatch` (`release.yml:66` also pins it to `MEFORORG/MessageFoundry`). Anyone who can already trigger it is a maintainer who could do worse directly. @@ -3430,13 +3440,14 @@ What makes it worth fixing anyway is the **blast radius if it lands**: arbitrary Honestly bounded: **this is build-time only.** No PHI path, no running-engine surface, no operator-reachable behaviour. It does not touch the store, the API, or any connector. And per ADR 0034 ยง3 (`:162-170`) the fix moves the OPEN Scorecard count only for the lines it actually converts to `--require-hashes`; the two genuinely-open `PinnedDependenciesID` alerts named at `:336-341` are the SBOM scratch-venv pair, not this one. -**Proposed** *(step status as of 2026-09-03: **2, 3, 5 DONE** in `a9354808e`; **4 measured, no -contamination, both ways**; **1 OPEN โ€” the owner ruling and the shipped pin disagree**; **6 NOT -STARTED**. Step 1's own warning โ€” "this is a recorded owner decision; do not invert it silently" โ€” is -the one that was not honoured, so it is restated in the banner rather than left here to be re-read -after the fact.)*: repeat the mechanism ADR 0034's 2026-07-29 amendment already proved, and handle the version question the residual row raises rather than stepping over it. +**Proposed** *(step status as of 2026-09-03: **1 SETTLED** โ€” owner ruling, 4.4.0, and the pin corrected +back to it here; **2, 3, 5 DONE** in `a9354808e`; **4 measured, no contamination, both ways**; **6 NOT +STARTED**, and now the only reason this item is open. Step 1's own warning โ€” "this is a recorded owner +decision; do not invert it silently" โ€” is the one that was not honoured, so it is restated in the +banner and backed by a `dependabot.yml` `ignore` entry rather than left here to be re-read after the +fact.)*: repeat the mechanism ADR 0034's 2026-07-29 amendment already proved, and handle the version question the residual row raises rather than stepping over it. -1. **Resolve the 4.4.0-vs-4.5.0 decision first, explicitly.** ADR 0034:350 keeps `sigstore` out of the lock *because* routing it through would resolve **4.5.0**, which was `<48 h` old against `.github/dependabot.yml`'s `cooldown: default-days: 5` (:26-30). That was written 2026-07-29. By the ADR's own arithmetic the window closes around 2026-08-01/02 โ€” i.e. now โ€” but **confirm the actual PyPI publish date before acting**; this item does not verify it, and the whole rationale hangs on it. If the window has closed, the objection is spent and the residual row should be amended, not quietly contradicted. This is a recorded owner decision; do not invert it silently. +1. **Resolve the 4.4.0-vs-4.5.0 decision first, explicitly.** *(DONE โ€” the owner ruled 4.4.0 on 2026-08-22 and re-affirmed it 2026-09-03. The premise below is also false: routing `sigstore` through the lock does NOT force 4.5.0, because a group entry carries its own specifier.)* ADR 0034's residuals-table row (`:358` at `46ea10a7`; **this line originally cited `:350`, which is blank**) keeps `sigstore` out of the lock *because* routing it through would resolve **4.5.0**, which was `<48 h` old against `.github/dependabot.yml`'s `cooldown: default-days: 5` (:26-30). That was written 2026-07-29. By the ADR's own arithmetic the window closes around 2026-08-01/02 โ€” i.e. now โ€” but **confirm the actual PyPI publish date before acting**; this item does not verify it, and the whole rationale hangs on it. If the window has closed, the objection is spent and the residual row should be amended, not quietly contradicted. This is a recorded owner decision; do not invert it silently. 2. **Add a PEP 735 `release-tools` group** to `pyproject.toml` alongside `ci-scanners`/`ci-quality`, non-default (ADR 0034:283-287, decision 2 โ€” an extra becomes a real install target; a default group lands in the release SBOM and in what `pip-audit` audits as runtime). 3. **Export it as a seventh lock** in `.github/workflows/security.yml:87-89`, added to the `git diff --exit-code` gate on `:89`, to `dependabot-lock-resync.yml`, and to `tests/test_dep1_lock_resync_lockstep.py` (which asserts the export set, the per-file flags, and that everything exported is staged โ€” currently six-place). Install it at `release.yml:255` with `--require-hashes`. 4. **Measure the resolve contamination before committing.** This is the known failure mode: ADR 0034:354 records that `semgrep` was excluded by decision precisely because a new group *"still forces a `click 8.4.1 โ†’ 8.4.2` re-resolve across all four artifacts."* `sigstore` pulls `cryptography`, `requests` and `pydantic`-adjacent packages that the runtime closure also carries. Acceptance criterion is the one that ADR used at :287 โ€” re-export all existing locks and require `git diff --exit-code` โ†’ 0. If it is non-zero, this becomes the same excluded-by-decision call semgrep got, recorded as a residual rather than forced through. @@ -3445,7 +3456,7 @@ after the fact.)*: repeat the mechanism ADR 0034's 2026-07-29 amendment already **Testability caveat, stated up front:** ADR 0034:218-225 records that `release.yml` runs **only on a tag push**, so no PR CI leg executes this path โ€” the first real run of any change here is a release. Follow that section's own protocol: dry-run via `workflow_dispatch` and read the log before the next tag. -**Related:** `.github/workflows/release.yml:104,207,244-259`, `.github/workflows/security.yml:87-89`, `.github/dependabot.yml:17,26-30,42,61`, `pyproject.toml:244-284`, `ci/locks/`, `tests/test_ci_venv_pinning.py`, `tests/test_dep1_lock_resync_lockstep.py`, [ADR 0034](adr/0034-static-analysis-triage-policy-accepted-risk-register.md) ยง3 + the 2026-07-29 amendment (esp. the residuals table, `:350`), #321, and the Dependabot auto-merge item from this same audit (a different file and a different fix โ€” the two are siblings, not one change). +**Related:** `.github/workflows/release.yml:104,207,244-259`, `.github/workflows/security.yml:87-89`, `.github/dependabot.yml:17,26-30,42,61`, `pyproject.toml:244-284`, `ci/locks/`, `tests/test_ci_venv_pinning.py`, `tests/test_dep1_lock_resync_lockstep.py`, [ADR 0034](adr/0034-static-analysis-triage-policy-accepted-risk-register.md) ยง3 + the 2026-07-29 amendment (esp. the residuals table row for `release.yml`'s `sigstore==4.4.0`, `:358` at `46ea10a7` โ€” **not `:350`**) + the 2026-09-03 amendment that retires it, #321, and the Dependabot auto-merge item from this same audit (a different file and a different fix โ€” the two are siblings, not one change). **Source:** public-repo disclosure audit, 2026-08-01. Classified close-the-weakness-instead: the `release.yml:253-254` note and ADR 0034's residual row are honest and stay โ€” the unhashed install is what needs fixing. diff --git a/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md b/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md index 545a86606..72e6e6833 100644 --- a/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md +++ b/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md @@ -203,7 +203,7 @@ make them PR-visible instead. The `dependabot-auto-merge.yml` scope row is still | Where | Recommendation | Status | Why it matters | |---|---|---|---| -| `release.yml` `pip install sigstore` | Pin `sigstore==` | **Done** โ€” `sigstore==4.4.0`. Deliberately *not* the newer 4.5.0: `.github/dependabot.yml` sets `cooldown.default-days: 5`, 4.5.0 was <48 h old, and pinning the *signing* toolchain fresher than the repo's own update policy allows would invert that policy at the highest-privilege point. Re-evaluate once it ages out. | The **highest residual in the group**: a completely unpinned install inside the job holding `contents: write` + `id-token: write` + `attestations: write`, resolved immediately before it signs the wheel, sdist, SBOM and VEX. A malicious release fetched at that moment runs with the OIDC identity used to publish. | +| `release.yml` `pip install sigstore` | Pin `sigstore==` | **Done, and SUPERSEDED TWICE โ€” read the 2026-09-03 amendment at the foot of this ADR before acting on this cell.** As written it says: `sigstore==4.4.0`, deliberately *not* 4.5.0, because `.github/dependabot.yml` sets `cooldown.default-days: 5` and 4.5.0 was <48 h old; *"Re-evaluate once it ages out."* **The version is still 4.4.0** โ€” that half is correct and is now an owner ruling (2026-08-22, re-affirmed 2026-09-03), not an inference from the cooldown. **Two things here are now wrong.** (1) It is no longer an inline `pip install` pin at all: `sigstore` flows through the PEP 735 `release-tools` group into `ci/locks/release-tools.lock`, installed with `--require-hashes`. (2) **"Re-evaluate once it ages out" is DISCHARGED, and must not be actioned again.** The cooldown expired 2026-08-02; both rulings postdate that, so re-deriving it does not reopen the version. It was re-derived once anyway, and shipped 4.5.0 in `a9354808e`. `.github/dependabot.yml` now carries a matching `ignore` entry so a bot cannot repeat it. | The **highest residual in the group**: a completely unpinned install inside the job holding `contents: write` + `id-token: write` + `attestations: write`, resolved immediately before it signs the wheel, sdist, SBOM and VEX. A malicious release fetched at that moment runs with the OIDC identity used to publish. | | `release.yml` `pip install --upgrade pip build` | Pin `build==` | **Done** โ€” `pip==26.1.2 build==1.5.0`, in **both** the engine and harness build steps. | Unpinned PEP 517 frontend that produces the published wheel/sdist. | | `release.yml` `pip install --quiet packaging` (harness job) | Pin `packaging==`; install into a throwaway venv as the engine job already does | **Done, both halves** โ€” pin *derived from `constraints.lock`* (it is a DEP-1 transitive, so a literal would rot), and moved into `/tmp/harnesssmoke` mirroring `/tmp/relsmoke`. | Resolved into the **publishing** job's main interpreter rather than a scratch venv. | | `release.yml` `pip install --quiet packaging` (`/tmp/relsmoke`) | Pin `packaging==` | **Done** โ€” same `constraints.lock`-derived pin. | Contained (disposable venv, version-compare only), but free to pin. | @@ -355,7 +355,7 @@ implied: | Residual | Why it is not fixed here | |---|---| -| **`release.yml`'s `sigstore==4.4.0`** | **SUPERSEDED BY THE SHIPPED CODE โ€” do not act on this row; its factual premise is false as written. Row kept rather than deleted because a reader arrives here by grep, and a missing row reads as "never considered".** It said `sigstore` was deliberately **absent** from `uv.lock` and from all six exports (0 hits). **Verified false 2026-09-03 at `46ea10a7`:** `sigstore` is in `uv.lock`, `ci/locks/release-tools.lock` is a tracked seventh export, and `release.yml:391` installs it with `--require-hashes`. The mechanism half of this row is spent. **The version half is now an OPEN QUESTION and is NOT resolved here:** this row and BACKLOG #332 record an **owner ruling of 2026-08-22 pinning `sigstore==4.4.0`, "NOT 4.5.0"**, while the shipped `pyproject.toml:295` pins **4.5.0** and `release.yml:385-390` argues the cooldown objection is spent. Both citations are live and they disagree. **It needs an owner ruling; do not resolve it by editing either side.** See BACKLOG #332. | +| **`release.yml`'s `sigstore==4.4.0`** | **RETIRED as a residual 2026-09-03 (BACKLOG #332) โ€” see the amendment of that date at the foot of this ADR. Row kept rather than deleted, per this table's own opening rule that a `won't fix` made invisible is worse than one stated with its reason.** Its factual premise is false as written: it said `sigstore` was deliberately **absent** from `uv.lock` and from all six exports (0 hits). **Verified false at `46ea10a7`** โ€” `sigstore` is in `uv.lock`, `ci/locks/release-tools.lock` is a tracked seventh export, and the install is `--require-hashes`. **The counterfactual is what made it wrong.** The row assumed routing `sigstore` through the lock *would* force 4.5.0, so it treated "hash-lock it" and "keep 4.4.0" as mutually exclusive. A dependency group carries an explicit specifier, so `sigstore==4.4.0` hash-locks 4.4.0. They were never in tension, and that false coupling is what held a real supply-chain gap open. **Both halves now hold at once, and both are settled:** hash-locked, at 4.4.0, by owner ruling. | | **The `uv` bootstrap** (`security.yml`, `python -m pip install --upgrade pip "uv==0.12.0"`) | **Permanently circular: you cannot hash-lock `uv` with `uv`.** That install produces every lock this repo commits. `uv` stays an inline `==` pin, and `pip` remains the sole registered *name* in `SECURITY_YML_ACCEPTED_UNPINNED`. Note it is also the pip that runs the **six exports and the diff gate** โ€” the `--require-hashes` install two steps later *downgrades* pip to the locked version afterwards, so the DEP-1 step's own posture is unchanged by this work. *Cheap out-of-band fix that removes it entirely:* `astral-sh/setup-uv@c771a70eโ€ฆ` is already SHA-pinned and used in 9 places (`ci.yml` ร—6, `quality-advisory.yml` ร—2, the resync ร—1); swapping it in deletes the install. Separate change. | | **`security.yml`'s unpinned `pip` in the `semgrep` step** โ€” `python -m pip install --upgrade pip "semgrep==1.172.0"` | **The SECOND surviving bootstrap, named because an undercounted inventory is how a real finding goes invisible.** The semgrep row below explains only the `[otel]` conflict that keeps *semgrep* inline; this row records that the same line is also an **unpinned `pip` fetch**. So two `--upgrade pip` bootstraps remain in the file, not one โ€” now asserted as an exact count by `test_security_yml_pip_bootstrap_count_is_exact`, since `SECURITY_YML_ACCEPTED_UNPINNED` registers the *name* `pip` and cannot tell two accepted bootstraps from twenty. **Mitigation WITHDRAWN 2026-08-04 (BACKLOG #334) โ€” it rested on a false premise.** This row previously read *"Mitigating: `semgrep` is not a required context (`tests/test_required_contexts.py`), so this one does not sit on the merge path."* That is false in the repo's own records: `semgrep (project SAST rules)` is at `.github/required-contexts.txt:78`, and `tests/test_security_posture.py`'s `_BLOCKING_SECURITY_JOBS` names `semgrep` and asserts that membership. `tests/test_required_contexts.py` never claimed the opposite โ€” it pins the required *set*, which contains it; the citation was to a file that says the reverse of what it was cited for. So this bootstrap **does** sit on the merge path, and #334 widened that same step's scan from a two-directory allow-list to the whole repo, which *increases* what rides on it. Re-accepted with that known, on the `uv` row's grounds (a bootstrap `pip` cannot hash-lock itself). It disappears whenever the semgrep row's `[tool.uv] conflicts` recipe is taken. | | **`quality-advisory.yml`'s `pipx install ruff`** | **Outside the guard's regex and outside Scorecard's.** `test_ci_venv_pinning.py`'s `_PIP_INSTALL` matches `pip`/`pip3`/`python -m pip` only, so the unpinned fallback branch is invisible to every existing guard โ€” and because it is not a `pip install`, **no alert exists to close**. `pipx` has no `--require-hashes`, so fixing it means changing the install mechanism, not the pin. Recorded, not done. | @@ -474,3 +474,67 @@ one is an accepted residual. exist with the same semantics; a decision on `require-opener-as-author`, which defaults to true and fails the check; and a rehearsal in a scratch repo. Land in a low-traffic window with a revert prepared. + +## Amendment โ€” 2026-09-03: the `sigstore` residual is retired, and the version it protected is restored + +BACKLOG #332 routed the release signing toolchain through the hashed-lock mechanism this ADR's +2026-07-29 amendment built. Two cells above are superseded by it: the `release.yml pip install +sigstore` row in the ยง3 register, and the `sigstore` row in the residuals table. Both are marked in +place rather than deleted. + +* **What is enforced now.** A non-default PEP 735 `release-tools` group in `pyproject.toml`; + `ci/locks/release-tools.lock` as its hashed export, the seventh in the DEP-1 set; and + `release.yml` installing the toolchain with `pip install --require-hashes -r + ci/locks/release-tools.lock`. The ~30 transitives that previously floated unhashed at signing + time, inside the job holding `id-token: write`, are pinned by hash. +* **The residual's reasoning contained a false counterfactual, and that is the transferable lesson.** + It asserted that routing `sigstore` through the lock *would* resolve 4.5.0, and so treated + "hash-lock it" and "keep 4.4.0" as mutually exclusive. A dependency group carries an explicit + specifier, so a group reading `sigstore==4.4.0` resolves 4.4.0. The two questions were never + coupled. That false coupling held a real supply-chain weakness open for five weeks, and it is the + same shape this ADR's own Decision warns about: a dismissal whose stated reason does not survive + being tested. **When a residual rests on a counterfactual about a mechanism nobody has run, say so + in the row, and treat running it as cheap.** +* **The version is 4.4.0 by owner ruling** โ€” given 2026-08-22, re-affirmed 2026-09-03 once the full + history below was put to the owner. **The cooldown clause at the ยง3 register row is DISCHARGED and + must not be actioned again.** 4.5.0 published 2026-07-28T07:34:00Z, so the 5-day window closed + 2026-08-02; both rulings postdate that. A spent cooldown is therefore not evidence the pin is + stale โ€” it is the argument the rulings were made in spite of. +* **This was inverted once, by a re-derivation of exactly that clause, and the record is the point.** + The 4.5.0 case was published on PR 531 at 2026-08-26T23:46:18Z, arguing the re-evaluation clause at + this ADR's two rows had come due. **Its own author retracted it at 2026-08-27T02:45:36Z**, in these + words: *"I checked whether the ADR's premise had expired. I never checked whether the question had + since been answered."* Commit `a9354808e` then shipped 4.5.0 on 2026-08-29, **two days after the + retraction**. Corrected under #332. +* **The clause now has a machine backstop.** `.github/dependabot.yml` carries `ignore: sigstore + >=4.5.0`, so a routine bot PR cannot re-propose the declined version. That entry is a deliberate + carve-out from the file's "the `==` pins in `[dependency-groups]` are NOT ignored" policy, whose + reason is about versions nobody has decided; the carve-out and its lifting condition are recorded + at the policy itself. +* **A gap this work found and closed.** `a9354808e` added `ci/locks/release-tools.lock` to the export + set and the byte-diff gate but **not** to `security.yml`'s audit step, leaving the release signing + closure the only committed lock nothing audited โ€” hash-pinned, therefore sticky, therefore exactly + the *"pinned, stale, unpatched is worse than floating"* case ยง3 names. `pip-audit -r + ci/locks/release-tools.lock` now runs beside the other two. It is also what makes the `ignore` + entry acceptable, since an `ignore` suppresses the security track for its range; each side's + comment names the other. +* **Patch availability does not discriminate between the two versions.** Measured against PyPI + 2026-09-03: sigstore 4.x is 4.0.0, 4.1.0, 4.2.0, 4.3.0, 4.4.0, 4.5.0 and nothing else โ€” zero patch + releases across the series, so neither 4.4.0 nor 4.5.0 has a patch line. The 3.x series did ship + patches (3.5.1, 3.6.7), so a 4.4.1 remains possible and the `ignore` is scoped `>=4.5.0` to leave + that track open. +* **Contamination: measured, none.** The acceptance criterion this ADR used at its own convergence โ€” + re-export every committed lock and require `git diff --exit-code` to return 0 โ€” was run with the + CI-pinned `uv==0.12.0`, control first (the unchanged tree re-exported byte-identically, so the + instrument matches CI's). All six pre-existing DEP-1 artifacts stay byte-identical; `uv lock` moves + one line and `release-tools.lock` three. The `semgrep`-style excluded-by-decision call was never + reached. +* **Still open, deliberately.** `build` and `cyclonedx-bom` remain inline installs in the same + privileged job. Per the "2 genuinely open `PinnedDependenciesID` alerts" paragraph above, + `cyclonedx-bom` is half of a pair `test_sbom_install_is_byte_identical_in_release_and_security` + requires to stay byte-identical, so both halves must move in one commit. Tracked at BACKLOG #332. +* **No PR CI leg executes the signing path.** `release.yml` runs only on a tag push or + `workflow_dispatch` (see "What no test can see"). The guards assert the lock exists, pins + `sigstore` exactly, is fully hashed and is installed with `--require-hashes`; they cannot assert it + installs. **The first real run of this change is a release** โ€” dry-run via `workflow_dispatch` and + read the log before the next tag.