Signing & attest
Sign a JSON report so it can later be checked for tampering, check a signed report, print a commit trailer, and write a shields.io badge.
Signing modes
Two modes, both under verify --sign. local
(the default: bare --sign or --sign=local)
uses an ed25519 keypair generated by paranoid attest
keygen and never touches the network. --sign=keyless
shells out to the cosign CLI to run Sigstore's
keyless signing flow, the same way the tool runs git and docker, without importing an SDK. There is no Sigstore Go dependency. Keyless
signing does use the network, and, outside a recognized CI OIDC
environment, needs an interactive browser login; it only runs
when you pass --sign=keyless.
--sign requires --format json and
--output: the signature covers exactly the bytes
written to --output, so there is always a file on
disk whose bytes match what was signed. It writes
<output>.sig next to it (and, for keyless,
<output>.pem, the signing certificate).
Generating a local key
$ paranoid attest keygen
wrote /home/you/.config/paranoid/attest.key and /home/you/.config/paranoid/attest.key.pub
Writes attest.key (private, hex-encoded ed25519
seed) and attest.key.pub (public), both permission
0600, to ${XDG_CONFIG_HOME:-~/.config}/paranoid.
--force overwrites an existing key; without it,
keygen refuses to run if a key already exists there, because overwriting the key would make every signature already made with the old key fail verification.
Signing a report
$ paranoid verify --format json --output report.json --sign
$ ls report.json report.json.sig
For --sign=keyless, cosign also needs to be on
PATH; without it, --sign=keyless fails with an error that names --sign=local as the
offline alternative.
paranoid attest verify
$ paranoid attest verify --report report.json --sig report.json.sig
valid: local ed25519, signed 2026-08-26T12:00:00Z, key fingerprint 3f2a9c1d7b4e6081
The mode is read from the signature file: a local signature
is a small, self-describing JSON envelope (mode
"local"); anything else is treated as a keyless
signature, checked by shelling out to cosign
verify-blob. No flag selects the mode.
| Flag | Default | Meaning |
|---|---|---|
| --report string | required | The report file to check |
| --sig string | required | The signature file written by verify --sign |
| --key string | attest.key.pub in the config directory | Public key file for a local signature |
| --cert string | the .pem file next to --sig | Certificate file for a keyless signature |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Signature is valid |
| 2 | Signature is invalid |
| 3 | Could not attempt verification (missing or unreadable file, malformed signature, cosign not on PATH for a keyless signature) |
For a local signature, exit code 2 always means the report bytes
do not match the signature and key: the report was edited after
signing, or the wrong public key was used. For a keyless
signature, exit code 2 means cosign's own
verify-blob refused it; cosign's exit code does not
distinguish a genuinely invalid signature from an operational
failure on its side (a network hiccup, a Sigstore outage), so
read the printed reason text to tell those apart.
Identity is not pinned
Keyless verification checks
--certificate-identity-regexp .* and
--certificate-oidc-issuer-regexp .*: it shows that someone signed the report through a Fulcio-issued certificate logged in Sigstore's Rekor transparency log, but not who (person, email, or CI job). The signer info line on a
successful verification reads the certificate's own identity
(email, else URL, else subject) for display only; that is not an
access-control decision. Identity pinning is not implemented.
--trailer
Prints Audited-by: paranoid score=NN verdict=V as
the last line on stdout, meant to go into a commit trailer:
$ paranoid verify --trailer
...
Audited-by: paranoid score=92 verdict=pass
$ git commit --trailer "$(paranoid verify --trailer | tail -n1)" -m "message"
--badge <file>
Writes a shields.io endpoint badge as JSON, deterministically: the same score and verdict always produce the same bytes.
$ paranoid verify --badge badge.json
$ cat badge.json
{
"schemaVersion": 1,
"label": "paranoid",
"message": "score 92",
"color": "brightgreen"
}
Color follows the verdict: brightgreen for pass,
yellow for warn, red for fail. Host the
file wherever your CI already publishes artifacts and point a
shields.io endpoint badge URL at it.
Where the signature lives
A signature is always a sidecar file next to the report, never
part of the report's own JSON. The report schema
(schema_version) is unchanged by signing.