Skip to content

feat(security): #504 MVP — --emit-attest sidecar + perry verify --attest#988

Merged
proggeramlug merged 1 commit into
mainfrom
feat/504-binary-attest
May 18, 2026
Merged

feat(security): #504 MVP — --emit-attest sidecar + perry verify --attest#988
proggeramlug merged 1 commit into
mainfrom
feat/504-binary-attest

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Closes #504 (MVP — see follow-up matrix; full reproducible builds + CI publication tracked separately).

Summary

perry compile --emit-attest main.ts -o myapp now writes myapp.attest.json next 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:

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 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: 1 reserves 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_command driver after strip / 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)

  • 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=1 env → --emit-attest CLI.
  • New module commands::attest:
    • AttestationManifest with #[derive(Serialize, Deserialize)].
    • sha256_hex(path) — streaming hash (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 / write_attestation / verify_against_sidecar.

New CLI: perry verify --attest <binary>

The existing perry verify does remote runtime-verification via verify.perryts.com. The new --attest flag 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 — readable git diff invariant.

End-to-end smoke

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)

Implementation note

First attempt hooked the emission next to the Wrote executable: print, which fires BEFORE strip. 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 the Binary size: print — a comment in the source documents the placement for the next reader.

What's NOT covered (MVP)

  • Deterministic builds — same source on two machines doesn't yet produce a bit-identical binary. The attestation proves "this binary came from THIS specific build run", not "this is the only binary that could come from this source". Full determinism (stable LLVM pass ordering, fixed timestamps, hash-stable iteration) is the next security: reproducible builds + binary attestation #504 milestone.
  • CI attestation publication — sigstore / GitHub native attestations / signed bundles. The MVP ships the local primitive; CI publication layers on top.
  • Verifying against a published attestation — currently the sidecar must travel with the binary. A future perry verify --attest-url <url> would fetch from a published source.

Notes

No Cargo.toml version bump, no CLAUDE.md touch, no CHANGELOG.md entry — maintainer folds those in at merge time.

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.
@proggeramlug proggeramlug force-pushed the feat/504-binary-attest branch from f27b35b to f465f8a Compare May 18, 2026 10:35
@proggeramlug proggeramlug merged commit 91f2ee2 into main May 18, 2026
@proggeramlug proggeramlug deleted the feat/504-binary-attest branch May 18, 2026 10:35
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: reproducible builds + binary attestation

1 participant