fix: fail closed on untrusted native releases - #115
Merged
Conversation
…ion-release-gates
This branch moves the frozen runtime to the newest published Flow so the tree builds and tests against it. That pin is not releasable: the immutable 1.31.0 wheel predates the `openadapt.push-result/v1` contract the governed push path requires, so an installer built on it can only fail closed on every hosted handoff. Record the hold where a release operator will see it, name the exact ordered exit conditions, and require the section to be deleted in the same pull request that lands the releasable pin. Also state plainly that the hold is a distribution hold and not a production-acceptance claim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y run
Every `gh attestation verify` invocation this branch adds passed both
`--signer-workflow` and `--cert-identity`. Those two flags, with
`--signer-repo` and `--cert-identity-regex`, form one mutually exclusive group:
`gh` refuses the command before it verifies anything.
if any flags in the group [cert-identity cert-identity-regex signer-repo
signer-workflow] are set none of the others can be
So the release workflow could never complete its own attest step, the mirror
job could never authenticate the verified index, and the public download
verifier always exited 2. The behaviour was fail-closed, not fail-open, but the
whole documented verification path was unreachable. The unit tests could not
see it because they monkeypatch `subprocess.run` to return success.
Keep only `--cert-identity`. It is the strictest of the four: it pins the
complete subject alternative name -- repository, workflow path, and ref --
rather than the repository and workflow path alone. Pin the OIDC issuer
explicitly beside it so `GH_HOST` cannot redirect the lookup.
Three further corrections in the same path:
- The verified index is attested by `mirror-installers-to-engine-release`,
which runs on the `release: published` event for the native tag. GitHub
therefore issues its certificate with `refs/tags/desktop-vX.Y.Z` as the
workflow ref. Both the workflow's own check and the download verifier
demanded `refs/heads/main`, which that certificate can never carry.
- `SHA256SUMS` was authenticated by digest and then read from disk twice more,
once to compare against the index and once inside `verify()`. A local writer
could swap it in that window and every installer would be checked against
attacker-chosen digests. The authenticated bytes now flow through
`validate_channel`, `validate_index`, `read_manifest`, and `verify`, so each
document is parsed from exactly the bytes its check hashed. The prior-channel
descriptor is read once for the same reason.
- `docs/CODE_SIGNING.md` and `docs/BETA_NATIVE_INSTALLERS.md` gave users the
same unrunnable command to copy. Both now show the corrected one.
New guards, since the old `--signer-workflow` assertion encoded the broken
shape: `test_release_workflow_attestation_checks_use_one_exact_identity_flag`
reads the workflow text and refuses any second identity flag, and
`test_public_download_verifier_uses_the_authenticated_checksum_bytes` swaps
`SHA256SUMS` after authentication and requires the result to be unaffected.
That test fails against the previous code.
Tests: full suite 1026 passed. Ruff passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
abrichr
marked this pull request as ready for review
August 19, 2026 22:40
This was referenced Aug 19, 2026
abrichr
added a commit
that referenced
this pull request
Aug 19, 2026
#115 hardened `verify_checksums` to accept only a manifest named exactly `SHA256SUMS`, but the CI build lane still wrote `SHA256SUMS-${platform}-${architecture}`. All four native installer jobs on `main` then failed: error: SHA256SUMS must be inside the release asset directory The native installer matrix is skipped on pull requests and runs on `main` pushes, so the exact-head checks on #115 could not see it. Write and verify `dist/native/SHA256SUMS`. Each matrix job owns its own `dist/native` directory and uploads under a distinct artifact name, so the plain name is unambiguous and nothing consumed the per-platform name. The hardened check is unchanged. New `test_every_workflow_checksum_manifest_uses_the_exact_name` reads every workflow and refuses a `--output` or `--manifest` value that mentions SHA256SUMS without ending in it, so this cannot regress silently again. It fails against the previous build.yml. Tests: full suite 1028 passed. Ruff passed. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
abrichr
added a commit
that referenced
this pull request
Aug 20, 2026
Three gaps that PR #115 deliberately deferred, plus three adjacent ones. Branch-name bypass of the version advance guard. The guard gated its only job on `startsWith(github.head_ref, 'native-version/v')`. The head branch name is author controlled and GitHub counts a skipped job as a satisfied required status check, so a version bump from any other branch skipped the guard entirely. The job now always runs for the five paths. It cannot simply demand the reserved branch, because Dependabot npm/cargo updates and ordinary feature work edit the same files for dependency reasons. It decides on content instead: when the pull request leaves the native version alone it passes; when the pull request moves the version, only the reserved `native-version/v*` branch of this repository may do it, from the current protected base, as the exact strict advance. The job also checks out the base rather than the head, so a pull request cannot rewrite the guard that judges it. Fail-open monotonicity check. `write_verified_release_index` and `write_verified_release_channel` skipped the strict-advance comparison when `--existing` named an absent file, and the release workflow made that reachable by ending its download with `|| true`. A passed but missing `--existing` is now an error, and the workflow decides from the release's authenticated asset list instead of from the absence of a downloaded file. Release-order comparison set filtered by mutable metadata. The set admitted a release only when `draft` was false, `prerelease` was true, and the body carried the installer marker. All three are mutable, and this repository rewrites release notes itself, so a release that lost the marker dropped out and a lower tag became acceptable. The comparison set is now the immutable `desktop-v*` Git tag namespace. That set cannot observe draft state, which the old set refused as a side effect, so `publish-draft` now refuses explicitly to return an already public release to draft. Also: - `verify()` proved each member regular and unlinked, then hashed `(directory / name).read_bytes()`, which resolved the name again and followed a link created in between. It now hashes the validated path objects, and every read in the standalone verifier opens with `O_NOFOLLOW` where the platform has it. - `_git_bytes` and the `git diff` call interpolated a caller ref into an argv element, so a ref beginning with `-` reached Git as an option. Every ref now resolves through `git rev-parse --verify` to a 40-character object id first, and the diff argument list ends with `--`. - `expected_asset_names` in the standalone verifier duplicates `expected_release_asset_names`. The verifier ships beside the installers and may not import the module, so a new contract test fails when the two definitions drift apart. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
mainwith a protected version-sync pull request; the post-merge job only creates the immutabledesktop-vX.Y.Ztagmain, strictly advances the version, and contains only the exact five-file transformSHA256SUMSby SHA-256SHA256SUMSagainst their exact GitHub workflow certificate identities before installer verificationApproval-held target-state work
This pull request remains a draft. It does not yet move native build, signing,
publication, mirror, or channel-pointer mutation into a protected-main
workflow_dispatchtransaction. The existing tag and release-event workflowmust not publish a release. The privileged workflow rewrite needs explicit
founder approval after its write-authority and publication risk is reviewed.
The Desktop Cloud Connector replacement and its shared qualification-v2
terminal admission are also approval-held in draft PR #116. No release can
occur until Desktop consumes the frozen shared verifier and accepts only an
exact signed
VERIFIEDresult that binds the permit, run sequence, policy,identity, and effect proof.
Runtime dependency state
This branch updates the frozen runtime from Flow 1.27.1 and Types 0.7.0 to the published Flow 1.31.0 and Types 0.10.0 set. Flow 1.31.0 includes the qualified-bundle sanitization fix from Flow commit
faf9945537d4011baeb36ce5f063b6e1814903e6and PR OpenAdaptAI/openadapt-flow#351.This is an interim exact pin. It is not the final hosted-operation release pin. Do not release the native application until the later immutable Flow release supplies the complete authorization and typed push-result contract, Cloud deploys and accepts that exact runtime, and Desktop updates its pin and lock from that accepted evidence.
External activation required
The workflow is not the complete trust boundary. Before the next native tag or release:
main. Require exact-head checks and require branches to be up to date before merge.Reject a stale native version pull requestcheck fornative-version/v*pull requests.v*. Only the engine release identity can create a tag. Do not permit update or deletion.desktop-v*. Only the native freshness identity can create a tag. Do not permit update or deletion.native-releaseenvironment:APPLE_CERTIFICATEAPPLE_CERTIFICATE_PASSWORDAPPLE_SIGNING_IDENTITYAPPLE_IDAPPLE_PASSWORDAPPLE_TEAM_IDAZURE_TENANT_IDAZURE_CLIENT_IDAZURE_CLIENT_SECRETTRUSTED_SIGNING_ENDPOINTTRUSTED_SIGNING_ACCOUNTTRUSTED_SIGNING_CERTIFICATE_PROFILEWINDOWS_CERTIFICATEWINDOWS_CERTIFICATE_PASSWORDWINDOWS_CERTIFICATE_THUMBPRINTDo not configure both Windows methods. Linux needs no founder-managed key. GitHub gives the pinned release workflow a short-lived OIDC identity.
Exact-head local evidence
Head:
997da51The full Python suite needed local loopback socket access. Its first sandboxed attempt failed only because the sandbox denied that socket bind. The full rerun with loopback access passed.
Release operation
Keep this PR as a draft. Do not merge it or publish a native release until the approval-held workflow changes, repository rules, signing identities, final Flow pin, Cloud acceptance, and live release verification are complete.
Historical
desktop-v0.15.0assets keep their original ad-hoc or unsigned labels. They do not satisfy this trust contract.