paranoid_
Trust

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.

FlagDefaultMeaning
--report stringrequiredThe report file to check
--sig stringrequiredThe signature file written by verify --sign
--key stringattest.key.pub in the config directoryPublic key file for a local signature
--cert stringthe .pem file next to --sigCertificate file for a keyless signature

Exit codes

CodeMeaning
0Signature is valid
2Signature is invalid
3Could 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.