> ## Documentation Index
> Fetch the complete documentation index at: https://docs.etalon.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Signing qualification evidence

> Ed25519 signatures over hashes.json.

# Signing qualification evidence

This page is part of the [operator guide](/docs/introduction.md). The commands are in the
[CLI reference](/docs/cli.md).

Etalon can sign an evidence bundle with Ed25519. The signature is evidence that
the hash manifest was produced by the holder of a seed. It does not determine
that a regulation, contract, or internal control has been met.

The runner version is `0.0.1`. Unsigned bundles from earlier runs still
verify.

## What is signed

`etalon sign` signs the raw bytes of `hashes.json` as they are stored on disk.
That file lists every evidence file and its SHA-256 digest. The pack summary
is one of those files, and it records the pack content hash, so the signature
binds the pack identity that the run evaluated. The verifier does not
re-serialize `hashes.json` before checking.

`signature.json` is written next to the bundle. It is not listed in
`hashes.json`. A signature cannot cover a file that contains the signature.
Any other extra file in the run directory still fails verification.

The signature document records version 1, algorithm `ed25519`, the payload
name `hashes.json`, the SHA-256 of those bytes, the public key, and the
64-byte signature as hex.

## Keys

The algorithm is Ed25519 (RFC 8032). A private key is a 32-byte seed. A
public key is 32 bytes. Implementation is the Python standard library in
`etalon/evidence/ed25519.py`. It is not a general-purpose cryptography library.
The arithmetic is not written to resist side channels. Use it to sign
evidence on the operator machine.

Generate a pair:

```bash theme={null}
etalon keygen --private etalon-private.json --public etalon-public.json
```

`etalon keygen` refuses to overwrite an existing file. The private file is
created with mode `0600`. Both files are JSON.

Private file, purpose `etalon-evidence-sign`:

```json theme={null}
{
  "algorithm": "ed25519",
  "public_key": "<64 hex characters>",
  "purpose": "etalon-evidence-sign",
  "seed": "<64 hex characters>"
}
```

Public file, purpose `etalon-evidence-verify`:

```json theme={null}
{
  "algorithm": "ed25519",
  "public_key": "<64 hex characters>",
  "purpose": "etalon-evidence-verify"
}
```

How to hold them:

* The seed is not encrypted. Protect the private file with filesystem
  permissions and by keeping it off shared machines.
* Do not commit the private file. Do not put it in a bundle, a log, or a CI
  secret that the job prints.
* Do not pass the private file to `etalon verify` or to the GitHub Action.
  Those commands reject a file that contains a seed.
* The public file is what auditors and the deployment gate receive. It can
  live in the deploy repository.
* There is no key server, registry, or rotation service. To change keys,
  generate a new pair and sign new bundles. Older bundles keep verifying
  with the public key that signed them.
* A signature check that uses only the public key embedded in
  `signature.json` detects edits that were not re-signed. It does not
  identify the holder of the seed. Pass the public key from your own files
  when the signer matters.

## Sign and verify with the runner

Signing refuses a bundle that fails hash, fingerprint, or manifest checks.

```bash theme={null}
etalon sign runs/<run-id> --key etalon-private.json
etalon verify runs/<run-id>
etalon verify runs/<run-id> --public-key etalon-public.json --require-signature
```

`etalon verify` without a public key still checks hashes. If `signature.json`
is present, it must verify under the embedded key. `--public-key` requires a
signature and requires that key. `--require-signature` fails closed when the
signature file is absent.

Exit 0 means the check passed. Exit 1 means it did not.

These edits fail a signed bundle:

* a hashed file changes and `hashes.json` is left as it was
* a hashed file changes and `hashes.json` is rewritten to the new digest
  without a new signature
* `signature.json` is replaced by a signature from a different seed while
  verification uses the original public key

## Verify without installing Etalon

`examples/verify_bundle.py` checks the file set, the SHA-256 digests, manifest
decision agreement, and the Ed25519 signature. It loads
`etalon/evidence/ed25519.py` by path. It does not import the Etalon package, so
it does not need the runner's dependencies and it does not use the network.

Keep this layout, or an equivalent checkout:

```text theme={null}
examples/verify_bundle.py
etalon/evidence/ed25519.py
```

```bash theme={null}
python3 examples/verify_bundle.py runs/<run-id> \
  --public-key etalon-public.json \
  --require-signature
```

The public key is required. Exit 0 is a match. Exit 1 is a failed check.
Exit 2 means the public key file could not be read.

`etalon verify` also checks the fingerprint against the fingerprint schema.
`etalon sign` refuses to sign a bundle that fails that check. The air-gapped
script then checks that those signed bytes are unchanged. It does not
re-implement the fingerprint schema.

## Deployment gate

`.github/actions/etalon-verify` runs the air-gapped script. The step fails when
the bundle path is missing, the public key path is missing, or verification
exits non-zero. Later deploy steps do not run after that failure.

```yaml theme={null}
- uses: actions/checkout@v4
- uses: coyos-ai/etalon/.github/actions/etalon-verify@main
  with:
    bundle: evidence/run
    public-key: etalon-public.json
```

`.github/workflows/etalon-evidence-gate.yml` is the same check, triggered only
by `workflow_dispatch` or `workflow_call`. It is not part of this
repository's pull-request test workflow.

The gate checks a qualification evidence bundle. It does not decide that a
deployment meets a regulation or a contract.

## Auditor export

```bash theme={null}
etalon export runs/<run-id> --public-key etalon-public.json --output auditor/
```

The command writes a directory. It does not modify the bundle.

* `auditor-report.html` — decision, fingerprint, coverage statement, judge
  and calibration summary when a judge ran, hashes, and signature status
* `manifest.json` — the same record, plus the SHA-256 of the HTML file

Coverage text states that realised coverage is value-presence and is not a
cross-product of coverage cells. The export does not copy raw case inputs or
outputs. When the bundle fails verification, the package still records the
failure and the command exits 1.

The statement on the package is: this is qualification evidence for human
review. It does not determine that a regulation, contract, or internal
control has been met.

## Out of scope

Passphrase encryption of the seed, a key registry, signing a pack directory
as its own artifact, and a hosted verification service are not part of this
version. Pack identity is already inside the signed hash manifest.
