feat(cli): sign synth's output ELF binaries via sigil (release Phase 5)#135
Merged
Conversation
Add `synth compile --sign-output`: after writing the ELF, invoke sigil's `wsc sign --keyless --format elf -i <path> -o <path>.signing.tmp` and atomically rename the signed file over the original. Off by default — opt in per invocation; unsigned compile path unchanged for consumers without `wsc` installed. The flag composes with `--all-exports` (one signing call covers the multi-function ELF) and with `--sbom` (the SBOM records the unsigned synth output; the on-disk ELF after signing is the signed version — documented). Error model is conservative: missing `wsc` produces a non-zero exit with an actionable message pointing at sigil's install instructions; wsc-side failures surface the wsc stderr verbatim plus a "sigil interface assumption broken — update crates/synth-cli/src/sign.rs" hint so a future wsc CLI break fails loudly rather than silently no-opping. The wsc contract this code depends on (sign --keyless --format elf -i -o) is pinned by a unit test on the argv shape and documented in the module-level doc-comment. End-to-end signing was not validated by the implementing agent — `wsc` was not on PATH; a maintainer with sigil installed should run the verification command in docs/sigil-integration.md to confirm the wsc contract holds. Closes the compiler-side of release roadmap Phase 5; pipeline integration (release.yml) is a separate follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
3 tasks
# Conflicts: # CHANGELOG.md
avrabe
added a commit
that referenced
this pull request
May 24, 2026
Merged
3 tasks
avrabe
added a commit
that referenced
this pull request
May 25, 2026
…pered WASM) The signing-e2e workflow's case 3 (tamper-negative on signed WASM) ran on commit 7f1a9c9 against wsc v0.9.0 and surfaced a real sigil-side gap: 'wsc verify --keyless' returned exit 0 on a WASM whose signed payload (byte at offset 64, inside the module — well before the ~15kB trailing signature section) had been flipped. Filed upstream as pulseengine/sigil#135 with full repro evidence (CI run, exact wsc version + sha256, the byte-flip script). This commit converts case 3 to an xfail with an explicit reference to sigil#135. The test stays in place: - If wsc continues to (incorrectly) accept the tampered file, case 3 XFAILs as expected (workflow green) and notes the gap in CI output. - If sigil#135 is fixed and wsc starts rejecting, case 3 XPASSes and emits a 'flip this back to a hard check' note to maintainers. Also documents the gap in docs/sigil-integration.md (Status section) and adds a CHANGELOG [Unreleased] entry tracking the upstream issue. Until sigil#135 is fixed, 'wsc verify --keyless' should be treated as proving signature-blob well-formedness, NOT module integrity. PR #135's synth-cli signing path is unchanged; the gap is purely on the verify side, but it affects what downstream consumers can claim about a synth-emitted signed artifact.
avrabe
added a commit
that referenced
this pull request
May 25, 2026
…ails on missing wsc (#140) * ci(signing): end-to-end wsc validation — sha256-pinned sigil v0.9.0 Closes the "end-to-end signing was not validated" gap explicitly flagged when Phase 5 (`synth compile --sign-output`, PR #135) shipped. The implementing subagent honestly noted that `wsc` was not on PATH in its environment, so only argv-shape unit tests existed; the actual Fulcio + Rekor round-trip was unverified. What this lands: - `.github/workflows/signing-e2e.yml`: downloads a sha256-pinned `wsc` from sigil's GitHub releases (v0.9.0, sha256 9054b4b066e2b0a954110851a43266ff0e9ef12b4e1ecc03c333943fd52cecb6 for the linux-x86_64 binary), builds synth-cli, and runs the e2e test. Triggered on PRs that touch `crates/synth-cli/src/sign.rs|main.rs`, on push to main, and on `v*` tag pushes. `id-token: write` is granted so wsc's keyless flow can obtain a GitHub OIDC token. - `tests/wsc_sign_e2e.sh`: three load-bearing cases. (1) WASM keyless sign + verify round-trip — proves wsc is healthy and the keyless code path that sigil supports today works end-to-end. (2) Synth's actual contract: `synth compile --sign-output` against ELF. As of sigil v0.9.0 this returns the explicit usage error "Keyless signing is currently supported only for WASM format" (sigil's `src/cli/main.rs:770-779`). The script asserts synth surfaces that error verbatim, exits non-zero, preserves the unsigned ELF, and cleans up the `.signing.tmp`. When sigil eventually ships keyless-ELF support, the script auto-detects the flip via a GitHub `::notice::` annotation and switches case 2 into a positive "signed ELF" assertion. (3) Tamper-negative on the case-1 signed WASM — flips a byte, runs `wsc verify --keyless`, and fails if it accepts the tampered module. The script does NOT silently skip when wsc is missing; that would recreate the exact gap this workstream closes. - `docs/sigil-integration.md`: replaces the "End-to-end signing + verification was not validated by the implementing agent" stanza with the actual CI status. Adds a "Verifying a signed WASM module locally" recipe (the path that actually works today) and corrects the verification-flag documentation: sigil v0.9.0 accepts the literal `--cert-identity`, not `--cert-identity-regexp`. - `docs/release-process.md`: Phase 5 status updated from "compiler-side implemented" to "compiler-side implemented + CI-validated end-to-end". Approach: release-download with sha256 pin, not Bazel. `rules_wasm_component` is already in MODULE.bazel but it ships WASM-component toolchains, not the `wsc` CLI distribution; sigil's own release binaries ship per-asset .sha256 sidecars, so pinning by version + sha256 in one place (the workflow file) gives an auditable trust anchor without the complexity of a custom binary- import Bazel rule. Locally validated against sigil v0.9.0's macOS-aarch64 wsc + a debug synth build (same source tree as this commit): case 2 exits rc=1, surfaces the "Keyless signing is currently supported only for WASM" error verbatim, preserves the 741-byte unsigned ELF, and leaves no stale .signing.tmp. Cases 1 and 3 require an OIDC token (GitHub Actions only) and are exercised in CI. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(signing): xfail case 3 against sigil#135 (wsc verify accepts tampered WASM) The signing-e2e workflow's case 3 (tamper-negative on signed WASM) ran on commit 7f1a9c9 against wsc v0.9.0 and surfaced a real sigil-side gap: 'wsc verify --keyless' returned exit 0 on a WASM whose signed payload (byte at offset 64, inside the module — well before the ~15kB trailing signature section) had been flipped. Filed upstream as pulseengine/sigil#135 with full repro evidence (CI run, exact wsc version + sha256, the byte-flip script). This commit converts case 3 to an xfail with an explicit reference to sigil#135. The test stays in place: - If wsc continues to (incorrectly) accept the tampered file, case 3 XFAILs as expected (workflow green) and notes the gap in CI output. - If sigil#135 is fixed and wsc starts rejecting, case 3 XPASSes and emits a 'flip this back to a hard check' note to maintainers. Also documents the gap in docs/sigil-integration.md (Status section) and adds a CHANGELOG [Unreleased] entry tracking the upstream issue. Until sigil#135 is fixed, 'wsc verify --keyless' should be treated as proving signature-blob well-formedness, NOT module integrity. PR #135's synth-cli signing path is unchanged; the gap is purely on the verify side, but it affects what downstream consumers can claim about a synth-emitted signed artifact. --------- Co-authored-by: Claude Opus 4.7 <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
Adds
synth compile --sign-output— when set, aftersynth compilewrites the ELF, invokeswsc sign --keyless --format elf(from the siblingpulseengine/sigiltoolchain) to embed a Sigstore keyless signature in the ELF. Closes Phase 5 of the release roadmap indocs/release-process.md. Off by default —wscis an external dependency.This is the complement to synth being signed at release: now the output of
synth compilecan also be signed. A downstream consumer running the firmware on a microcontroller gets a cryptographic proof of what produced it (Fulcio cert + Rekor log).Implementation
crates/synth-cli/src/sign.rs—sign_elf(path)invokeswscviastd::process::Command, writes to<path>.signing.tmp, thenfs::renames atomically over the original.["sign", "--keyless", "--format", "elf", "-i", <path>, "-o", <tmp>]— cross-checked against sigil's real CLI (pulseengine/sigil'ssrc/cli/main.rs:signsubcommand accepts-i/--input-file,-o/--output-file,--keylessSetTrue,--formatacceptingelf).--all-exportscovered: that path produces a single multi-function ELF, so one signing call after the write covers everything.wscproduces an actionable error namingwsc, sigil's URL, and PATH guidance — exits non-zero.Tests (unit-only)
Three tests in
sign.rs::tests:argv_shape_matches_wsc_contract— pins the exact argv vector against sigil's CLI.tmp_path_is_deterministic_sibling— confirms the.signing.tmppath.missing_wsc_produces_actionable_error— verifies the error message mentionswscandsigil.Workspace tests: 0 regressions, clippy + fmt clean.
Honest blocker (verified clean-room)
End-to-end signing was NOT validated —
wscisn't installed in the agent's environment, so the actual Fulcio/Rekor round-trip is untested. The argv vector is pinned against sigil's source by the unit test; a future sigil CLI break will fail loudly (the post-success file-existence check guards against silent no-ops). A maintainer with sigil installed should manually run:Documented in
docs/sigil-integration.md§ "What was not tested".Other notes
--sign-output+--sbomproduce divergent hashes. SBOM records the unsigned ELF hash; on-disk ELF is the signed version. Documented as intentional; downstream chains needing one hash should sign the SBOM separately. Comparison table indocs/sigil-integration.md.std::process::Commandis enough).mainas of the agent's read. Any sigil CLI break surfaces via the unit test + a runtime error with a "sigil interface assumption broken" hint.Clean-room verification
Findings independently verified by a clean-room subagent (no inherited context): 11 of 12 claims CONFIRMED with file:line citations; 1 CANNOT-VERIFY (historical test run — structural). The verifier independently fetched
pulseengine/sigil's source to cross-check the wsc CLI surface — argv matches.🤖 Generated with Claude Code