Re-vendoring the native PSI prebuild
The PSI addon @openmined/psi.js is not installed from a registry. It is built by the fork georgetown-mdi/OpenMinedPSI and copied into this repository as lib/openmined-psi.js-<version>.tgz, a file: dependency. This is the procedure for replacing those bytes, and the steps a reviewer performs to establish where they came from.
The tarball contains native .node addons that are loaded into the process with full privilege and run the PSI crypto, so its chain of custody is the point of this document, not a formality.
The two controls
| Control | What it proves | What it needs |
|---|---|---|
lib/<tarball>.sha256 | The bytes in the tree are the bytes the commit intended: no truncated checkout, corrupt download, or half-finished re-vendor | sha256sum alone -- no network, no token |
lib/<tarball>.provenance.json plus gh attestation verify | The bytes came out of a named workflow run in the fork, at a named commit | Network, a GitHub token, and gh |
The sidecar cannot do the provenance job: it is committed alongside the bytes it describes, so whoever writes one writes the other. The attestation lives in the fork's attestation store, outside any Alcove commit, which is why it closes the hop the sidecar cannot. Field-by-field format, the exact verifier invocation, and what arms it: DEPENDENCY_PINS.md.
Arming state
Verification is armed per vendored tarball by attestation_expected in its provenance marker.
- Disarmed (
false): CI reports the state as a warning annotation on every run and passes. The sidecar is the whole control. - Armed (
true): CI runsgh attestation verifyagainst the recorded fork repository, signer workflow, and source commit, and any non-zero exit fails the build.
The marker itself is not optional in either state. CI fails if it is missing or malformed, and it holds the recorded digest against the tarball's real bytes whichever way the switch is set. Turning verification off is therefore a visible true -> false edit in a tracked file, not a deletion that looks like nothing.
Arming requires the producing workflow to attest the artifact. The fork's native-prebuilds.yml does: its package job runs actions/attest-build-provenance under id-token: write and attestations: write, ahead of the artifact upload, so a tarball is published only once its provenance was signed. A tarball packed by a run older than that step has nothing to verify, and its marker stays disarmed.
Procedure
Run the fork's Native prebuilds workflow (workflow_dispatch) and note the run's commit. Then, from a branch in this repository:
Download the tarball from the run's
openmined-psi-tarballartifact intolib/, and delete the tarball it replaces along with its.sha256and.provenance.json. Give a rebuild its ownseclinksuffix --2.0.6-seclink.4rather than a second2.0.6-seclink.3-- so byte-distinct tarballs never share a version string: it costs one digit and removes the condition step 5 has to work around, though what guarantees the new bytes install is the lockfile integrity that step brings onto them, not this naming.Verify provenance before anything else records the new digest. This ordering matters: a sidecar written first is a hash over unverified bytes.
shgh attestation verify lib/openmined-psi.js-<version>.tgz \ --repo georgetown-mdi/OpenMinedPSI \ --signer-workflow georgetown-mdi/OpenMinedPSI/.github/workflows/native-prebuilds.yml \ --source-ref refs/heads/master \ --source-digest <fork commit the run built> \ --predicate-type https://slsa.dev/provenance/v1 \ --deny-self-hosted-runnersThe exit code is the result: 0 means verified. With its output captured rather than sent to a terminal, a successful run prints nothing, neither the policy it enforced nor the attestation that matched. To keep a record of what verified, add
--format json, which writes the matched attestation and its verification result to stdout when captured, and save that file with the pull request's evidence.If the fork does not attest yet, this returns HTTP 404 for the digest. That is the disarmed case: record it in the pull request and leave the marker's
attestation_expectedatfalse.Write the provenance marker
lib/<tarball>.provenance.json, with the new digest, the producer identity above, and -- when step 2 succeeded --attestation_expected: truewith thesource_refandsource_digestthat verified. Arming without those two is refused.Regenerate the sidecar, now over verified bytes:
shsha256sum lib/openmined-psi.js-<version>.tgz > lib/openmined-psi.js-<version>.tgz.sha256Write it with the path in the form
sha256sum -cis run against, so the committed line matches the CI invocation's working directory.Update the lockfile's integrity for the tarball.
package-lock.jsonpins the vendored tarball by sha512 undernode_modules/@openmined/psi.js, and npm resolves afile:dependency out of its content-addressed cache by that value rather than by re-readinglib/. Left stale, a warm cache installs the bytes being replaced while every other control passes, so this is the step that decides which addon actually loads. Compute the new value and edit that one field:shecho "sha512-$(openssl dgst -sha512 -binary lib/openmined-psi.js-<version>.tgz | openssl base64 -A)"npm run test:scriptsfails while that field and the committed tarball disagree, naming both values, so a skipped or mistyped edit is caught here rather than in whatever the next warm-cache install loads.Then force the reinstall, which npm otherwise skips when the version string did not change:
shrm -rf node_modules/@openmined/psi.js npm installConfirm the installed addon is the new bytes rather than assuming it, since this is the failure the ordering above cannot catch:
shref="$(mktemp -d)" tar -xzf lib/openmined-psi.js-<version>.tgz -C "$ref" diff -r "$ref/package/prebuilds" node_modules/@openmined/psi.js/prebuildsA cold cache fails loudly instead --
npm error code EINTEGRITY, naming the wanted and got sha512 -- but that is the cold-cache case only, and the cache this install reads is your own machine's, warm with the bytes being replaced: a skipped edit leaves the lockfile's key unchanged, so the old bytes install with no error at all. Thediff -rabove is the confirmation that does not depend on cache state. CI does not stand in for it: the setup action's dependency cache is keyed on the vendored tarball's bytes, so a re-vendor misses that cache and installs, but what it installs is what the lockfile names.Run the checks locally.
shsha256sum -c lib/openmined-psi.js-*.tgz.sha256 npm run check:prebuild-provenance npx vitest run --project repo-scripts scripts/verify-prebuild-provenance.test.mjs npx vitest run --project repo-scripts scripts/vendored-psi-deps.test.mjs npm run build -w packages/core && npm testOpen the pull request stating the fork run URL, the fork commit, the new digest, and the arming state. A rebuild that touches the native crypto is crypto-code review scope (CONTRIBUTING.md).
What the reviewer does
The point of the marker is that a third party can reproduce the chain of custody from the pull request alone, without trusting the person who ran the procedure. Against the branch:
Confirm the bytes are the bytes described.
sha256sum -c lib/openmined-psi.js-*.tgz.sha256, and check that the digest in the diff's.sha256and.provenance.jsonare the same value.Confirm the lockfile pins the same bytes.
package-lock.json'sintegrityfornode_modules/@openmined/psi.jsmust be the sha512 of the tarball in the diff. It is what npm installs from, so a stale value leaves the sidecar and the attestation describing bytes that never load. CI'snpm run test:scriptsholds that pair against each other offline, so this step is reading a green check rather than hashing the tarball by hand.Verify provenance yourself, by running
npm run check:prebuild-provenanceor thegh attestation verifyinvocation above. The verifier reaches pastapi.github.com, fetching the Sigstore bundle from its blob host and the trust root from the TUF CDN, so a host fenced from either -- a locked-down dev container among them -- fails closed. Run it where that egress exists, or read CI's own run of the check on the pull request; a red result from a fenced host says nothing about the attestation. The check names an egress or credential failure as itself, rather than as the no-attestation conclusion, wherever the verifier's own output shows one; that recognition is best effort, so an unrecognized failure is reported as the lookup's answer, and the verifier's own output, printed above the check's line, stays the authority on the cause. Do not read the CI result as a substitute while the marker is disarmed: a disarmed check reports rather than verifies, and says so in its annotation.Check the identity the marker pins, not just that verification passed.
producer_repositoryandsigner_workflowmust name the fork and itsnative-prebuilds.yml; a marker pointing at any other repository or workflow is a finding regardless of whether the attestation validates against it.Tie the source commit to reviewable source.
source_digestmust be a commit that exists in the fork and whose tree is the source the addon is meant to be built from. This is the step that connects an attested build to code someone read; the attestation itself claims only that the run happened.Read an arming change as a change. A diff that flips
attestation_expectedfromtruetofalseremoves the provenance control for that artifact, and needs the same explanation as removing any other security check.
First armed run
The positive path has run end to end: the fork attests, and npm run check:prebuild-provenance passes against a real attestation rather than only in its failing direction. Two things still need watching on the pull request that arms a marker:
The verifier passes in CI, not only locally, which only CI's own run can exercise. CI authenticates with the workflow's
GITHUB_TOKENagainst the fork's attestations, which is a different credential from a maintainer'sghlogin. The fork's attestation endpoint answers an unauthenticated request while both repositories are public, so any token with public read suffices; a fork turned private would be where this breaks.The
--source-refrecorded matches what the fork's default branch is called. It is checked independently of--source-digest: an attestation issued on a branch fails arefs/heads/masterverification even when the two refs point at the same commit (a fast-forwarded branch run still fails this way even though its commit already sits onmaster), and the failure looks like tampering.
If either surprises, the correct response is to leave the marker disarmed and fix the mismatch rather than to weaken the invocation.