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. |
||
|---|---|---|
| .. | ||
| examples | ||
| action.yml | ||
| index.mjs | ||
| README.md | ||
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
- Masks the secret inputs (
::add-mask::) before anything else is printed. - Refuses if
source-pathhas uncommitted changes or untracked files. What is attested is the committed tree. - Builds the artifact as
npm packof the committed tree, written from the git object store, not from the working copy. A checkout withcore.autocrlf=true(CRLF in the working copy) gives the same bytes as one without. - 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) whensigning-keyis given. - 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.
- Only then, when
aere-api-keyis given, notarizes the statement hash on Aere Network (chain 2800) throughPOST 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. - 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 packcontains only the package's own files; - anything about packages that need a build step: the pack runs with
--ignore-scripts, soprepare/prepackscripts do not run and such packages are not supported by thenpm-packbuild 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) andnpmandgitonPATH. - 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 insdk-pq-sign/package-lock.jsonwithnpm 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.