aere-proof-of-software/tools/proof-of-software/action/README.md
Aere Network 065f84e34a Aere Proof of Software 1.4.0: an attestation of what was built, from what, by whom and when, verifiable without trusting Aere Network
Every artifact's SHA-256 and size, the SBOM's digest, the git commit and tree of the source, a hybrid signature (classical + ML-DSA,
both required) and, when notarized, a first-seen time on Aere Network covered by the validators' post-quantum certificate. verify
recomputes everything from the files; --rebuild-from repeats an npm pack build from a clone the verifier chose; the SBOM is
re-derived from the committed package-lock.json or, new in 1.4.0, from the committed go.mod and go.sum; --signer requires the
signing keys you expect (1.4.0); a developer credential binds the keys to a person, judged against a trust root you choose. The
builder's declared date can only accuse, never acquit (1.4.0). Includes the GitHub Action and the hybrid signature library it uses.

Laid out as in the development repository (tools/proof-of-software/, sdk-pq-sign/, sdk/) so that nothing is rewritten for
publication. Tests and negative controls measured on 2026-09-29 are listed in README.md.
2026-09-29 22:46:57 +03:00

7.6 KiB

Aere Proof of Software: GitHub Action

Attests an npm package in CI so that anyone can check, without trusting the CI run, that the published .tgz is exactly what npm pack of a named git tree produces. It is a thin wrapper around tools/proof-of-software/pos.mjs (the same attest and verify --rebuild-from a stranger runs); the attestation logic is not copied into the action.

What a run does

  1. Masks the secret inputs (::add-mask::) before anything else is printed.
  2. Refuses if source-path has uncommitted changes or untracked files. What is attested is the committed tree.
  3. Builds the artifact as npm pack of the committed tree, written from the git object store, not from the working copy. A checkout with core.autocrlf=true (CRLF in the working copy) gives the same bytes as one without.
  4. Writes the attestation: artifact SHA-256 and size, commit, the git tree hash of source-path, the npm version, builder, time; signed with a hybrid signature (classical + ML-DSA, both required) when signing-key is given.
  5. Verifies it by rebuilding: clones the commit into a clean directory, packs the attested tree again and requires the attested digest byte for byte. The step fails if it is not reproduced.
  6. Only then, when aere-api-key is given, notarizes the statement hash on Aere Network (chain 2800) through POST https://cloud.aere.network/v1/notarize. Anything but HTTP 200 with a transaction hash and a block fails the step; redirects are not followed. Without a key nothing is sent, and the step summary says the attestation is not notarized.
  7. Reads the attestation file back from disk, verifies it again, then writes the outputs and a step summary.

Usage

- uses: actions/checkout@v4
- uses: actions/setup-node@v4
  with: { node-version: 22 }
- id: pos
  uses: <owner>/<repo>/tools/proof-of-software/action@<full-commit-sha>
  with:
    source-path: packages/mylib
    signing-key: ${{ secrets.AERE_POS_SIGNING_KEY }}   # optional
    aere-api-key: ${{ secrets.AERE_API_KEY }}           # optional
- uses: actions/upload-artifact@v4
  with:
    name: proof-of-software
    path: |
      ${{ steps.pos.outputs.attestation-path }}
      ${{ steps.pos.outputs.artifact-path }}      

A complete workflow is in examples/attest-npm-package.yml. Pin the action to a full commit SHA. If you publish to npm, publish the attested file itself (npm publish <artifact-path>), not a new pack: a new pack is a new build and the attestation does not cover it.

The signing key is the file written by node tools/proof-of-software/pos.mjs keygen --out keys.json (JSON, or base64 of it). Generate it once, store it as a repository or environment secret, and publish only its public keys.

Inputs

input required default meaning
source-path yes directory of the npm package, a subdirectory of the repository
build no npm-pack the build to record and repeat; only npm-pack is supported
signing-key no secret: key file from pos.mjs keygen; signs the statement (hybrid)
aere-api-key no secret: Aere Cloud API key; notarizes the statement hash after the rebuild check
verify-rebuild no true rebuild from a clean clone of the commit and require the same bytes
out-dir no $RUNNER_TEMP/aere-proof-of-software where the .tgz and the attestation are written (not inside source-path)

Outputs

output set when value
attestation-path always on success path of the attestation JSON
artifact-path always on success path of the attested .tgz
artifact-sha256 always on success SHA-256 of the artifact, 0x hex
tree always on success git tree hash of source-path at the attested commit
statement-hash always on success SHA-256 of the statement text: what is signed and notarized
notarized-tx notarized only transaction hash on chain 2800
proof-url notarized only https://cloud.aere.network/v1/proof/<statement-hash>

On failure no output is written.

Secrets

Secret inputs are registered with ::add-mask:: before any other output. Nothing the action prints (log lines, errors, step summary, outputs) contains them: every message passes through a filter that removes the exact secret values, and messages that come from libraries or tools are additionally stripped of long hex strings. A signing key that does not parse is refused without quoting any of it. The API key is sent only to https://cloud.aere.network; the only other base the code accepts is a loopback address, which exists for the local tests.

Verifying an attestation yourself

You need the attestation file, the artifact, your own clone of the attested repository, and pos.mjs (Node 18 or later; run npm ci once in sdk-pq-sign/ next to it):

node tools/proof-of-software/pos.mjs verify attestation.json --rebuild-from /path/to/your/clone mylib-1.4.2.tgz

It checks the artifact digest, the statement hash, the hybrid signature if present, that <commit>:<path> in your clone is the attested tree, and it packs that tree again and requires the attested digest. It prints VALID and exits 0 only when every present claim holds; what is absent (no signature, no notarization) is reported as absent. With an Aere Cloud key (--cloud-key-file) it also reads the notarization proof and its finality from the chain.

What it proves, and what it does not

It proves that the artifact is byte for byte what npm pack --ignore-scripts of the named git tree produces (anyone can repeat that), that the statement was signed by the holder of the signing key (if signed), and that the statement hash existed on chain 2800 no later than the notarization block (if notarized).

It does not prove:

  • that the source is safe, correct or free of malicious code; only that the artifact comes from that tree;
  • who wrote or reviewed the commit; git authorship is not verified;
  • that the signing key was used only by your CI; anyone holding the secret can sign;
  • anything about the package's dependencies: npm pack contains only the package's own files;
  • anything about packages that need a build step: the pack runs with --ignore-scripts, so prepare/prepack scripts do not run and such packages are not supported by the npm-pack build yet;
  • reproducibility with another npm major version or operating system: the npm version is recorded in the statement so that a mismatch can be told apart from tampering, but it has not been measured across versions.

Requirements and limits

  • A runner with Node 24 for the action itself (runs.using: node24) and npm and git on PATH.
  • The package must be in a subdirectory of the repository; a package at the repository root is refused by this version.
  • When the action's own checkout lacks the dependencies of sdk-pq-sign, it installs the versions pinned in sdk-pq-sign/package-lock.json with npm ci --ignore-scripts (integrity-checked), which needs access to the npm registry.
  • The verification clone is a full local clone of the checkout; its time grows with the size of the repository.

Tests

node tools/proof-of-software/test/action.test.mjs

The tests run the action the way a runner does (inputs in INPUT_*, GITHUB_OUTPUT and GITHUB_STEP_SUMMARY in files, GITHUB_WORKSPACE on a scratch git repository) and send notarizations to a local HTTP server on 127.0.0.1. Each negative control requires its named reason: an untracked file, HTTP 403, a changed artifact byte, an artifact that does not come from the tree, and planted secrets that must not appear in any output.