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 pattern this tool already uses for git and docker: shell out to a real binary instead of importing its 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, since overwriting silently would strand every signature already made with the old key: a later verify against the new public key would read as tampered, not merely "signed with an old key".

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 a clear error naming --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

Reads the signature file to decide the mode: 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 is needed to say which mode a given .sig file is.

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 proves the report was signed by someone through a Fulcio-issued certificate logged in Sigstore's Rekor transparency log, not by any specific 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. Pinning a specific identity is left for a future task.

--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.