Verifying AIWG Releases

Verifying AIWG Releases

AIWG ships four cryptographic verifications you can run on any release:

1. npm provenance attestation — proves a published tarball was produced by a specific GitHub Actions workflow run from a specific source commit. Lands on npmjs.org starting with the first release published via #1283 (A5, the GitHub Actions OIDC trusted-publishing path). 2. Signed git tag — proves the tag was created by a holder of the AIWG maintainer release key. Applies to every release tag from `cee91c96` (#1299 / A9, Wave 3 of #1278) forward. 3. Cosign tarball signature — proves the release tarball was produced by the AIWG GitHub Actions publish workflow, registry-independent. Verifiable offline against a Sigstore Rekor transparency-log entry embedded in the signature bundle. Applies to the first release published after #1287 (A8, Wave 5 of #1278) and forward. 4. CycloneDX SBOM — a registry-independent, signed software bill of materials disclosing exactly what shipped in the tarball, down to the transitive dep level. Applies to the first release published after #1288 (A13, Wave 6 of #1278) and forward.

This doc walks through all four verifications and shows what each one rules out.

Historical note: releases earlier than #1299 (`v2026.5.2` and prior) are

not retroactively signed. The signed-tag gate is forward-going only.

Provenance attestations and cosign tarball signatures are also forward-

going — releases before each control's first activation do not have

them.

Verification 1 — npm provenance attestation

What it proves

The published tarball was built by GitHub Actions workflow `.github/workflows/npm-publish.yml` on commit `<sha>` of the `github.com/jmagly/aiwg` repository, on a runner npmjs.org could cryptographically attest to. Independent of AIWG's claim — the chain of trust roots in npmjs.org's verification of GitHub's OIDC token, not in any AIWG-controlled credential.

How to verify

# Replace 2026.5.3 with the version you want to verify.
npm view [email protected] --json | jq .dist.attestations

Expected output: a non-empty JSON value under `.dist.attestations`. Recent npm CLI/registry versions may represent the attestation metadata as an object or an array. The important signal is that the published version has an attestation entry linking the tarball SHA to:

  • The GitHub Actions workflow file path (`.github/workflows/npm-publish.yml`)
  • The workflow run URL
  • The source commit SHA
  • The build environment (`runner.os`, action versions)

If the output is `null`, `[]`, or `{}`, the release does not have an OIDC-issued provenance attestation. Either it predates A5's first OIDC publish, or something went wrong with the trusted-publisher configuration. Treat the release with the same trust level you would treat a release from any registry without provenance — i.e., verify the signed tag below as the next-strongest signal.

Deeper verification

`npm` itself can validate registry signatures and provenance attestations for the dependency tree installed in your current project:

mkdir -p /tmp/aiwg-verify && cd /tmp/aiwg-verify
npm init -y >/dev/null
npm install --package-lock-only --ignore-scripts [email protected]
npm audit signatures --include-attestations

This builds a lockfile without running package lifecycle scripts, then asks npm to verify registry signatures and available provenance attestations for the downloaded packages. Output like `verified registry signatures, audited <N> packages` means npm accepted the signatures for that dependency tree.

What it does not prove

  • That the source commit was made by an AIWG maintainer. A push from a

compromised GitHub account could produce a valid attestation. Use verification 2 (signed tag) to close that gap.

  • That the tarball is functionally safe. Provenance is about *who built

what, not whether the result is malware-free*. Run AIWG through your own runtime sandbox if that matters to you.

Verification 2 — Signed git tag

What it proves

The annotated tag `v<version>` was created by a holder of the AIWG maintainer release-signing key. Independent of npmjs.org, GitHub, and Gitea — the chain of trust roots in the published maintainer key fingerprint in `SECURITY.md`.

How to verify

# Clone (or use an existing checkout) from a public mirror you trust the
# transport of. The signature verification doesn't trust the mirror itself.
git clone https://github.com/jmagly/aiwg.git
cd aiwg

# Import the published maintainer key.
gpg --import .gitea/keys/maintainers.asc
gpg --fingerprint [email protected]
# Confirm the fingerprint matches SECURITY.md:
# FE9272F0BC5781E1DE77FAAA719AB63879E84CE8

# Verify a specific tag.
git tag -v v2026.5.3

Expected output:

object <commit-sha>
type commit
tag v2026.5.3
tagger AIWG Release Signing <[email protected]> 2026-05-12 12:34:56 +0000

<tag annotation message>
gpg: Signature made <date>
gpg:                using RSA key <key-id>
gpg: Good signature from "AIWG Release Signing <[email protected]>" [ultimate]

The key line is `Good signature from`. The fingerprint `gpg` reports must match the fingerprint published in `SECURITY.md`. If the fingerprint differs, treat the tag as untrusted regardless of the "Good signature" line.

If the tag is signed with SSH instead of GPG

AIWG supports both GPG and SSH signing on the maintainer side. The `.gitea/allowed_signers` file (if present) holds the SSH allowed-signers for verification. The command becomes:

# Configure git to verify against the published allowed-signers file.
git config gpg.ssh.allowedSignersFile .gitea/allowed_signers
git tag -v v2026.5.3

Same `Good signature` expectation; the verifying key will be the SSH public key whose fingerprint is published in `SECURITY.md`.

What it does not prove

  • That the tag was created in CI versus on a maintainer's workstation.

The signing key signs the tag, not the environment that produced it. Use verification 1 (provenance) to close that gap on npmjs.org artifacts.

  • That the maintainer key itself is uncompromised. If the key leaks,

every tag signed with it before rotation is also implicated. Mitigated by AIWG's key-rotation procedure documented in `docs/contributing/versioning.md` and the operator-hygiene controls in `docs/contributing/secret-rotation.md`.

Verification 3 — Cosign tarball signature

What it proves

The specific tarball bytes you have on disk were produced by the AIWG GitHub Actions publish workflow at `.github/workflows/npm-publish.yml`, running on `github.com/jmagly/aiwg` at the tagged commit. Independent of both npmjs.org and the registry the tarball came from — works the same whether you pulled the file from npmjs.org, the Gitea bundled npm registry, the GitHub release, the Gitea release, or any mirror.

The chain of trust roots in Sigstore's Fulcio CA (which only signs keyless certificates against verified OIDC identities) and is anchored in the Rekor transparency log (which makes silent re-signing detectable by anyone watching).

How to verify

Required: `cosign` CLI v2.0+ — install from https://github.com/sigstore/cosign/releases.

Download the assets from either the GitHub release or the Gitea release for the version you want to verify. You need:

  • `aiwg-X.Y.Z.tgz` (the tarball itself)
  • `aiwg-X.Y.Z.tgz.sigstore` (the signature bundle)

Verify:

cosign verify-blob \
  --bundle aiwg-X.Y.Z.tgz.sigstore \
  --certificate-identity-regexp '^https://github.com/jmagly/aiwg/\.github/workflows/npm-publish\.yml@refs/tags/v' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  aiwg-X.Y.Z.tgz

Expected output: `Verified OK`.

The two `--certificate-*` flags are non-optional. They are what bind the signature to a specific workflow (`.github/workflows/npm-publish.yml` on `jmagly/aiwg`) and a specific OIDC issuer (GitHub Actions). Without them, `cosign verify-blob` would accept any keyless signature from any GitHub Actions workflow on any repo — defeating the point of the identity binding.

Cross-check the release manifest

Every release also ships a signed `release-manifest.json` containing the tarball SHA-256, the version, the tag SHA, the source commit SHA, and the workflow run URL. Verify it the same way:

cosign verify-blob \
  --bundle release-manifest.json.sigstore \
  --certificate-identity-regexp '^https://github.com/jmagly/aiwg/\.github/workflows/npm-publish\.yml@refs/tags/v' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  release-manifest.json

Then sanity-check that the manifest's `tarball_sha256` matches the actual tarball on disk:

jq -r '.tarball_sha256' release-manifest.json
sha256sum aiwg-X.Y.Z.tgz

The two values must match. If they don't, the manifest is signed for a different tarball than the one you have — treat the artifact as untrusted.

What it does not prove

  • That the source commit was made by an AIWG maintainer. The cosign

signature binds the tarball to a specific workflow run, not to a specific human author. Use verification 2 (signed tag) to close that gap.

  • That the tarball is functionally safe. The signature is about *who

built what, not whether the result is malware-free*. Run AIWG through your own runtime sandbox if that matters to you.

  • That the signing workflow itself is uncompromised. If an attacker can

push to `.github/workflows/npm-publish.yml` AND push a signed tag, they can produce a cosign-verifiable malicious tarball. The combined defense (signed tag + cosign signature) closes this — the attacker would need both Gitea/GitHub write access AND the maintainer release key AND a Sigstore-recognized OIDC identity on the AIWG repo.

Verification 4 — CycloneDX SBOM

What it proves

The signed SBOM discloses what AIWG ships at release time: the exact direct + transitive npm deps, their versions, and any non-npm components syft detected in the tree. The SBOM bytes are signed with the same keyless OIDC identity that signed the tarball (Verification 3), so the SBOM can be trusted to the same extent and via the same chain as the tarball itself.

This is composition disclosure, not malware detection — the SBOM tells you what's in the box, not whether it's safe to run. Pipe the SBOM into your SCA tool of choice (Grype, Trivy, Dependency-Track, Snyk, your internal scanner) for vulnerability assessment.

How to verify

Required: `cosign` CLI v2.0+ — same as Verification 3.

Download from either release:

  • `aiwg-X.Y.Z.cdx.json` (the SBOM itself, CycloneDX JSON format)
  • `aiwg-X.Y.Z.cdx.json.sigstore` (the signature bundle)

Verify the signature:

cosign verify-blob \
  --bundle aiwg-X.Y.Z.cdx.json.sigstore \
  --certificate-identity-regexp '^https://github.com/jmagly/aiwg/\.github/workflows/npm-publish\.yml@refs/tags/v' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  aiwg-X.Y.Z.cdx.json

Expected output: `Verified OK`.

Inspecting the SBOM

CycloneDX JSON is consumable by any standard SCA tool. A few quick inspections:

# Top-level metadata
jq '.metadata' aiwg-X.Y.Z.cdx.json

# Total component count
jq '.components | length' aiwg-X.Y.Z.cdx.json

# Component names + versions, sorted
jq -r '.components[] | "\(.name)@\(.version)"' aiwg-X.Y.Z.cdx.json | sort

Feeding into an SCA scanner

# Grype
grype sbom:aiwg-X.Y.Z.cdx.json

# Trivy
trivy sbom aiwg-X.Y.Z.cdx.json

# Dependency-Track (upload to your DT instance)
curl -X POST "https://dt.example.com/api/v1/bom" \
  -H "X-Api-Key: $DT_API_KEY" \
  -H "Content-Type: multipart/form-data" \
  -F "project=$PROJECT_UUID" \
  -F "[email protected]"

What it does not prove

  • That a deps in the SBOM is safe. An SBOM is a manifest, not a clean

bill of health. Run an SCA scanner against it.

  • That syft found everything. SBOM completeness depends on the

generator's ecosystem coverage. For AIWG (pure-npm + Go-binary tools), syft's coverage is comprehensive; for polyglot projects, gaps are possible. The signed manifest tells you what syft observed at release-build time, not what an exhaustive analysis would find.

  • That the SBOM matches the tarball byte-for-byte. The SBOM is generated

on the working tree before `npm publish`; we don't post-verify it against the published tarball contents. If you need that, recompute the SBOM yourself with `syft scan` against the downloaded tarball and diff against the signed one.

How the four verifications combine

What it catchesProvenance onlySigned tag onlyCosign signature onlySBOM onlyAll four
Forged tag from compromised maintainer accountNoYesNoNoYes
Tampered tarball uploaded directly to npmjs.orgYesNoYesPartialYes
Tampered tarball uploaded to Gitea bundled npm registryNoNoYesPartialYes
Tampered tarball on a third-party mirrorNoNoYesPartialYes
Workflow-injection attack via Gitea write accessPartialYesPartialPartialYes
Both Gitea write access AND maintainer key compromisedNoNoNoNoPartial (still needs Sigstore identity)
Build environment compromised mid-workflowPartialNoPartialNoPartial
Unknown dep present in tarballNoNoNoYesYes
Known-vulnerable dep present in tarballNoNoNoYes (via SCA scan)Yes

Run all four. Each rules out a different attacker. The audit (#1278) treats the combination as the supply-chain-defense baseline.

The signed tag is the only check that does not depend on GitHub-as-OIDC-issuer or npmjs.org-as-registry. It is the cryptographic anchor that makes the other two trustworthy even if a registry or OIDC provider is itself attacked.

What if a verification fails

1. Stop the install. Do not proceed with `npm install` or `git checkout` of the suspect artifact. 2. Confirm you have the right fingerprint. Re-fetch `SECURITY.md` from the canonical Gitea URL (`https://git.integrolabs.net/roctinam/aiwg/raw/branch/main/SECURITY.md`) and compare. If your local copy diverges from the canonical one, your local copy may be tampered. 3. Open a private report via the channel in `SECURITY.md` (`[email protected]`). Include the version, the verification that failed, the exact output, and how you obtained the artifact. 4. Do not file a public Gitea issue for a verification failure — that's exactly what `SECURITY.md`'s private channel is for.

Two-leg model: what gets verified where

AIWG publishes to two registries on every release. The signed tag and the cosign signature are both registry-independent, so the only verification that varies by registry is the npm provenance attestation:

RegistryURLProvenance?Signed tag?Cosign signature?SBOM?
npmjs.org`https://registry.npmjs.org/aiwg`Yes (post-A5)Yes (via the tag itself)Yes (post-A8, via GitHub/Gitea release assets)Yes (post-A13, via GitHub/Gitea release assets)
Gitea bundled npm`https://git.integrolabs.net/api/packages/roctinam/npm/aiwg`NoYesYes (post-A8, via GitHub/Gitea release assets)Yes (post-A13, via GitHub/Gitea release assets)

The signed tag and cosign signature are registry-independent — they verify the same way regardless of which registry you pulled the tarball from, or whether you pulled it from a mirror. The cosign signature specifically closes the "Gitea-registry tarball lacks a provenance chain" gap that #1286 (A10) covered with operator-hygiene controls only.

To verify a Gitea-registry install end-to-end: download the matching `.sigstore` bundle from the Gitea release, then run `cosign verify-blob` against the tarball that npm installed (typically at `$(npm root -g)/aiwg/` or wherever your global npm prefix points).

References