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.
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.
{
"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:
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):
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:
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:
{
"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:
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:
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?
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:
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 attestdoes. - Always pass
--outtokeygen. 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:
- 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.
- 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.
- 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.
- Verify on receipt. The reader runs
evidence verifywith--reportand--expect-key. If any of the four checks fails, the file is not evidence. - 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
| Question | Digest only | Digest, attestation and pinned key |
|---|---|---|
| The report is the file the tool wrote | Against accidental or naive edits only | |
| A known party stands behind the result | ||
| When it was produced | Scanner clock, unsigned | Signer'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.