Verify a release yourself
This page shows how to confirm, yourself, that a downpipes release is what its channel entry says: built by CI from a specific tagged commit, and byte-identical from that build through the channel to what your account applied. At apply time your engine checks the channel signature and the signed digest automatically. This page is for the security team that wants to re-derive every step independently.
Check your release carries a provenance block
Every check below applies to a release whose channel entry carries a provenance block. Engine 0.3.10, the recommended release, carries one, and its four sidecars resolve. For a release without one, the console states “not published for this release” rather than linking to anything, and your console’s Provenance section shows what the release carries. The read-back gate runs in warn mode by default, recording evidence on every apply; set UPDATE_READBACK_MODE to enforce to make it refuse promotion.
What this proves, and what it does not
It proves that the bundle your engine applied is byte-for-byte the artefact CI built from the tagged source, and that the build is repeatable by anyone from that same source. It proves the platform held exactly those bytes at promotion only when the read-back verdict is “verified”. A hash of the returned module bytes that differs from the signed digest gives “mismatch”. A response that cannot be read gives “unavailable”. In the default warn mode the apply promotes and records the verdict; enforce mode refuses any verdict but “verified”. The pre-upload signed SHA-384 check and the post-promote canary still run.
It does not prove the source is benign. A defect or a backdoor written into the source would reproduce perfectly. Source review, which anyone can do against the published repository, is the separate assurance that covers that. Reproducibility and source review are different guarantees.
It does not prove real-time runtime execution. Cloudflare Workers, like every serverless platform, exposes no hardware attestation of the code running at a given instant. Your engine verifies the bytes at apply time, records the verdict in your own hash-chained audit trail, and re-compares the running deployment’s version identity against the last verified apply on every Security Centre evaluation (the update-version-drift check). Between those checks you are trusting the Cloudflare control plane and whoever holds access to your own account, the same trust every Worker there already carries.
What protects the release path
The steps below let you check a release yourself. This section explains what makes each check meaningful in the first place: the controls that stand between a source-code change and a bundle your engine will accept.
Getting a change into main. Every downpipes source repository requires a pull request and a green CI check before a change reaches main. There are no bypass actors on any repository ruleset, administrators included, so this applies uniformly regardless of who is proposing the change.
Signing the release tag. The maintainer signs each release tag with an SSH signing key held on a hardware security key: producing a signature needs the key’s PIN and a physical touch. The maintainer’s GitHub account and the repository’s allowed-signers file both list the signing key. The release workflow checks the tag’s signature against that file before it builds; a tag that does not verify never reaches step 1’s artefact.
Building reproducibly. The release workflow builds the artefact once. The build is deterministic: the artefact is a function of the tagged source, not of who built it or on what, so a rebuild from the tagged source on any machine gives the same bytes. Step 1 shows how to check that yourself.
Provenance, signing and transparency. Each release carries SLSA Build Level 3 provenance from the SLSA generator, a cosign keyless signature with a Rekor transparency-log entry, checksums, and a CycloneDX SBOM. Together these are what step 3 checks: what built the artefact, that it has not been altered since, and what it contains, all from public material rather than anything the vendor merely asserts.
Signing the update channel separately. The manifest your engine polls at update.downpipes.io is signed with a different key from the release tag: a hybrid Ed25519 plus ML-DSA-87 key whose secret is sealed to the same hardware security key used for release signing, using age encryption, with a physical touch required for every use. That secret is never stored in plaintext; an offline recovery copy is held under separate custody. Your engine verifies the manifest signature and pins the channel’s public key (step 2). This is why a compromised GitHub account, on its own, cannot push a malicious update to your engine: GitHub access lets someone merge code and cut a tag, but it does not hold the channel-signing secret, and a manifest built without that secret fails your engine’s pinned-key verification.
Compensating for a single maintainer. downpipes is maintained by a sole maintainer, so there is no second-person review gate on ordinary changes. The compensating controls are: no bypass actors on any ruleset, Dependabot alerts on every product repository, OpenSSF Scorecard on every product repository, Gitleaks and Semgrep scanning in CI, and a public verify route for issued documents.
Step 1: rebuild the artefact from source
The release artefact is a deterministic function of the tagged source: the same commit with its lockfile-pinned toolchain produces the same bytes wherever it is built.
git clone https://github.com/downpipes-io/engine && cd engine
git checkout vX.Y.Z
npm ci
node scripts/build-release.mjs
# prints { version, sha384, sha256, ... } and writes dist/engine-X.Y.Z.mjs
The sha384 printed here is the value to carry through every later step. The build refuses to run on a dirty tree or a stamped build-stamp.ts, so what you build is a function of the commit alone. If you would rather not rebuild the artefact yourself, start at step 2: everything from the signed channel onward verifies with public material alone.
Step 2: compare against the signed channel
The channel document your engine verifies is public. Fetch it and confirm the digest matches your rebuild:
curl -fsSL https://update.downpipes.io/stable.json
# components.engine.sha384 must equal your rebuilt sha384
The channel is signed with the pinned post-quantum hybrid key (Ed25519 plus ML-DSA-87, both halves required); your engine refuses any channel that does not verify under the pin your account holds. The provenance block inside the same signed body names the source commit, the release tag and the CI run, so the offline release key vouches for those pointers too. The same block names the Rekor transparency-log index when the publishing tool can read it from the cosign bundle. The tool reads the index from the bundle form that cosign v3 writes. For a channel entry with no index, the console does not show the Transparency log (Rekor) row.
Your engine fetches the channel only from update.downpipes.io. A channel URL on any other host is a configuration fault, and the engine requests nothing from it (engine/src/admin/updates.ts).
Step 3: verify the CI attestations
The attestation sidecars are republished on the channel host for any release whose channel entry carries a provenance block, so this step needs no access to our repositories. Read your own channel entry before fetching anything: if the entry for the version you are checking carries no provenance block, the sidecars were not published for that release and the paths below return 404. That is the state the warning above describes, not a tampering signal. Your console reports the same state as “not published for this release”.
The checksum file names the bundle as engine-X.Y.Z.mjs, with no folder, so the check runs inside the dist folder. Step 1 writes the bundle there. If you did not rebuild, download the bundle into a dist folder first.
BASE=https://update.downpipes.io/provenance/X.Y.Z
mkdir -p dist
# Only if step 1 did not write dist/engine-X.Y.Z.mjs.
# BUNDLE_URL is components.engine.url in the channel document from step 2.
curl -fsSL -o dist/engine-X.Y.Z.mjs "$BUNDLE_URL"
curl -fsSLO $BASE/engine.intoto.jsonl
curl -fsSLO $BASE/engine-X.Y.Z.mjs.cosign-bundle
curl -fsSLO $BASE/engine.SHA256SUMS.txt
curl -fsSLO $BASE/release-record.json
grep ' engine-X.Y.Z.mjs$' engine.SHA256SUMS.txt | (cd dist && shasum -a 256 -c -)
# engine-X.Y.Z.mjs: OK
The checksum command checks the bundle’s own line of the checksum file. It passes only when the output is engine-X.Y.Z.mjs: OK. Do not check the whole file with the --ignore-missing option. That option skips a listed file that is absent, so a bundle in another folder is not checked.
cosign verify-blob --bundle engine-X.Y.Z.mjs.cosign-bundle \
--certificate-identity-regexp '^https://github.com/downpipes-io/engine/\.github/workflows/release\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
dist/engine-X.Y.Z.mjs
slsa-verifier verify-artifact dist/engine-X.Y.Z.mjs \
--provenance-path engine.intoto.jsonl \
--source-uri github.com/downpipes-io/engine
The cosign identity pin means the signature is valid only if it was minted by the engine repository’s release workflow on a version tag; the keyless certificate and the signature are logged in the public Sigstore Rekor transparency log, so a signature served to you and hidden from everyone else is detectable.
Step 4: read your own account’s record
This is the step no vendor can do for you, and no vendor can fake after the fact. Open the console’s Licence and updates screen, Provenance section. It shows, from your engine’s own persisted records:
- the apply-time evidence: the signed digest your engine enforced at the last apply (recorded at promote time; the settled record and the hash-chained audit log both carry it) and the platform read-back verdict. it reads “verified”, “mismatch” or “unavailable”; in the default warn mode the apply promotes and records any of them, and enforce mode refuses any verdict but “verified”;
- an on-demand cross-check of your recorded digest against the published release record.
The Security Centre adds two standing checks: update-apply-provenance (the last apply left durable content-hash evidence) and update-version-drift (the running deployment is still the one the last verified apply left live; any out-of-band redeploy flips it). Both also ride into the signed evidence packs where a framework maps them: the drift check evidences ISO/IEC 27001 A.8.16 (monitoring activities), and the provenance check evidences NIS2 Article 21(2)(d) (supply chain security).
If any step fails
A mismatch at any step is what these checks exist to catch. Check first that you compared the same version (the channel moves; your account’s record names the version it applied). If the versions match and the digests do not, treat it as an incident: contact security@maelstrom.au, and keep the artefacts you fetched.
A 404 in step 3 is a different thing and is not an incident. It means the release you are checking was published without a provenance block, so there is nothing to fetch; your channel entry shows which state you are in. What still holds for such a release is the signed channel digest, which is what actually gates an apply, and your own account’s record of what it enforced.
Last updated .