Build, sign and reject the wrong release
Node.js 22 or newer. No dependencies, account, network access or signing service.

Reference solution: not included here, so the exercise does not hand you the answer.
Attempt the repair first. Then download release-verification-solution.zip from the project card
on https://softwaresecurity.com/labs/#project-release-verification and extract it into this folder;
it adds reference.mjs without changing your files, and the reference commands below
need it.

Outcome
Build an executable artifact, produce a small CycloneDX 1.6 SBOM, sign a build
provenance statement with a real Ed25519 key in a DSSE envelope, and repair the
consuming release gate. A signed build can still be the wrong build. Your deliverable is a verifier
plus a table connecting every trust decision to a failing regression test.

Prerequisites and time
Know SHA-256, public/private keys, JSON and Node imports. Allow 45–75 minutes.
The .example.test identities are deliberate local fixtures, not real services.

Files and trust boundary
app-source.mjs -> build.mjs -> artifact + SBOM + signed provenance
                                     |
consumer policy + trusted public key -> verifyRelease -> ACCEPT or REJECT
The provenance is an in-toto Statement v1 with a SLSA build-provenance v1
predicate, wrapped in a DSSE v1.0 envelope, the format in-toto attestations,
SLSA generators and Sigstore bundles use:
  {"payloadType": "application/vnd.in-toto+json",
   "payload": base64(statement bytes),
   "signatures": [{"keyid": hint, "sig": base64(signature)}]}
The key signs PAE(payloadType, payload), the DSSE pre-authentication encoding
  "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body
not the bare payload, so a signature cannot be replayed as another document
type. build.mjs implements pae(), signing and verification; exercise.test.mjs
checks pae() against the DSSE specification's own test vector. The envelope
contains no trusted key, and the keyid is only a hint: the verifier tries every
signature against the key it already trusts. This is not a Sigstore bundle:
there is no certificate, transparency log entry or timestamp.
The build type is this lab's copy-plus-banner process, defined in build.mjs;
its example URI is an identifier, not a hosted standard build type.

1. Build something you can inspect
   node build.mjs
   node verify.mjs
   Open release/invoice-app.mjs, release/sbom.cdx.json and
   release/provenance.intoto.jsonl (one DSSE envelope per line). Decode its
   payload with: node -e "const e=JSON.parse(require('fs').readFileSync('release/provenance.intoto.jsonl','utf8'));console.log(Buffer.from(e.payload,'base64').toString())"
   build.mjs reads the actual source, prepends a fixed banner, hashes the shipped
   bytes, generates the inventory, and signs subjects for BOTH artifact and SBOM.
   The application has no third-party packages. The SBOM records its application
   and included source file; it is not an OS, runtime or transitive-dependency scan.
   A fresh temporary key is generated each build; only its public half is saved.

2. Observe why success is insufficient
   node --test exercise.test.mjs
   Expect 10 passing and 10 failing tests in the unmodified starter. The intended
   release, wrong key, changed signature bytes, changed artifact, malformed JSON
   and the five DSSE envelope checks already pass, because build.mjs verifies the
   envelope. But a genuine
   signer can sign evidence for a fork, an older run, a different source or a
   different builder. A valid signature cannot supply your release policy.
   Read the failing test for the fork. It deliberately re-signs the incorrect
   claim. This distinguishes authenticity from authorization, rather than merely
   testing that broken signatures fail.

3. Define the decision before writing the repair
   Record expected artifact/SBOM names, digests, builder, build type, source URI,
   source digest, and invocation ID. The expected values come from policy.json-like
   consumer configuration (policy in build.mjs), never the candidate release.
   In this toy distribution, build.mjs also emits consumer-policy.json and a key
   for convenient inspection. A real consumer MUST acquire those through a
   separately administered trusted channel. Downloading all three from an
   untrusted release defeats the trust boundary.
   Decide whether unknown external parameters are allowed. This lab's build type
   has exactly one: repository. Unknown flags are rejected, since a new flag might
   disable a check. That rule is specific to external parameters; the lab does
   not impose a general rule rejecting every extension in the SLSA format.

4. Repair exercise.mjs
   Authenticate first. Enforce the statement/predicate versions. Require unique
   exact subjects for artifact and SBOM, then compare their SHA-256 hashes.
   Verify builder, build type, exact parameters, source digest and invocation.
   Finally check that the signed SBOM actually names and hashes this application.
   Fail closed on missing/invalid structures. Do not catch a failure and accept.
   Repeat node --test exercise.test.mjs until all 20 cases pass.

5. Compare and investigate
   macOS/Linux: EXERCISE_MODULE=reference node --test exercise.test.mjs
   PowerShell: $env:EXERCISE_MODULE='reference'; node --test exercise.test.mjs
   Clear that environment variable before testing your own code.
   node verify.mjs --reference
   Expected: ACCEPT: signature, content and release policy agree.
   Change a byte in release/invoice-app.mjs and run verify again. Expected: REJECT.
   Rebuild to restore the demo. Do not sign the changed bytes merely to make the
   failure disappear: the consumer still needs to decide whether they are allowed.

Worked reasoning
The tampered-artifact case fails because the signature authenticates an old
hash; the bytes no longer match. The wrong-builder case has a perfectly valid
signature AND matching artifact hash; it fails because the signer asserted an
unapproved builder. The wrong-SBOM case binds the SBOM's bytes but shows why
checking an envelope alone is insufficient: that inventory must describe the
same artifact. Duplicate subjects are denied under this narrow two-subject
profile instead of letting one match mask another conflicting claim.

Completion criteria
All 20 tests pass; your table separates signature, content identity, build policy
and inventory binding. Explain who can rotate the trusted key, how the next
expected invocation is authorized, and what evidence prevents a compromised
signer from lying. Those controls are intentionally outside the executable lab.

Troubleshooting and cleanup
Run in the extracted project directory; missing release files mean build.mjs has
not run. A fresh build changes the key: pair the newly generated public key with
its envelope. Node does not need OpenSSL CLI or npm installation. Remove only this
project's release/ directory to clean up; no servers or global state are created.

Limits
This is real cryptographic verification, not a production signing platform or a
claim of any SLSA level. Builder isolation, reproducible execution, trustworthy
key distribution, key revocation, transparency logs, freshness windows and full
schema validation require separate designs. The consumer profile is purposely
narrow; it does not accept arbitrary CycloneDX/SLSA documents. The producer and
consumer live in one project so you can see the mechanism; they are not isolated.

Primary references
https://nodejs.org/api/crypto.html#cryptosignalgorithm-data-key-callback
https://nodejs.org/api/crypto.html#cryptoverifyalgorithm-data-key-signature-callback
https://github.com/secure-systems-lab/dsse/blob/master/protocol.md
https://github.com/in-toto/attestation/blob/main/spec/v1/envelope.md
https://slsa.dev/spec/v1.2/build-provenance
https://cyclonedx.org/docs/1.6/json/

Dev container
.devcontainer/devcontainer.json opens this folder in VS Code Dev Containers or
GitHub Codespaces using the Dev Containers Node.js 24 image, pinned by digest.
Run the commands above in its terminal.
