🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@graneth/verify

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@graneth/verify - npm Package Compare versions

Comparing version
0.1.1
to
0.2.0
+37
-3
dist/cliCore.d.ts

@@ -6,2 +6,19 @@ /**

* wires this up to real fs + process.exit.
*
* ── WHY THIS FILE HAS THREE OUTCOMES AND NOT TWO ────────────────────────────
* This is the one thing we ship that runs on the *recipient's* machine. An
* agency forwards a receipt to its DACH client; the client runs this; whatever
* this prints is what the agency then has to explain.
*
* It used to print FAIL for "I forgot --key", for "the key file isn't where I
* said", for "the receipt didn't download properly", and for "this document
* was altered after signing" — one word for a mistake in the command and for
* an accusation of forgery. The first four are true of thousands of correct
* receipts. Printing the same word for them spends the credibility the receipt
* exists to earn, on the machine where we are least able to explain ourselves.
*
* So: PASS / FAIL / MALFORMED / UNCHECKED, as a word for the person AND as an
* exit code for the script, because in CI nobody reads the word. Which of the
* last two a document gets is decided by `loadReceipt` / `readKeyDocument`
* below — see the note there; it is a security boundary, not a taxonomy.
*/

@@ -14,2 +31,19 @@ export interface CliIO {

}
/**
* The contract a script may depend on.
*
* 0 is reserved: it means a signature was checked and is valid, and it means
* nothing else. Everything that is not a verdict — including `--help` — is 2,
* so no invocation that failed to check anything can be mistaken for one that
* did.
*
* 2 and 3 differ by WHOSE fault blocked the check, which decides whether the
* reader may be reassured: 2 is the reader's side, 3 is the document's.
*/
export declare const EXIT: {
readonly pass: 0;
readonly fail: 1;
readonly unchecked: 2;
readonly malformed: 3;
};
/** Runs the CLI logic against injected I/O. Returns the process exit code. Never throws. */

@@ -42,6 +76,6 @@ export declare function runVerifyCli(argv: string[], io: CliIO): number;

*
* The exit code stays 0. The signature IS valid, scripts key on that, and
* changing the contract to express a display concern would break automation
* to fix prose.
* This is a DIFFERENT AXIS from PASS/FAIL/UNCHECKED: it describes the coverage
* of a signature that verified. The exit code stays 0, because the signature
* is valid and scripts key on that.
*/
export declare function describeAttestation(attestation: unknown): string[];

@@ -6,4 +6,98 @@ /**

* wires this up to real fs + process.exit.
*
* ── WHY THIS FILE HAS THREE OUTCOMES AND NOT TWO ────────────────────────────
* This is the one thing we ship that runs on the *recipient's* machine. An
* agency forwards a receipt to its DACH client; the client runs this; whatever
* this prints is what the agency then has to explain.
*
* It used to print FAIL for "I forgot --key", for "the key file isn't where I
* said", for "the receipt didn't download properly", and for "this document
* was altered after signing" — one word for a mistake in the command and for
* an accusation of forgery. The first four are true of thousands of correct
* receipts. Printing the same word for them spends the credibility the receipt
* exists to earn, on the machine where we are least able to explain ourselves.
*
* So: PASS / FAIL / MALFORMED / UNCHECKED, as a word for the person AND as an
* exit code for the script, because in CI nobody reads the word. Which of the
* last two a document gets is decided by `loadReceipt` / `readKeyDocument`
* below — see the note there; it is a security boundary, not a taxonomy.
*/
import { verifyReceipt } from "./index.js";
/**
* The contract a script may depend on.
*
* 0 is reserved: it means a signature was checked and is valid, and it means
* nothing else. Everything that is not a verdict — including `--help` — is 2,
* so no invocation that failed to check anything can be mistaken for one that
* did.
*
* 2 and 3 differ by WHOSE fault blocked the check, which decides whether the
* reader may be reassured: 2 is the reader's side, 3 is the document's.
*/
export const EXIT = { pass: 0, fail: 1, unchecked: 2, malformed: 3 };
const KEY_DOWNLOAD_HINT = "Download it once with:\n" +
" curl -o graneth-key.json https://graneth.com/.well-known/graneth-attestation-key.json\n" +
"graneth-verify is an offline tool and will not fetch it for you.";
const HELP = `graneth-verify — offline check of a Graneth governance receipt's Ed25519 signature.
Usage:
graneth-verify <receipt.json> --key <key.json>
Options:
-k, --key <path> public key to check against: either Graneth's published key
document or a raw SPKI PEM file
-h, --help show this text
Get the key once (this tool makes no network calls of its own):
curl -o graneth-key.json https://graneth.com/.well-known/graneth-attestation-key.json
Exit codes:
0 PASS the signature was checked, and it is valid
1 FAIL the signature was checked, and the file does not match it:
the document was altered after it was signed
2 UNCHECKED nothing was checked, and the obstacle is on YOUR side — a
missing or wrong key, an unreadable file, a mistake in the
command. This one says nothing about the receipt.
3 MALFORMED this is not a well-formed Graneth receipt: no signature, or a
signed field removed since it was issued. Not the same as
UNCHECKED, and not to be treated as evidence.
Only 0 means verified. \`--help\` exits 2 for the same reason: exit 0 is reserved
for a receipt that actually verified.`;
const USAGE_HINT = "Usage: graneth-verify <receipt.json> --key <key.json>\n" +
"Run `graneth-verify --help` for the exit codes.";
/**
* Reports that no check happened, in a form that cannot be read as a verdict.
* The leading sentence is the one that saves the argument on the client's
* machine, so it is printed before the cause, every time.
*/
function unchecked(io, cause, hint) {
// "remains unverified" is load-bearing: without it the reassurance on the
// second half reads as an all-clear, which it is not — nothing was checked.
io.error("UNCHECKED — no signature was checked, so this receipt remains unverified.");
io.error(` ${cause}`);
if (hint)
for (const line of hint.split("\n"))
io.error(` ${line}`);
io.error(" The obstacle is on this side, not in the receipt's contents.");
return EXIT.unchecked;
}
/**
* The document is not a well-formed receipt. Deliberately carries none of the
* reassurance UNCHECKED carries: the case this exists for is a signed field
* being REMOVED, and telling that reader "this says nothing about the receipt"
* would be false at exactly the moment it matters.
*/
function malformed(io, cause) {
io.error("MALFORMED — this is not a well-formed Graneth receipt, and it has not been shown to be genuine.");
io.error(` ${cause}`);
io.error(" Do not treat it as evidence. Ask the sender for the receipt as it was issued.");
return EXIT.malformed;
}
/**
* Flags are recognised as flags. Previously anything unrecognised became the
* receipt path, so `graneth-verify --help` looked for a file called `--help`
* and reported FAIL — the tool's own help flag printed the word it prints for
* a forged document.
*/
function parseArgs(argv) {

@@ -13,14 +107,25 @@ const out = {};

const a = argv[i];
if (a === "--key" || a === "-k") {
out.keyPath = argv[++i];
if (a === "--help" || a === "-h") {
out.help = true;
}
else if (a === "--key" || a === "-k") {
const value = argv[++i];
if (value === undefined)
return { usageError: `${a} needs a path to a key file` };
out.keyPath = value;
}
else if (a.startsWith("-") && a !== "-") {
return { usageError: `unknown option: ${a}` };
}
else if (!out.receiptPath) {
out.receiptPath = a;
}
else {
// Checking the first and ignoring the rest would report on less than the
// caller believes it reported on.
return { usageError: `this tool checks one receipt at a time, and got two: ${out.receiptPath} and ${a}` };
}
}
return out;
}
const KEY_DOWNLOAD_HINT = "Download it once with:\n" +
" curl -o graneth-key.json https://graneth.com/.well-known/graneth-attestation-key.json\n" +
"graneth-verify is an offline tool and will not fetch it for you.";
/**

@@ -54,4 +159,4 @@ * Accepts either the JSON key document served at

return {
error: `no published key matches the receipt's keyId ${wantedKeyId}. The signing key may have been ` +
"rotated without republishing that key document — download a fresh copy, or contact the issuer.",
error: `your key document has no key matching the receipt's keyId ${wantedKeyId}. The signing key may have been ` +
"rotated since you downloaded it — download a fresh copy, or contact the issuer.",
};

@@ -66,2 +171,15 @@ }

}
/**
* Which published key to try — read ONLY from the signed object.
*
* This runs BEFORE verifyReceipt classifies the document, so it is the earlier
* of the two places an unsigned field could steer the verdict. Falling back to
* the envelope's `keyId` (which no signature covers) let a stripped receipt
* reach "your key document has no key matching…" — an UNCHECKED, reassuring
* message — without the removal of the signed field ever being reported.
*
* With no signed keyId, no key is requested, `resolvePublicKeyPem` hands back
* the current one, and the document reaches the malformed check that is meant
* to catch it.
*/
function receiptKeyId(receipt) {

@@ -74,43 +192,102 @@ const attestation = receipt.attestation;

}
const topLevel = receipt.keyId;
return typeof topLevel === "string" ? topLevel : undefined;
return undefined;
}
/**
* ── THE LINE, DRAWN ONCE ────────────────────────────────────────────────────
* These two loaders exist so the reader's-fault / document's-fault split is a
* choice made in ONE place per artifact, instead of a judgement re-made at every
* error site. It had to be re-made three times, and got it wrong three times.
*
* Before the tool holds bytes claimed to be a receipt — no path, no file, no
* key, a mistyped command — the obstacle is the READER'S, and UNCHECKED's
* reassurance is true.
*
* Once it holds those bytes, a defect IN them is the DOCUMENT'S, and it is
* MALFORMED. Not because we know who damaged them: because one corrupted byte
* is indistinguishable, from in here, from a file that was edited — and the
* person who edits the file is the one who would benefit from the doubt.
*
* The key stays the reader's artifact end to end, so every defect in it —
* unreadable, not JSON, no matching key — is UNCHECKED, and that stays true.
* The default side is the one whose sentence does not lie under either reading:
* "this is not a well-formed Graneth receipt" is honest even when the reader
* simply pointed at the wrong file.
*/
function loadReceipt(io, path) {
let text;
try {
text = io.readFile(path);
}
catch (err) {
// Reading failed — a directory, a permission, a vanished file. No bytes
// were ever claimed to be a receipt, so this is still the reader's side.
return unchecked(io, `receipt file could not be read: ${err.message}`);
}
try {
return { receipt: JSON.parse(text) };
}
catch (err) {
return malformed(io, `this file is not valid JSON, so it is not a receipt: ${err.message}. ` +
"It may have been truncated or edited — ask the sender for the receipt as it was issued");
}
}
/** The key is the reader's own artifact: every way it can be wrong is UNCHECKED. */
function readKeyDocument(io, path) {
try {
return io.readFile(path);
}
catch (err) {
return unchecked(io, `key file could not be read: ${err.message}`, KEY_DOWNLOAD_HINT);
}
}
/** Runs the CLI logic against injected I/O. Returns the process exit code. Never throws. */
export function runVerifyCli(argv, io) {
const { receiptPath, keyPath } = parseArgs(argv);
const { receiptPath, keyPath, help, usageError } = parseArgs(argv);
if (help) {
io.log(HELP);
return EXIT.unchecked;
}
if (usageError) {
return unchecked(io, usageError, USAGE_HINT);
}
if (!receiptPath) {
io.error("Usage: graneth-verify <receipt.json> --key <key.json>");
return 2;
return unchecked(io, "no receipt file given.", USAGE_HINT);
}
if (!io.exists(receiptPath)) {
io.error(`FAIL — receipt file not found: ${receiptPath}`);
return 1;
return unchecked(io, `receipt file not found: ${receiptPath}`);
}
let receipt;
try {
receipt = JSON.parse(io.readFile(receiptPath));
}
catch (err) {
io.error(`FAIL — receipt file is not valid JSON: ${err.message}`);
return 1;
}
const loaded = loadReceipt(io, receiptPath);
if (typeof loaded === "number")
return loaded;
const receipt = loaded.receipt;
const receiptObj = receipt && typeof receipt === "object" ? receipt : {};
if (!keyPath) {
io.error(`FAIL — no public key supplied. Pass --key <path-to-key.json>.\n${KEY_DOWNLOAD_HINT}`);
return 1;
return unchecked(io, "no public key supplied. Pass --key <path-to-key.json>.", KEY_DOWNLOAD_HINT);
}
if (!io.exists(keyPath)) {
io.error(`FAIL — key file not found: ${keyPath}\n${KEY_DOWNLOAD_HINT}`);
return 1;
return unchecked(io, `key file not found: ${keyPath}`, KEY_DOWNLOAD_HINT);
}
const resolved = resolvePublicKeyPem(io.readFile(keyPath), receiptKeyId(receiptObj));
const keyText = readKeyDocument(io, keyPath);
if (typeof keyText === "number")
return keyText;
const resolved = resolvePublicKeyPem(keyText, receiptKeyId(receiptObj));
if ("error" in resolved) {
io.error(`FAIL — ${resolved.error}`);
return 1;
return unchecked(io, resolved.error);
}
const result = verifyReceipt(receipt, resolved.pem);
if (!result.valid) {
io.error(`FAIL — ${result.reason}`);
return 1;
return report(io, verifyReceipt(receipt, resolved.pem), receiptObj);
}
/** Turns the one verdict into the one word and the one exit code it owns. */
function report(io, result, receiptObj) {
if (result.outcome === "unverifiable") {
return unchecked(io, result.reason ?? "the check could not be completed.");
}
if (result.outcome === "malformed") {
return malformed(io, result.reason ?? "this document is not a well-formed receipt.");
}
if (result.outcome === "mismatch") {
io.error("FAIL — this file does not match the signature it carries.");
io.error(` ${result.reason}`);
io.error(" Do not rely on it. Ask the sender for the receipt as it was issued.");
return EXIT.fail;
}
io.log("PASS — Ed25519 signature is valid.");

@@ -120,3 +297,3 @@ for (const line of describeAttestation(receiptObj.attestation))

io.log(` keyId: ${result.keyId}`);
return 0;
return EXIT.pass;
}

@@ -155,5 +332,5 @@ /** Fields a reader expects a governance receipt to attest to, and their labels. */

*
* The exit code stays 0. The signature IS valid, scripts key on that, and
* changing the contract to express a display concern would break automation
* to fix prose.
* This is a DIFFERENT AXIS from PASS/FAIL/UNCHECKED: it describes the coverage
* of a signature that verified. The exit code stays 0, because the signature
* is valid and scripts key on that.
*/

@@ -160,0 +337,0 @@ export function describeAttestation(attestation) {

@@ -49,7 +49,37 @@ /**

}
/**
* Which of three things happened — NOT two.
*
* `valid: false` used to carry both "the cryptography ran and disagreed" and
* "the cryptography never ran", and a caller had no way to tell them apart
* short of string-matching `reason`. Those are opposite statements: the first
* accuses the document, the second describes the verifier's own inputs. A
* verifier that cannot say which one it means is the failure mode this whole
* package exists to argue against.
*
* - `valid` — checked, and the signature matches.
* - `mismatch` — checked, and it does not. The signed bytes changed after
* signing.
* - `malformed` — this is not a well-formed Graneth receipt: no signature,
* no attestation, or an attestation with a signed field
* removed. The DOCUMENT is at fault, and it has not been
* shown to be genuine.
* - `unverifiable` — no check took place, and the obstacle is on the reader's
* side: no key, the wrong key, an unreadable file. This one
* — and only this one — says nothing about the receipt.
*
* The line between the last two is load-bearing, not taxonomy. `unverifiable`
* is the state a reader may be reassured about, so whoever can steer a document
* INTO it can choose the sentence a reader sees. Nothing about a document's own
* contents is allowed to land there.
*/
export type VerifyOutcome = "valid" | "mismatch" | "malformed" | "unverifiable";
export interface VerifyResult {
/** `outcome === "valid"`. Unchanged meaning — existing callers keep working. */
valid: boolean;
/** Which of the three states this is. Prefer this over `!valid`. */
outcome: VerifyOutcome;
/** keyId derived from the public key you supplied. Compare it yourself if you don't trust `valid` alone. */
keyId: string;
/** Present only when valid is false: why. */
/** Present unless valid: why. */
reason?: string;

@@ -56,0 +86,0 @@ }

@@ -53,6 +53,99 @@ /**

}
function fail(keyId, reason) {
return { valid: false, keyId, reason };
/**
* No check took place, and the obstacle is on the READER'S side: no key, the
* wrong key, a file we could not read, an algorithm this build does not
* implement. Nothing here is a statement about the receipt's contents — which
* is exactly why nothing that IS about its contents may land in this state.
*/
function unverifiable(keyId, reason) {
return { valid: false, outcome: "unverifiable", keyId, reason };
}
/** The check ran and the signature does not cover these bytes. The only accusation. */
function mismatch(keyId, reason) {
return { valid: false, outcome: "mismatch", keyId, reason };
}
/**
* The document is not a well-formed Graneth receipt. Distinct from
* `unverifiable` because the fault is the FILE'S, and a reader must not be
* reassured about it — see the note on `signedKeyId` for who gets to choose
* which of those two a reader sees.
*/
function malformed(keyId, reason) {
return { valid: false, outcome: "malformed", keyId, reason };
}
/**
* Which key the receipt says signed it — read ONLY from the signed object.
*
* ── WHY THE ENVELOPE IS NOT CONSULTED ───────────────────────────────────────
* `keyId` sits inside the signed object (attestationKey.ts:117 —
* `SignedAttestationObject` requires it, and `buildSignedObject` always sets
* it), so it is covered by the signature. The copy in the receipt envelope is
* not covered by anything, and reading it here would hand an editor of the
* file the choice of which verdict the reader sees: strip the signed keyId,
* write a foreign one into the envelope, and the tool reports "this is not the
* key that signed it" — a reassuring sentence, chosen by the person who
* altered the document.
*
* The envelope copy is still fine for *selecting* a key to try, because a bad
* selection can only end in a non-passing state. It is not fine for deciding
* what to tell the reader.
*/
function signedKeyId(so) {
return typeof so.keyId === "string" ? so.keyId : undefined;
}
/**
* Does this object present itself as a Graneth attestation? A genuine v1 always
* carries version, algorithm AND keyId together; an object claiming the first
* two while missing the third has had a signed field removed.
*/
function claimsToBeAnAttestation(so) {
return so.version !== undefined || so.algorithm !== undefined;
}
/**
* Everything a document must be before running cryptography on it is worth the
* electricity. Split out of verifyReceipt so the accusing branch is the only
* thing left in that function that decides an accusation.
*/
function prepare(receipt, keyId) {
const defect = shapeDefect(receipt, keyId);
if (defect)
return defect;
const r = receipt;
const signedObject = r.attestation;
const so = signedObject;
const declaredAlgorithm = r.algorithm ?? so.algorithm;
if (declaredAlgorithm && declaredAlgorithm !== "ed25519") {
return unverifiable(keyId, `this build only checks ed25519, and the receipt declares ${declaredAlgorithm}`);
}
const declaredKeyId = signedKeyId(so);
if (declaredKeyId && declaredKeyId !== keyId) {
return unverifiable(keyId, `this is not the key that signed the receipt — it was signed with keyId ${declaredKeyId}, and you supplied ${keyId}. ` +
"Download the current key document, which also lists retired keys.");
}
return { signedObject, signature: r.signature, declaredKeyId };
}
/**
* The ways a file can fail to be a receipt at all. Every one of these is the
* DOCUMENT's defect, so none of them may be reported as "we could not check".
*/
function shapeDefect(receipt, keyId) {
if (!receipt || typeof receipt !== "object") {
return malformed(keyId, "this is not a JSON object, so it is not a receipt");
}
const r = receipt;
const signedObject = r.attestation;
if (!signedObject || typeof signedObject !== "object") {
return malformed(keyId, "there is no signed `attestation` object here — this is not a Graneth receipt");
}
if (typeof r.signature !== "string" || r.signature.length === 0) {
return malformed(keyId, "this document carries no `signature` — it has never been signed by anyone");
}
const so = signedObject;
if (!signedKeyId(so) && claimsToBeAnAttestation(so)) {
return malformed(keyId, "the signed object declares a version and an algorithm but names no keyId. Every receipt Graneth issues " +
"names its signing key INSIDE the signature — so this field has been removed since it was issued");
}
return null;
}
/**
* Verify a Graneth governance receipt against a public key, entirely offline.

@@ -75,24 +168,8 @@ *

catch (err) {
return fail("", `invalid public key PEM: ${err.message}`);
return unverifiable("", `the public key you supplied is not usable: ${err.message}`);
}
if (!receipt || typeof receipt !== "object") {
return fail(keyId, "receipt is not a JSON object");
}
const r = receipt;
const signedObject = r.attestation;
if (!signedObject || typeof signedObject !== "object") {
return fail(keyId, "receipt is missing the signed `attestation` object");
}
const signature = r.signature;
if (typeof signature !== "string" || signature.length === 0) {
return fail(keyId, "receipt is missing the `signature` field");
}
const so = signedObject;
const declaredAlgorithm = r.algorithm ?? so.algorithm;
if (declaredAlgorithm && declaredAlgorithm !== "ed25519") {
return fail(keyId, `unsupported algorithm: ${declaredAlgorithm}`);
}
if (typeof so.keyId === "string" && so.keyId !== keyId) {
return fail(keyId, `public key does not match — receipt was signed with keyId ${so.keyId}, supplied key is ${keyId}`);
}
const prepared = prepare(receipt, keyId);
if ("outcome" in prepared)
return prepared;
const { signedObject, signature, declaredKeyId } = prepared;
const canonical = canonicalize(signedObject);

@@ -104,8 +181,21 @@ let signatureOk = false;

catch (err) {
return fail(keyId, `verification threw: ${err.message}`);
return unverifiable(keyId, `the check could not be run: ${err.message}`);
}
if (!signatureOk) {
return fail(keyId, "Ed25519 signature does not verify against the supplied public key — receipt is forged or tampered");
/**
* A mismatch means one of two things, and only a declared keyId tells them
* apart: the bytes changed after signing, or this is simply the wrong key.
*
* Reaching here without one means the object does not even claim to be an
* attestation (the claiming-but-nameless case is malformed, above). It is
* still the document's defect, not ours — an unsigned-by-anyone blob whose
* signature does not verify is not something we "could not check".
*/
if (!declaredKeyId) {
return malformed(keyId, "the signature does not match, and this document names no signing key at all — " +
"there is nothing here that could be attributed to Graneth");
}
return mismatch(keyId, "the signed bytes are not the bytes in this file — it was altered after it was signed");
}
return { valid: true, keyId };
return { valid: true, outcome: "valid", keyId };
}
+1
-1
{
"name": "@graneth/verify",
"version": "0.1.1",
"version": "0.2.0",
"description": "Offline verifier for Graneth's signed governance receipts — checks the Ed25519 signature against a public key you already have. Zero network calls, zero runtime dependencies.",

@@ -5,0 +5,0 @@ "type": "module",

@@ -18,8 +18,60 @@ # @graneth/verify

`--key` accepts either that key document or a raw PEM public key. Exits 0 on a
valid signature, 1 otherwise, with a human-readable reason.
`--key` accepts either that key document or a raw PEM public key.
## Four outcomes, not two
| Exit | Word | What it means |
| --- | --- | --- |
| `0` | `PASS` | The signature was checked, and it is valid. |
| `1` | `FAIL` | The signature was checked, and the file does not match it — the document was altered after it was signed. |
| `2` | `UNCHECKED` | Nothing was checked, and the obstacle is on **your** side: a missing or wrong key, an unreadable file, a mistake in the command. This one says nothing about the receipt. |
| `3` | `MALFORMED` | This is not a well-formed Graneth receipt — no signature, or a signed field removed since it was issued. Not evidence of anything. |
Only `0` means verified.
Forgetting `--key`, pointing at a key file that isn't there, or using a key
document downloaded before the signing key was rotated are all mistakes on the
*reader's* side — and a verifier that answers them with the same word it uses for
a forgery is telling the reader something false about a receipt that may be
perfectly good.
`2` and `3` are separated for the opposite reason, and the line between them is
load-bearing. `UNCHECKED` is the one state a reader is explicitly reassured
about, so anyone who can steer a document *into* it gets to choose the sentence
the reader sees.
The line is drawn once, by artifact rather than by case:
> **Before** the tool holds bytes claimed to be a receipt — no path, no file, an
> unreachable URL, a mistyped command — the obstacle is **yours**, and
> `UNCHECKED` is honest. **Once** it holds those bytes, a defect *in* them is the
> **document's**, and it is `MALFORMED`.
Not because we can tell who damaged the bytes: because one corrupted byte is
indistinguishable, from in here, from an edited one — and the person editing a
receipt is the one who benefits from the doubt. The default side is the one
whose sentence is not a lie under either reading, and *"this is not a
well-formed Graneth receipt"* is true even when you simply pointed at the wrong
file.
Your copy of the **key** stays your artifact end to end, so every defect in it —
unreadable, not JSON, no matching key — is `UNCHECKED`. The same unparseable
file is therefore `MALFORMED` when passed as a receipt and `UNCHECKED` when
passed with `--key`.
Two consequences of the same rule: every receipt Graneth issues names its
signing key **inside** the signature, so a document declaring a version and an
algorithm but no `keyId` has had that field removed and is `MALFORMED`; and the
copy of `keyId` in the receipt envelope is never consulted for a verdict,
because no signature covers it.
`--help` also exits `2`, because exit `0` is reserved for a receipt that verified.
Programmatically, `verifyReceipt()` returns `outcome: "valid" | "mismatch" |
"malformed" | "unverifiable"` alongside the original `valid` boolean, which still
means exactly `outcome === "valid"`.
## What a PASS tells you — and what it does not
```
```text
PASS — Ed25519 signature is valid.

@@ -35,3 +87,3 @@ scan #4821 acme-corp/payments-api

```
```text
PASS — Ed25519 signature is valid.

@@ -47,2 +99,5 @@ scan #4821

`NOT ATTESTED` is a different axis from PASS/FAIL/UNCHECKED: it describes what a
*valid* signature covers, not whether a check happened.
Programmatic use: `import { verifyReceipt } from "@graneth/verify"`.

@@ -49,0 +104,0 @@