twzrd-receipt-verifier 1.0.7 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -13
- package/package.json +8 -6
- package/verify_twzrd_receipt.js +185 -25
package/README.md
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
# TWZRD Receipt Verifier (standalone)
|
|
2
2
|
|
|
3
|
-
Verify a TWZRD
|
|
4
|
-
|
|
5
|
-
widely-audited crypto libraries.
|
|
3
|
+
Verify a TWZRD receipt offline, trusting **nothing from TWZRD's servers or
|
|
4
|
+
codebase** - only the receipt, TWZRD's published public key, and two
|
|
5
|
+
widely-audited crypto libraries. The verifier **auto-detects** two receipt
|
|
6
|
+
families:
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
| Family | What it is | Scheme | Signing key |
|
|
9
|
+
|--------|-----------|--------|-------------|
|
|
10
|
+
| **AO-Receipt V5/V6** | trust-API receipts from `intel.twzrd.xyz` | `keccak256` leaf over a packed preimage, Ed25519 over the leaf bytes | `9V6Pn19...` (fetched/pinned) |
|
|
11
|
+
| **cNFT Receipt** | the 95k genesis compressed-NFT receipts | Ed25519 **directly** over a compact-JSON payload (no leaf), hex sig | `2ELSDx...` (built-in) |
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
For V5/V6 the verifier reads the domain the receipt carries and applies the
|
|
14
|
+
matching leaf rules (V6 binds the `reputation_*` provenance fields into the signed
|
|
15
|
+
leaf; V5 left them unsigned). For cNFT receipts there is no leaf - tamper-evidence
|
|
16
|
+
**is** the signature: any change to a signed field (including the wallet)
|
|
17
|
+
invalidates it.
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
key
|
|
15
|
-
Unsigned, wrong-key, or tampered receipts fail.
|
|
19
|
+
If it says `VALID`, the receipt was authored by TWZRD and was not altered.
|
|
20
|
+
Unsigned, wrong-key, wrong-wallet, or tampered receipts fail.
|
|
16
21
|
|
|
17
22
|
## Where this fits: the agent trust loop
|
|
18
23
|
|
|
@@ -48,10 +53,70 @@ Also published, machine-readable, at:
|
|
|
48
53
|
> **Most paranoid mode:** pin the key out-of-band with `--pubkey` instead of
|
|
49
54
|
> fetching it, so you never trust the live endpoint to tell you which key to trust.
|
|
50
55
|
|
|
56
|
+
## cNFT Receipts (the 95k genesis receipts)
|
|
57
|
+
|
|
58
|
+
Every genesis receipt is a compressed NFT on Solana mainnet (tree
|
|
59
|
+
`8QFdTqBkSeyuvp47dXdpwfWzXTuYSbAC64oT4soPGnXS`, verified creator `2ELSDx...`). Its
|
|
60
|
+
at-mint snapshot is published as a signed `anchor` block in the cNFT metadata,
|
|
61
|
+
served at `https://twzrd.xyz/r/<wallet>.json`:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"anchor": {
|
|
66
|
+
"tier_at_mint": "Platinum",
|
|
67
|
+
"score_at_mint": 255,
|
|
68
|
+
"verified_tx": "<solana settlement signature>",
|
|
69
|
+
"behavior_proof": "<sha256 hex>",
|
|
70
|
+
"minted_at": 1782415336,
|
|
71
|
+
"signature": "<128-hex Ed25519 sig>",
|
|
72
|
+
"verify_pubkey": "2ELSDxLkb7dYrN6EUG69tNtULAq4Fo7WPvXyrZPmuFif"
|
|
73
|
+
},
|
|
74
|
+
"live": { "...": "current decayed reputation (NOT signed)" }
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The signed payload is the compact JSON `{wallet, tier_at_mint, score_at_mint,
|
|
79
|
+
verified_tx, behavior_proof, minted_at}` (exact key order). The `wallet` is the
|
|
80
|
+
first signed field but is **not** stored in the anchor - it is the `<wallet>.json`
|
|
81
|
+
filename / the cNFT leaf owner - so pass `--wallet` or keep the filename. The
|
|
82
|
+
signing key (`2ELSDx...`) is **built in** to the verifier (pinned in the audited
|
|
83
|
+
package); override with `--pubkey`.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# fetch a receipt and verify it (wallet inferred from the filename, key built-in)
|
|
87
|
+
W=zoz7neLHXoaLwNBuckSqNqaMsacpqJsphtFuNNpQyt3
|
|
88
|
+
curl -s https://twzrd.xyz/r/$W.json -o $W.json
|
|
89
|
+
npx twzrd-receipt-verifier $W.json --self-test
|
|
90
|
+
|
|
91
|
+
# or pass the wallet explicitly (e.g. when piping from stdin)
|
|
92
|
+
npx twzrd-receipt-verifier anchor.json --wallet $W
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
mode : cNFT (Bubblegum anchor)
|
|
97
|
+
trusted pubkey : 2ELSDxLkb7dYrN6EUG69tNtULAq4Fo7WPvXyrZPmuFif [source: built-in genesis authority]
|
|
98
|
+
wallet : zoz7neLHXoaLwNBuckSqNqaMsacpqJsphtFuNNpQyt3 [source: filename]
|
|
99
|
+
signature_valid : true
|
|
100
|
+
RESULT : VALID (TWZRD-authored, untampered)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Only the `anchor` block is signed. The `live` block (current decayed reputation)
|
|
104
|
+
is informational and intentionally NOT covered by the signature. For full on-chain
|
|
105
|
+
binding, confirm the cNFT exists in the genesis tree with verified creator
|
|
106
|
+
`2ELSDx` via any DAS provider (`getAsset` / `getAssetProof`); the signature alone
|
|
107
|
+
already proves `2ELSDx` authorship of the at-mint snapshot.
|
|
108
|
+
|
|
51
109
|
## Get a receipt to verify
|
|
52
110
|
|
|
53
|
-
Any TWZRD V5 receipt works. To mint a fresh one, pay the trust endpoint
|
|
54
|
-
0.05 USDC on Solana mainnet)
|
|
111
|
+
Any TWZRD V5/v6 receipt works. To mint a fresh one, pay the trust endpoint
|
|
112
|
+
(x402, 0.05 USDC on Solana mainnet) with an x402 client that preserves TWZRD's
|
|
113
|
+
sponsored fee-payer slot semantics.
|
|
114
|
+
|
|
115
|
+
Current caveat (2026-06-23): `npx agentcash@latest fetch ...` is not a green
|
|
116
|
+
TWZRD paid-trust repro. It failed closed with `payment_invalid` /
|
|
117
|
+
`fee_payer_slot_already_signed`, and AgentCash balance stayed unchanged.
|
|
118
|
+
|
|
119
|
+
Known-bad compatibility command:
|
|
55
120
|
|
|
56
121
|
```bash
|
|
57
122
|
npx agentcash@latest fetch https://intel.twzrd.xyz/v1/intel/trust/<PUBKEY> > resp.json
|
|
@@ -94,7 +159,7 @@ twzrd-verify-receipt receipt.json --max-age 300
|
|
|
94
159
|
cat receipt.json | twzrd-verify-receipt -
|
|
95
160
|
```
|
|
96
161
|
|
|
97
|
-
Source: [
|
|
162
|
+
Source: [twzrd-sol/twzrd-receipt-verifier](https://github.com/twzrd-sol/twzrd-receipt-verifier)
|
|
98
163
|
|
|
99
164
|
## Node
|
|
100
165
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "twzrd-receipt-verifier",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Standalone offline verifier for TWZRD AO-Receipt V5 (
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Standalone offline verifier for TWZRD receipts: AO-Receipt V5/V6 (keccak256 leaf) and genesis cNFT receipts (Ed25519 over compact JSON). No trust in TWZRD servers or code.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twzrd",
|
|
7
7
|
"x402",
|
|
@@ -11,16 +11,18 @@
|
|
|
11
11
|
"receipt",
|
|
12
12
|
"verifier",
|
|
13
13
|
"agent",
|
|
14
|
-
"attestation"
|
|
14
|
+
"attestation",
|
|
15
|
+
"cnft",
|
|
16
|
+
"bubblegum",
|
|
17
|
+
"compressed-nft"
|
|
15
18
|
],
|
|
16
19
|
"homepage": "https://intel.twzrd.xyz",
|
|
17
20
|
"bugs": {
|
|
18
|
-
"url": "https://github.com/twzrd-sol/
|
|
21
|
+
"url": "https://github.com/twzrd-sol/twzrd-receipt-verifier/issues"
|
|
19
22
|
},
|
|
20
23
|
"repository": {
|
|
21
24
|
"type": "git",
|
|
22
|
-
"url": "git+https://github.com/twzrd-sol/
|
|
23
|
-
"directory": "packages/twzrd-agent-intel/verifier"
|
|
25
|
+
"url": "git+https://github.com/twzrd-sol/twzrd-receipt-verifier.git"
|
|
24
26
|
},
|
|
25
27
|
"license": "MIT",
|
|
26
28
|
"author": "TWZRD",
|
package/verify_twzrd_receipt.js
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/*
|
|
3
|
-
* Standalone offline verifier for TWZRD
|
|
3
|
+
* Standalone offline verifier for TWZRD receipts (Node). Two receipt families,
|
|
4
|
+
* auto-detected:
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* A. AO-Receipt V5/V6 (trust-API) - keccak256 leaf over a packed preimage,
|
|
7
|
+
* Ed25519-signed over the leaf bytes by the receipt key (9V6Pn19...).
|
|
8
|
+
* Shape: { preimage, leaf, signature, signing_pubkey }.
|
|
9
|
+
* B. cNFT Receipt (Bubblegum anchor) - the genesis compressed-NFT receipts.
|
|
10
|
+
* Ed25519 signed DIRECTLY over a compact-JSON payload (no keccak leaf) by
|
|
11
|
+
* the airship genesis authority (2ELSDx...), signature hex-encoded.
|
|
12
|
+
* Shape: { anchor: { tier_at_mint, score_at_mint, verified_tx,
|
|
13
|
+
* behavior_proof, minted_at, signature, verify_pubkey }, ... }.
|
|
11
14
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
15
|
+
* Verifies, with NO trust in TWZRD's servers or codebase, that a receipt was
|
|
16
|
+
* authored by TWZRD's published Ed25519 key and was not tampered with. Crypto
|
|
17
|
+
* comes from audited libs (tweetnacl = ref Ed25519, js-sha3 = Keccak), not from
|
|
18
|
+
* this script. base58 + the TWZRD byte layout are the only logic here.
|
|
14
19
|
*
|
|
15
20
|
* npm install tweetnacl js-sha3 bs58
|
|
16
21
|
*
|
|
17
|
-
*
|
|
22
|
+
* # trust-API receipt (A)
|
|
18
23
|
* node verify_twzrd_receipt.js receipt.json --pubkey 9V6Pn19kiUA5Rn6JpQfNduanvGt2aXGwsarosNfa2Ldf
|
|
24
|
+
* # cNFT receipt (B) - wallet is part of the signed payload but not in the
|
|
25
|
+
* # anchor block, so pass it or name the file <wallet>.json
|
|
26
|
+
* node verify_twzrd_receipt.js zoz7...json # wallet inferred from filename
|
|
27
|
+
* node verify_twzrd_receipt.js anchor.json --wallet zoz7neLHXoaLwNBuckSqNqaMsacpqJsphtFuNNpQyt3
|
|
19
28
|
* cat receipt.json | node verify_twzrd_receipt.js - # stdin
|
|
20
29
|
* node verify_twzrd_receipt.js receipt.json --self-test # tamper must fail
|
|
21
30
|
*
|
|
@@ -24,6 +33,7 @@
|
|
|
24
33
|
'use strict';
|
|
25
34
|
|
|
26
35
|
const fs = require('fs');
|
|
36
|
+
const path = require('path');
|
|
27
37
|
const crypto = require('crypto');
|
|
28
38
|
const https = require('https');
|
|
29
39
|
const nacl = require('tweetnacl');
|
|
@@ -32,6 +42,11 @@ const bs58 = require('bs58');
|
|
|
32
42
|
|
|
33
43
|
const DEFAULT_BASE_URL = 'https://intel.twzrd.xyz';
|
|
34
44
|
const KECCAK_EMPTY = 'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470';
|
|
45
|
+
// Genesis cNFT receipt authority (airship). Baked in as the most paranoid form of
|
|
46
|
+
// out-of-band pinning: the key ships in this audited package, never fetched live.
|
|
47
|
+
// Override with --pubkey. Matches `verify_pubkey` in every genesis anchor and the
|
|
48
|
+
// verified creator on every cNFT in tree 8QFdTqBkSeyuvp47dXdpwfWzXTuYSbAC64oT4soPGnXS.
|
|
49
|
+
const DEFAULT_CNFT_PUBKEY = '2ELSDxLkb7dYrN6EUG69tNtULAq4Fo7WPvXyrZPmuFif';
|
|
35
50
|
|
|
36
51
|
function b58decode(s) { return Buffer.from(bs58.decode(s)); }
|
|
37
52
|
|
|
@@ -166,31 +181,119 @@ function verify(receipt, trustedPubkey) {
|
|
|
166
181
|
return out;
|
|
167
182
|
}
|
|
168
183
|
|
|
184
|
+
// ── cNFT (Bubblegum anchor) receipt ───────────────────────────────────────
|
|
185
|
+
// The genesis compressed-NFT receipts are NOT keccak-leaf receipts. Each is an
|
|
186
|
+
// Ed25519 signature made DIRECTLY over the UTF-8 bytes of a compact JSON object,
|
|
187
|
+
// in this EXACT key order (JSON.stringify defaults: no spaces). Do not reorder.
|
|
188
|
+
const CNFT_SIGNED_FIELDS = ['wallet', 'tier_at_mint', 'score_at_mint', 'verified_tx', 'behavior_proof', 'minted_at'];
|
|
189
|
+
|
|
190
|
+
// A cNFT receipt is the metadata JSON served at /r/<wallet>.json: it carries an
|
|
191
|
+
// `anchor` block (at-mint snapshot + signature) instead of a keccak `leaf`.
|
|
192
|
+
function isCnftReceipt(receipt) {
|
|
193
|
+
const a = receipt && receipt.anchor;
|
|
194
|
+
return !!(a && typeof a === 'object' && a.signature &&
|
|
195
|
+
(a.tier_at_mint !== undefined || a.score_at_mint !== undefined));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Reconstruct the exact bytes the issuer signed (airship.ts). `wallet` is the
|
|
199
|
+
// first signed field but is NOT stored in the anchor block (it is the leaf owner /
|
|
200
|
+
// the <wallet>.json filename), so it must be supplied by the caller.
|
|
201
|
+
function cnftSignedPayload(anchor, wallet) {
|
|
202
|
+
return Buffer.from(JSON.stringify({
|
|
203
|
+
wallet,
|
|
204
|
+
tier_at_mint: anchor.tier_at_mint,
|
|
205
|
+
score_at_mint: anchor.score_at_mint,
|
|
206
|
+
verified_tx: anchor.verified_tx,
|
|
207
|
+
behavior_proof: anchor.behavior_proof,
|
|
208
|
+
minted_at: anchor.minted_at,
|
|
209
|
+
}), 'utf8');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Resolve the wallet from (in priority): explicit --wallet, the receipt body (if a
|
|
213
|
+
// future format embeds it), or the <wallet>.json filename. Only accepts a filename
|
|
214
|
+
// stem that base58-decodes to a 32-byte pubkey, so a stray filename can't be passed
|
|
215
|
+
// off as the signed wallet.
|
|
216
|
+
function resolveWallet({ explicitWallet, receipt, receiptPath } = {}) {
|
|
217
|
+
if (explicitWallet) return { wallet: explicitWallet, src: '--wallet' };
|
|
218
|
+
if (receipt && typeof receipt.wallet === 'string') return { wallet: receipt.wallet, src: 'receipt.wallet' };
|
|
219
|
+
if (receipt && receipt.anchor && typeof receipt.anchor.wallet === 'string') return { wallet: receipt.anchor.wallet, src: 'anchor.wallet' };
|
|
220
|
+
if (receiptPath && receiptPath !== '-') {
|
|
221
|
+
const stem = path.basename(receiptPath).replace(/\.json$/i, '');
|
|
222
|
+
try { if (Buffer.from(bs58.decode(stem)).length === 32) return { wallet: stem, src: 'filename' }; } catch (_) {}
|
|
223
|
+
}
|
|
224
|
+
return { wallet: undefined, src: 'none' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Authenticity for a cNFT receipt: Ed25519-verify the hex signature over the
|
|
228
|
+
// reconstructed compact-JSON payload against the trusted key. There is no keccak
|
|
229
|
+
// leaf to recompute - tamper-evidence is the signature itself: any change to a
|
|
230
|
+
// signed field (incl. wallet) invalidates it.
|
|
231
|
+
function verifyCnft(receipt, trustedPubkey, wallet) {
|
|
232
|
+
const out = { mode: 'cnft', signature_valid: false, errors: [] };
|
|
233
|
+
const a = (receipt && receipt.anchor) || {};
|
|
234
|
+
out.wallet = wallet;
|
|
235
|
+
if (!wallet) {
|
|
236
|
+
out.errors.push('cNFT receipt: wallet unknown - it is part of the signed payload but not in the anchor block. Pass --wallet <addr> or name the file <wallet>.json.');
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
const embedded = a.verify_pubkey;
|
|
240
|
+
if (embedded && embedded !== trustedPubkey) {
|
|
241
|
+
out.errors.push(`anchor.verify_pubkey ${embedded} != trusted key ${trustedPubkey}`);
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
const sigHex = String(a.signature || '').toLowerCase().replace(/^0x/, '');
|
|
245
|
+
if (!sigHex) { out.errors.push('missing anchor.signature'); return out; }
|
|
246
|
+
if (!/^[0-9a-f]+$/.test(sigHex) || sigHex.length !== 128) {
|
|
247
|
+
out.errors.push(`anchor.signature must be 64 hex bytes (got ${sigHex.length / 2 | 0})`);
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
const sig = Buffer.from(sigHex, 'hex');
|
|
251
|
+
const msg = cnftSignedPayload(a, wallet);
|
|
252
|
+
out.signed_payload = msg.toString('utf8');
|
|
253
|
+
let pk;
|
|
254
|
+
try { pk = b58decode(trustedPubkey); }
|
|
255
|
+
catch (e) { out.errors.push('trusted pubkey not base58: ' + e.message); return out; }
|
|
256
|
+
try {
|
|
257
|
+
out.signature_valid = nacl.sign.detached.verify(
|
|
258
|
+
new Uint8Array(msg), new Uint8Array(sig), new Uint8Array(pk));
|
|
259
|
+
} catch (e) { out.errors.push('signature check error: ' + e.message); return out; }
|
|
260
|
+
if (!out.signature_valid) {
|
|
261
|
+
out.errors.push('signature not valid for the trusted key (payload tampered, or wrong --wallet / --pubkey)');
|
|
262
|
+
}
|
|
263
|
+
out.valid = out.signature_valid && out.errors.length === 0;
|
|
264
|
+
out.trusted_pubkey = trustedPubkey;
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
|
|
169
268
|
async function main() {
|
|
170
269
|
const args = process.argv.slice(2);
|
|
171
|
-
const HELP = `twzrd-receipt-verifier -- offline verifier for TWZRD
|
|
270
|
+
const HELP = `twzrd-receipt-verifier -- offline verifier for TWZRD receipts (Ed25519)
|
|
172
271
|
|
|
173
272
|
Verifies, with NO trust in TWZRD's servers or code, that a receipt was authored by
|
|
174
|
-
TWZRD's published Ed25519 key and was not tampered with.
|
|
273
|
+
TWZRD's published Ed25519 key and was not tampered with. Auto-detects two families:
|
|
274
|
+
- AO-Receipt V5/V6 (trust-API): keccak256 leaf, signed by ${'9V6Pn19...'} (default fetch)
|
|
275
|
+
- cNFT Receipt (genesis anchor): compact-JSON payload, signed by ${DEFAULT_CNFT_PUBKEY.slice(0, 7)}... (built-in)
|
|
175
276
|
|
|
176
277
|
usage:
|
|
177
|
-
twzrd-receipt-verifier <receipt.json|-> [--pubkey KEY] [--base-url URL] [--max-age SECS] [--self-test]
|
|
278
|
+
twzrd-receipt-verifier <receipt.json|-> [--pubkey KEY] [--wallet ADDR] [--base-url URL] [--max-age SECS] [--self-test]
|
|
178
279
|
|
|
179
280
|
arguments:
|
|
180
281
|
<receipt.json> path to the receipt JSON, or "-" to read from stdin
|
|
181
|
-
--pubkey KEY trust this base58 Ed25519 pubkey (out-of-band) instead of fetching
|
|
182
|
-
--
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
282
|
+
--pubkey KEY trust this base58 Ed25519 pubkey (out-of-band) instead of fetching/built-in
|
|
283
|
+
--wallet ADDR (cNFT only) the leaf-owner wallet, which is part of the signed payload but
|
|
284
|
+
not stored in the anchor block. Inferred from a <wallet>.json filename if omitted.
|
|
285
|
+
--base-url URL (trust-API only) where to fetch the published key (default: ${DEFAULT_BASE_URL})
|
|
286
|
+
--max-age SECS replay-resistance policy: reject if the receipt's timestamp (preimage.timestamp_unix
|
|
287
|
+
for trust-API, anchor.minted_at for cNFT) is older than SECS, OR is missing.
|
|
288
|
+
Crypto is time-independent; this is opt-in relying-party policy.
|
|
186
289
|
--self-test additionally confirm a tampered copy FAILS (proves the check works)
|
|
187
290
|
-h, --help show this help
|
|
188
291
|
|
|
189
292
|
exit code: 0 = VALID, 1 = INVALID / error
|
|
190
|
-
key source: ${DEFAULT_BASE_URL}/.well-known/x402`;
|
|
293
|
+
trust-API key source: ${DEFAULT_BASE_URL}/.well-known/x402`;
|
|
191
294
|
if (args.includes('-h') || args.includes('--help')) { console.log(HELP); process.exit(0); }
|
|
192
295
|
if (args.length === 0) {
|
|
193
|
-
console.error('usage: twzrd-receipt-verifier <receipt.json|-> [--pubkey KEY] [--base-url URL] [--max-age SECS] [--self-test]');
|
|
296
|
+
console.error('usage: twzrd-receipt-verifier <receipt.json|-> [--pubkey KEY] [--wallet ADDR] [--base-url URL] [--max-age SECS] [--self-test]');
|
|
194
297
|
console.error(' twzrd-receipt-verifier --help');
|
|
195
298
|
process.exit(1);
|
|
196
299
|
}
|
|
@@ -201,16 +304,63 @@ key source: ${DEFAULT_BASE_URL}/.well-known/x402`;
|
|
|
201
304
|
const maxAgeArg = getOpt('--max-age');
|
|
202
305
|
const maxAge = maxAgeArg ? parseInt(maxAgeArg, 10) : 0; // 0 = freshness check off (opt-in policy)
|
|
203
306
|
|
|
204
|
-
// keccak self-test: refuse to run with a broken hash backend
|
|
205
|
-
if (keccak256('') !== KECCAK_EMPTY) { console.error('FATAL: keccak256 backend is wrong'); process.exit(1); }
|
|
206
|
-
|
|
207
307
|
const raw = receiptArg === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(receiptArg, 'utf8');
|
|
208
308
|
const receipt = JSON.parse(raw);
|
|
209
309
|
|
|
310
|
+
// ── cNFT (Bubblegum anchor) receipt: Ed25519 over compact JSON, no keccak leaf ──
|
|
311
|
+
if (isCnftReceipt(receipt)) {
|
|
312
|
+
let trusted = getOpt('--pubkey'), keySrc;
|
|
313
|
+
if (trusted) { keySrc = '--pubkey (out-of-band)'; }
|
|
314
|
+
else { trusted = DEFAULT_CNFT_PUBKEY; keySrc = 'built-in genesis authority'; }
|
|
315
|
+
const { wallet, src: walletSrc } = resolveWallet({
|
|
316
|
+
explicitWallet: getOpt('--wallet'), receipt, receiptPath: receiptArg,
|
|
317
|
+
});
|
|
318
|
+
console.log(`mode : cNFT (Bubblegum anchor)`);
|
|
319
|
+
console.log(`trusted pubkey : ${trusted} [source: ${keySrc}]`);
|
|
320
|
+
console.log(`wallet : ${wallet || '(unknown)'} [source: ${walletSrc}]`);
|
|
321
|
+
|
|
322
|
+
const res = verifyCnft(receipt, trusted, wallet);
|
|
323
|
+
res.errors = res.errors || [];
|
|
324
|
+
|
|
325
|
+
// Opt-in freshness gate (anchor.minted_at). cNFT receipts are long-lived by
|
|
326
|
+
// design, so this is rarely useful, but kept for parity with the trust-API path.
|
|
327
|
+
if (maxAge > 0) {
|
|
328
|
+
const ts = receipt.anchor ? Number(receipt.anchor.minted_at) : NaN;
|
|
329
|
+
if (!Number.isFinite(ts) || ts <= 0) {
|
|
330
|
+
res.errors.push(`--max-age ${maxAge}s set but anchor has no valid minted_at`);
|
|
331
|
+
res.valid = false;
|
|
332
|
+
} else {
|
|
333
|
+
const age = Math.abs(Math.floor(Date.now() / 1000) - ts);
|
|
334
|
+
if (age > maxAge) { res.errors.push(`receipt too old (age ${age}s > --max-age ${maxAge}s)`); res.valid = false; }
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
console.log(`signature_valid : ${res.signature_valid}`);
|
|
339
|
+
res.errors.forEach((e) => console.log(' - ' + e));
|
|
340
|
+
let ok = !!res.valid;
|
|
341
|
+
console.log(`RESULT : ${ok ? 'VALID (TWZRD-authored, untampered)' : 'INVALID'}`);
|
|
342
|
+
|
|
343
|
+
if (selfTest) {
|
|
344
|
+
const tampered = JSON.parse(raw);
|
|
345
|
+
tampered.anchor = tampered.anchor || {};
|
|
346
|
+
tampered.anchor.score_at_mint = (Number(tampered.anchor.score_at_mint) || 0) + 1;
|
|
347
|
+
const t = verifyCnft(tampered, trusted, wallet);
|
|
348
|
+
const passed = !t.valid;
|
|
349
|
+
console.log(`self-test (tampered score must FAIL): ${passed ? 'PASS' : 'BROKEN'}`);
|
|
350
|
+
ok = ok && passed;
|
|
351
|
+
}
|
|
352
|
+
process.exit(ok ? 0 : 1);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ── trust-API receipt (V5/V6): keccak256 leaf, signed over the leaf bytes ──
|
|
356
|
+
// keccak self-test: refuse to run with a broken hash backend
|
|
357
|
+
if (keccak256('') !== KECCAK_EMPTY) { console.error('FATAL: keccak256 backend is wrong'); process.exit(1); }
|
|
358
|
+
|
|
210
359
|
let trusted = getOpt('--pubkey'), src;
|
|
211
360
|
if (trusted) { src = '--pubkey (out-of-band)'; }
|
|
212
361
|
else { trusted = await fetchPublishedPubkey(baseUrl); src = baseUrl + '/.well-known/x402'; }
|
|
213
|
-
console.log(`
|
|
362
|
+
console.log(`mode : AO-Receipt (trust-API)`);
|
|
363
|
+
console.log(`trusted pubkey : ${trusted} [source: ${src}]`);
|
|
214
364
|
|
|
215
365
|
const res = verify(receipt, trusted);
|
|
216
366
|
|
|
@@ -251,4 +401,14 @@ key source: ${DEFAULT_BASE_URL}/.well-known/x402`;
|
|
|
251
401
|
process.exit(ok ? 0 : 1);
|
|
252
402
|
}
|
|
253
403
|
|
|
254
|
-
|
|
404
|
+
// Export the pure verifiers for tests / programmatic use. Only run the CLI when
|
|
405
|
+
// invoked directly (so `require()` from the test suite does not trigger main()).
|
|
406
|
+
module.exports = {
|
|
407
|
+
verify, recomputeLeaf,
|
|
408
|
+
verifyCnft, cnftSignedPayload, isCnftReceipt, resolveWallet,
|
|
409
|
+
DEFAULT_CNFT_PUBKEY, CNFT_SIGNED_FIELDS,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
if (require.main === module) {
|
|
413
|
+
main().catch((e) => { console.error('error:', e.message); process.exit(1); });
|
|
414
|
+
}
|