feat(security): #504 MVP — --emit-attest sidecar + perry verify --attest#988
Merged
Conversation
Compile-time foundation primitive for reproducible builds / binary
attestation. `perry compile --emit-attest main.ts -o myapp` writes
`myapp.attest.json` next to the executable carrying SHA-256 of the
*post-strip* binary plus provenance metadata:
{
"version": 1,
"sha256": "abcd1234...",
"size": 1048576,
"perry_version": "0.5.999",
"commit_sha": "0a1b2c3...",
"built_at_unix": 1715990400,
"binary_filename": "myapp"
}
Users who download the binary verify it locally:
perry verify --attest myapp
The verifier recomputes SHA-256 of the binary on disk and compares
against the sidecar. Mismatch fails with a verbose diagnostic that
reproduces both hashes — exactly the signal a swapped or tampered
binary would produce.
Full #504 acceptance includes deterministic builds + CI attestation
publication + signed sigstore bundles. The MVP ships the local
primitive — versioned manifest format, hash-and-compare verifier —
and the rest can layer on top:
- **Determinism work** can land later without changing the
manifest format; the version field reserves room.
- **CI publication** can wrap the local artifact in sigstore /
GitHub native attestations — the local file is the source of
truth either way.
- **Remote verification** (`perry verify --attest-url <url>`)
plumbs the same verify_against_sidecar() path with a fetched
manifest instead of an on-disk one.
Hooks into the compile_command driver after strip/codesign/etc.
post-link rewrites, so the captured hash matches the file users will
actually download. Same code path for every target (LLVM / WASM /
ArkTS / HarmonyOS / Glance / SwiftUI / JS) — no platform-specific
plumbing.
- CompileArgs gains `pub emit_attest: bool` (#[arg(long)]).
- CompilationContext gains `pub emit_attest: bool` (default false).
- Precedence ladder (last wins): perry.emitAttest in package.json
-> PERRY_EMIT_ATTEST env -> --emit-attest CLI.
- run.rs + dev.rs CompileArgs initialisers updated.
- AttestationManifest with #[derive(Serialize, Deserialize)].
- sha256_hex(path) -> streaming hash that doesn't OOM on big binaries.
- discover_commit_sha(project_root) -> shells out to git; empty
string when not a git checkout (best-effort).
- build_attestation(binary, project_root) -> manifest.
- write_attestation(binary, &manifest) -> writes pretty-printed JSON.
- verify_against_sidecar(binary) -> Result<manifest> with the
matching verbose-diagnostic error.
Existing `perry verify` does remote runtime-verification via
verify.perryts.com. `--attest` short-circuits to local-attest mode:
- reads <binary>.attest.json,
- recomputes SHA-256 + size,
- prints either "✓ attestation matches" + provenance OR the
mismatch diagnostic + exit 1.
No tokio runtime, no network, no beta-consent prompt under --attest.
- sha256_hex_matches_known_vector — pins SHA-256 of "hello\n".
- build_and_write_roundtrip — manifest fields + on-disk JSON shape.
- verify_passes_when_hash_matches — golden path.
- verify_fails_when_binary_tampered — pins the (#504) cite in the
mismatch diagnostic.
- verify_fails_when_sidecar_missing — pins the actionable
"produced by --emit-attest" guidance in the missing-sidecar error.
- manifest_is_pretty_printed_json — readable git diff invariant.
perry compile --emit-attest main.ts -o out
# → Wrote executable: out
# → Wrote attestation: out.attest.json
# → Binary size: 1.0MB
perry verify --attest out
# → ✓ attestation matches: /path/to/out + provenance
echo extra >> out
perry verify --attest out
# → Error: attestation MISMATCH ... (exit 1)
After strip / codesign / etc. post-link rewrites (between the strip
pass at compile.rs:7381 and the "Binary size:" print at 7389) so
the captured hash matches the file users will actually download.
The first attempt hooked next to the "Wrote executable:" print
which fired BEFORE strip — caught immediately by the smoke test
when the verify step found a size mismatch. Comment in the source
documents the placement for the next reader.
`docs/src/cli/emit-attest.md` documents the workflow + manifest
shape + verifier behavior + scope of the MVP. Linked from
SUMMARY.md under CLI Reference.
f27b35b to
f465f8a
Compare
3 tasks
proggeramlug
added a commit
that referenced
this pull request
May 18, 2026
…Rs (#1019) #981 (PERRY_SANDBOX_BUILDRS), #988 (--emit-attest), and #969 (perry.permissions) each landed via admin-bypass with their SUMMARY.md entries intact but without the actual .md content files (or, for #969, without any docs entry at all). docs/src/cli/lockdown.md and docs/src/cli/emit-sandbox.md did make it into main and are fine; the others left dead links. Separately, #976 / #972 / #974 added perry/system runtime methods (getOSVersion, shareText, shareUrl, appGroupSet/Get/Delete) but never updated the hand-maintained types/perry/system/index.d.ts. TypeScript users importing those APIs from `perry/system` get a type error today. This PR: - Creates docs/src/cli/sandbox-buildrs.md (#505) - Creates docs/src/cli/emit-attest.md (#504) - Creates docs/src/cli/capabilities.md (#501) and adds the SUMMARY.md entry - Adds the six new perry/system signatures to types/perry/system/index.d.ts The auto-generated docs/api/perry.d.ts + docs/src/api/reference.md were regenerated during the original PRs and are already current. Pure docs-only diff. No code changes.
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.
Closes #504 (MVP — see follow-up matrix; full reproducible builds + CI publication tracked separately).
Summary
perry compile --emit-attest main.ts -o myappnow writesmyapp.attest.jsonnext to the executable. The sidecar holds SHA-256 of the post-strip binary plus provenance metadata (perry version, git commit, build timestamp) so users who download the binary can verify it matches the publisher's build:The verifier recomputes SHA-256 of the binary on disk and compares against the sidecar. Mismatch fails with a verbose diagnostic reproducing both hashes — exactly the signal a swapped/tampered binary would produce.
Manifest shape
{ "version": 1, "sha256": "abcd1234...", "size": 1048576, "perry_version": "0.5.999", "commit_sha": "0a1b2c3...", "built_at_unix": 1715990400, "binary_filename": "myapp" }version: 1reserves room for future top-level keys (CI signature blob, sigstore bundle, reproducible-builds flags log) without breaking existing parsers.Cross-platform
Hooks into the platform-agnostic
compile_commanddriver afterstrip/ codesign / other post-link rewrites — same code path for every target (LLVM / WASM / ArkTS / HarmonyOS / Glance / SwiftUI / JS). The captured hash matches the file users will actually download regardless of which backend produced it.Plumbing (mirrors
--fast-math/--emit-sandbox)CompileArgsgainspub emit_attest: bool(#[arg(long)]).CompilationContextgainspub emit_attest: bool(default false).perry.emitAttestin package.json →PERRY_EMIT_ATTEST=1env →--emit-attestCLI.commands::attest:AttestationManifestwith#[derive(Serialize, Deserialize)].sha256_hex(path)— streaming hash (doesn't OOM on big binaries).discover_commit_sha(project_root)— shells out togit; empty string when not a git checkout (best-effort).build_attestation/write_attestation/verify_against_sidecar.New CLI:
perry verify --attest <binary>The existing
perry verifydoes remote runtime-verification viaverify.perryts.com. The new--attestflag short-circuits to local-attest mode — no tokio runtime, no network, no beta-consent prompt. Prints either✓ attestation matches+ provenance OR the mismatch diagnostic with exit 1.Test coverage
6 unit tests in
commands::attest::tests:sha256_hex_matches_known_vector— pins SHA-256 of"hello\n".build_and_write_roundtrip— manifest fields + on-disk JSON shape.verify_passes_when_hash_matches— golden path.verify_fails_when_binary_tampered— pins security: reproducible builds + binary attestation #504 cite in mismatch diagnostic.verify_fails_when_sidecar_missing— pins actionable "produced by--emit-attest" guidance.manifest_is_pretty_printed_json— readablegit diffinvariant.End-to-end smoke
Implementation note
First attempt hooked the emission next to the
Wrote executable:print, which fires BEFOREstrip. The smoke test caught this immediately when verify detected a 1.3 MB → 1.0 MB size shift between the linker output and the final on-disk binary. Final placement is between the strip pass and theBinary size:print — a comment in the source documents the placement for the next reader.What's NOT covered (MVP)
perry verify --attest-url <url>would fetch from a published source.Notes
No
Cargo.tomlversion bump, noCLAUDE.mdtouch, noCHANGELOG.mdentry — maintainer folds those in at merge time.