Signed evidence for payment assessments: digests, ed25519 and offline verification

A security report gets forwarded, attached and archived. By the time an auditor reads it, the only useful question is whether they can check it themselves. Here is how Settlens makes that possible.

In short

A SHA-256 digest only shows that a report was not edited carelessly, because anyone who edits it can recompute the digest. Settlens adds an ed25519 attestation that binds the digest to a signing key and a time; an auditor verifies both offline and pins the assessor's public key to reject forgeries.

On this page

A payment security assessment rarely stays with the people who ran it. The report goes to a partner who wants to integrate, a customer's security team, a procurement questionnaire, an auditor a year later. On the way it becomes an email attachment, a PDF, a file in a shared drive. Whoever finally reads it has three questions, and none of them is answered by the report looking official:

  • Is this the report the tool produced, or has something been changed since?
  • Who stands behind it?
  • What exactly does it cover: which endpoint, which tool version, which mode, when?

Settlens answers them in two layers: a deterministic SHA-256 digest inside every JSON report, and an optional ed25519 attestation that binds that digest to a signing key and a time. Both verify offline. This article explains how each layer works, shows real commands and output, and is explicit about what each layer does not prove.

Why a checkmark is not evidence

Turning a failing result into a passing one takes seconds in a text editor. The demo target in the Settlens repository has a route that serves paid content for an invalid signature, a critical payment bypass. Its report says so. Below, we edit that report so the critical failure reads as a pass and see which verification catches the edit.

There is also a structural problem. When an operator assesses its own payment system, the party that benefits from a clean result is the party producing it. That does not make self-assessment dishonest, but it means a reader should be able to check the result instead of trusting it. Evidence is useful to the extent that someone else can verify it without asking you.

The assessor sends the report and its attestation; the auditor needs neither network access nor the assessor's help to check them, only the assessor's public key from a trusted channel.

Layer 1: the integrity digest

Every JSON report Settlens writes ends with an integrity block. The digest is computed over the whole report except the block itself, after canonicalization: the JSON is re-serialized with object keys sorted by UTF-16 code unit and arrays kept in order, with no whitespace. Settlens calls that format settlens-stable-json-v2 and records the name in the block; reports from Settlens 0.4.0 and earlier used settlens-stable-json-v1, which still verifies. The result is hashed with SHA-256.

JSONTop of a Settlens JSON report (targets elided)
{
  "schemaVersion": 1,
  "reportId": "a92a9d60",
  "product": "Settlens",
  "productVersion": "0.5.0",
  "protocol": "x402",
  "protocolVersion": 2,
  "integrations": { "officialX402Core": "2.28.0" },
  "generatedAt": "2026-10-13T11:13:35.213Z",
  "scope": {
    "targetCount": 1,
    "activeTargetCount": 0,
    "settlementCalls": 0,
    "privateKeysUsed": 0
  },
  "metadata": { "source": "cli" },
  "summary": { "pass": 13, "fail": 0, "warn": 0, "skip": 0 },
  "targets": [ … ],
  "integrity": {
    "algorithm": "sha256",
    "canonicalization": "settlens-stable-json-v2",
    "digest": "455b6f3e15b4886bda9862bb7148b5852691a4f3daaed1ed04464d125acb5b3f"
  }
}

Canonicalization makes the digest independent of formatting: pretty-printing the file or reordering its keys does not change it, while changing any value does. Note what else the report states about its own scope: the tool and @x402/core versions, how many targets ran in active mode, and that it made zero settlement calls and used zero private keys. Reports also leave out the resource's response body (only its size and SHA-256 are kept) and request header values, and redact credentials and common secret query parameters from URLs, so they can be shared without leaking assessment tokens.

Anyone holding the file can recompute the digest:

Recompute the digest of an untouched report
settlens verify-report reports/settlens-a92a9d60.json
VALID Report content matches its SHA-256 evidence digest.
Expected: 455b6f3e15b4886bda9862bb7148b5852691a4f3daaed1ed04464d125acb5b3f
Actual:   455b6f3e15b4886bda9862bb7148b5852691a4f3daaed1ed04464d125acb5b3f

Now the edit. We took the report of the bypass scan, flipped its critical failure to pass and adjusted the summary, once without touching the digest (edited.json) and once recomputing the digest with the same canonicalization (redigested.json):

A naive edit, then an edit with a recomputed digest
settlens verify-report edited.json
INVALID Report content was modified after the evidence digest was created.
Expected: 11c8f0535c9e76cc7c47db297e53d460089cafbfdbb063a4f5f36ef7ecb78cb7
Actual:   ffc358d5c1e1c806537d870f9533720012a8b19f9c917ea4e34a72efd61cdabd
 
settlens verify-report redigested.json
VALID Report content matches its SHA-256 evidence digest.
Expected: ffc358d5c1e1c806537d870f9533720012a8b19f9c917ea4e34a72efd61cdabd
Actual:   ffc358d5c1e1c806537d870f9533720012a8b19f9c917ea4e34a72efd61cdabd

The naive edit is caught. The careful one is not, and cannot be: a digest is a checksum, and anyone who can edit the file can also recompute it. The Settlens methodology says it plainly: “The digest establishes integrity only.” It protects against accidental corruption and lazy tampering. It says nothing about who produced the report.

Layer 2: the ed25519 attestation

An attestation adds the missing piece: a signature from a key the reader can recognize, which is what turns a report into signed x402 evidence. Settlens uses ed25519, a widely implemented signature scheme with 32-byte public keys and 64-byte signatures that runs everywhere from node:crypto to OpenSSL. Generating a key and signing a report takes two commands:

Generate a signing key and attest a report
settlens evidence keygen --out settlens-evidence.key
Private key written to settlens-evidence.key (keep it secret).
Public key: 0xb4d78c4d6485f43eb41ff3f82270b7cd9f0131621a0fa72bb442e73c34426e48
 
settlens evidence attest reports/settlens-a92a9d60.json \
    --key settlens-evidence.key --out attestation.json
Attestation written to attestation.json

keygen writes the private key as a new PKCS#8 PEM file readable only by its owner, refuses to overwrite a file that already exists, and prints the public key as 32 bytes of hex. attest reads the report and signs a compact statement about it:

JSONattestation.json
{
  "kind": "settlens-evidence-attestation",
  "version": 1,
  "canonicalization": "settlens-stable-json-v2",
  "reportId": "a92a9d60",
  "productVersion": "0.5.0",
  "protocolVersion": 2,
  "subjects": [
    "http://127.0.0.1:4402/secure"
  ],
  "summary": {
    "pass": 13,
    "fail": 0,
    "warn": 0,
    "skip": 0
  },
  "reportDigest": "455b6f3e15b4886bda9862bb7148b5852691a4f3daaed1ed04464d125acb5b3f",
  "attestedAt": "2026-10-13T11:13:35.683Z",
  "publicKey": "0xb4d78c4d6485f43eb41ff3f82270b7cd9f0131621a0fa72bb442e73c34426e48",
  "signature": "0xa341371dd11c19c699774755d994e32b05245e3c1446e5c465541abb20853d5e0e08264c74c985b698adecbbef5130325eea44bf8213c2d0fd56d74ce24cde05"
}

The signed payload is every field except signature, serialized with the canonical JSON named in its canonicalization field. It names the report (reportId, tool and protocol versions), the subjects (the target URLs), the summary, the report digest, the time of signing and the public key. The signature covers all of them, so none can be altered without invalidating it. Signing uses node:crypto only: no network call, no third-party service and no hardware security module are required.

How an auditor verifies, offline

The auditor needs the report, the attestation and the Settlens CLI, nothing else. evidence verify checks the ed25519 signature against the public key in the attestation, recomputes the report's own digest, and confirms it equals the digest that was signed:

Verify an attestation against its report
settlens evidence verify attestation.json --report reports/settlens-a92a9d60.json
VALID evidence attestation
Signature valid: true
Report matches:  true
Public key:      0xb4d78c4d6485f43eb41ff3f82270b7cd9f0131621a0fa72bb442e73c34426e48

The assessor had also signed the genuine bypass report, as bypass-attestation.json. Checked against that attestation, both forged reports from earlier fail. The naive edit fails its own integrity check. The re-digested one passes that check, but its digest is not the one the assessor signed:

The edited reports against the genuine attestation
settlens evidence verify bypass-attestation.json --report edited.json
INVALID evidence attestation
Signature valid: true
Report matches:  false
Public key:      0xb4d78c4d6485f43eb41ff3f82270b7cd9f0131621a0fa72bb442e73c34426e48
Reasons:
  - The provided report failed its own integrity check.
 
settlens evidence verify bypass-attestation.json --report redigested.json
INVALID evidence attestation
Signature valid: true
Report matches:  false
Public key:      0xb4d78c4d6485f43eb41ff3f82270b7cd9f0131621a0fa72bb442e73c34426e48
Reasons:
  - The attestation does not match the provided report digest.

Without --report, evidence verify checks the signature alone. That tells you the attestation is intact, but not that it matches the file in front of you, so always pass the report when you have it. The same checks are available to AI agents: the Settlens MCP server exposes verify_report and verify_attestation as offline tools, public on the remote endpoint.

Pin the public key

There is one more forgery to try. Anyone can generate a key. What happens if the attacker signs their own edited report?

A forged report signed with a different key
settlens evidence keygen --out attacker.key
Private key written to attacker.key (keep it secret).
Public key: 0xecb54c8a074ffa313ec65dc39a6bd6f0f64fba8e57a95657d6262324d57b620a
settlens evidence attest redigested.json --key attacker.key --out forged-attestation.json
Attestation written to forged-attestation.json
settlens evidence verify forged-attestation.json --report redigested.json
VALID evidence attestation
Signature valid: true
Report matches:  true
Public key:      0xecb54c8a074ffa313ec65dc39a6bd6f0f64fba8e57a95657d6262324d57b620a

It verifies. It should: the signature is valid, the report is intact and the digests match. The only thing wrong is the key, and on its own the tool cannot know which key you expected. A signature proves that the holder of a key signed a digest. It does not prove who holds the key.

Tell it. Save the assessor's public key, obtained through a separate channel, in a file and pass it with --expect-key (the 0x hex printed by keygen or an ed25519 PEM public key). An attestation from any other key is then rejected:

The same forgery against the pinned key
settlens evidence verify forged-attestation.json --report redigested.json \
    --expect-key assessor.pub
INVALID evidence attestation
Signature valid: true
Report matches:  true
Expected signer: false
Public key:      0xecb54c8a074ffa313ec65dc39a6bd6f0f64fba8e57a95657d6262324d57b620a
Reasons:
  - The attestation is signed by 0xecb54c8a074ffa313ec65dc39a6bd6f0f64fba8e57a95657d6262324d57b620a, not by the expected key 0xb4d78c4d6485f43eb41ff3f82270b7cd9f0131621a0fa72bb442e73c34426e48.

Pin the key, every time

The expected key must come from somewhere the forger cannot write to: the assessor's website, the engagement letter or pilot agreement, or a direct exchange. An attestation from an unknown key is a valid signature from a stranger. The MCP verify_attestation tool takes the same key as expected_public_key.

What the timestamp means

attestedAt is the signer's clock at signing time. Because it is inside the signed payload, nobody can change it afterwards without breaking the signature, but it is exactly as trustworthy as the signer: an assessor could sign with a wrong clock. When independent proof of time matters, for example in a dispute, a common approach is to have the attestation's hash timestamped by an external timestamping authority. Settlens does not do that for you; it is worth deciding up front whether your use case needs it.

The report's own generatedAt and per-target startedAt and completedAt fields are covered by the digest, so they cannot change without changing the report. They are likewise the scanning machine's clock.

Keeping the signing key safe

  • Use one key per assessor or team, generated on the machine that signs. Scans never need it; only evidence attest does.
  • Always pass --out to keygen. Without it, the private key is printed to standard output, which in CI means into the job log. With it, an existing key file is never overwritten.
  • Never commit the key file. Treat it like any other signing secret and restrict who can read it.
  • Publish the public key where your readers will look for it, and keep a list of retired keys so older attestations remain checkable after a rotation.

A workflow that holds up

The mechanics are simple; the discipline around them is what makes the evidence useful months later. This is the sequence we follow in a pilot:

  1. Agree on scope and the key first. The engagement letter names the endpoints, the mode (passive, or active with an authorization reference) and the assessor's public key, before the first scan runs.
  2. Keep the JSON report untouched. It is the file that carries the integrity block and the one that gets signed. The HTML and SARIF files display the same digest, so a reader can match them to the JSON, but only the JSON can be recomputed.
  3. Attest right after the scan, then send the report and its attestation together. The attestation names the subjects and the summary, so a reader sees what it covers before opening the report.
  4. Verify on receipt. The reader runs evidence verify with --report and --expect-key. If any of the four checks fails, the file is not evidence.
  5. Archive the set: report, attestation and the configuration that produced them, so the result can be reproduced with the same tool version.

What signed evidence proves, and what it does not

What each layer establishes
QuestionDigest onlyDigest, attestation and pinned key
The report is the file the tool wroteAgainst accidental or naive edits only
A known party stands behind the result
When it was producedScanner clock, unsignedSigner's clock, signed
The endpoint is secure in general
Payments settled correctly

The last two rows matter as much as the first three. A Settlens report describes the behavior that was observed during the recorded window: which checks ran, in which mode, with which results. A signature makes that statement verifiable and attributable. It does not turn it into a certification, a guarantee about untested code, or proof that a payment settled; Settlens never calls /settle.

Within those limits, signed evidence changes the conversation with partners and auditors from “trust us” to “check it yourself”. The evidence documentation lists every command and field, the report reference describes the report format, the Evidence page explains how attestations fit into an assessment, and Settlens for security teams shows how auditors and risk teams use them in a review. To get signed evidence for your own x402 endpoints, request a pilot.

Sources

  1. RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA)IRTF · rfc-editor.org
  2. FIPS 180-4: Secure Hash Standard (SHS)NIST · csrc.nist.gov
  3. RFC 3161: Time-Stamp Protocol (TSP)IETF · rfc-editor.org