TT Lab
Get started
Learn Learning paths Courses

The Build Was Green — So Who Put That Library In?

The signature checks out, but we never allowed that builder

Continue in TT Lab

Goal

You attach a provenance to a release bundle and sign it, then build as a program a gate that blocks bundles with insufficient evidence before deployment. You build three blocked cases yourself and confirm them.

Why it matters

A signature answers "who made it?" and an SBOM answers "what is in it?" But the sentence most often left in incident reports is none of the three — it is "nobody knows where these bytes came from". Provenance records the answer to that question at build time. What was used as input, from which commit, by which builder, and when it was made. And that record is the same as nonexistent if it is not read before deployment. That is what the gate does, and the value of a gate comes not from what it blocked but from whether a person can read why it blocked. Be sure to build the last case yourself — a bundle whose signature is right and whose digest is right, but whose builder you never allowed.

Steps

  1. Gather the artifact and the SBOM in /root/gate/release and write /root/gate/subject.json.
  2. Write an in-toto Statement to /root/gate/release/provenance.json.
  3. Add the digests of the materials and the build times.
  4. Create a key pair and sign to /root/gate/release/provenance.json.sig.
  5. Build the gate with /root/gate/allowed-builders.txt and /root/gate/release-gate.sh.
  6. Save the verdict for the normal bundle to /root/gate/06-pass.json.
  7. Create /root/gate/unsigned, /root/gate/tampered, and /root/gate/rogue and save them to /root/gate/07-reject.json.
  8. Record the four verdicts as an audit record in /root/gate/08-audit.jsonl.

Notes

Gather the release bundle in one place

Under /root/gate/release/, copy /opt/fixtures/sbom/release/paygate-1.4.2.js as paygate-1.4.2.js and /opt/fixtures/sbom/sbom/paygate-1.4.2.cdx.json as sbom.cdx.json. Then save the in-toto subject array to /root/gate/subject.json — it has one entry, where name is the file name and digest is {"sha256": the computed value}.

An attestation is always an attestation 'about something'. The subject is that 'something', and only if you point to it by digest rather than name can you catch a later swap.

Write what the build made from what

Save an in-toto Statement to /root/gate/release/provenance.json. _type is "https://in-toto.io/Statement/v1", subject is the array from step 1, and predicateType is "https://slsa.dev/provenance/v1". In predicate.buildDefinition, put buildType "https://labhub.example/buildtypes/npm-bundle/v1" and externalParameters, where externalParameters holds source (uri "git+https://git.internal/payments/paygate", digest {"gitCommit": "9f2c1d7a4b6e8035c1a2d4f6b8093e5a7c1d2f40"}) and entryPoint "npm run build". predicate.runDetails.builder.id is "https://labhub.example/builders/paygate-ci@v3".

buildDefinition and runDetails are required, and within them buildType, externalParameters, and builder are required. buildType is a URI because it is an address that points to 'how these fields should be read'.

Write the materials and the times

Add resolvedDependencies to predicate.buildDefinition in /root/gate/release/provenance.json. Each entry has uri and digest, the two materials are file:///opt/fixtures/sbom/release/package-lock.json and file:///opt/fixtures/sbom/sbom/paygate-1.4.2.cdx.json, and digest is {"sha256": the hash of that file}. List them in uri alphabetical order. Then write invocationId "build-2026-09-11-0007", startedOn "2026-09-11T03:40:00Z", and finishedOn "2026-09-11T03:52:00Z" in predicate.runDetails.metadata.

If you record digests for the materials, you can later ask 'if we rebuild from the same inputs, do we get the same thing?' If you write only names, you cannot ask that question.

Sign the attestation and put it in the bundle

Create a prime256v1 key pair at /root/gate/release-2026.key and /root/gate/release-2026.pub, sign /root/gate/release/provenance.json with that private key using SHA-256, and save the signature to /root/gate/release/provenance.json.sig.

An unsigned attestation is where the SLSA documentation places Build L1 — it prevents mistakes but not forgery. Only once a signature is attached can you ask 'was it touched after the build?'

Write the gate as a program

Write the allowed builders, one per line, in /root/gate/allowed-builders.txt (this time just https://labhub.example/builders/paygate-ci@v3), and create /root/gate/release-gate.sh. When called as bash /root/gate/release-gate.sh <묶음 디렉터리> (the argument is the bundle directory), it prints to standard output one JSON object containing bundle, verdict, and reasons, and exits with 1 if there is anything to block. It checks four things — whether the components in sbom.cdx.json are non-empty (sbom_missing if not), whether provenance.json.sig exists and verifies with /root/gate/release-2026.pub (signature_missing if absent, signature_invalid if wrong), whether the subject digest in provenance.json equals the actual hash of that file in the bundle (subject_mismatch if different), and whether builder.id is in the allowed list (builder_not_allowed if not). reasons holds them in alphabetical order with no duplicates.

The value of a gate comes not from 'what it blocked' but from 'whether a person can read why it blocked'. If you leave the reason as a code, dashboards and retrospectives both count by that code.

Confirm that the normal bundle passes

Run bash /root/gate/release-gate.sh /root/gate/release and save its output to /root/gate/06-pass.json. verdict must be pass and reasons must be an empty array.

If you test blocking first without confirming the passing case, even a gate that 'always blocks' looks successful.

Build three bundles with insufficient evidence and block them

Copy /root/gate/release to create /root/gate/unsigned (with only the signature file removed), /root/gate/tampered (with the contents of paygate-1.4.2.js changed), and /root/gate/rogue (with builder.id in provenance.json changed to "https://labhub.example/builders/laptop@v1" and re-signed with the same key). Put each of the three through the gate and save unsigned, tampered, and rogue to /root/gate/07-reject.json, where each value holds exit (the exit code) and reasons (the array of reasons the gate produced).

The third is the heart of this lab — the signature is sound and the digest is right, but we never allowed that builder. A gate that looks only at the signature lets this through.

Record the verdicts as an audit record

Record the four verdicts in /root/gate/08-audit.jsonl as one JSON object per line. The lines are in alphabetical order of bundle name (release, rogue, tampered, unsigned), and each line holds bundle (the directory name only), verdict, reasons, builder, artifact_sha256, and checked_at ("2026-09-11T04:00:00Z"). artifact_sha256 is the actual hash of paygate-1.4.2.js in that bundle.

The fact that the gate blocked something remains only on the screen at that moment. To ask later 'why did it block then?', you have to record the verdict together with its basis.