twzrd-receipt-verifier 1.0.9 → 1.2.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 +86 -14
- package/package.json +6 -3
- package/verify_twzrd_receipt.js +228 -25
package/README.md
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
# TWZRD Receipt Verifier (standalone)
|
|
2
2
|
|
|
3
|
-
Verify a TWZRD
|
|
4
|
-
|
|
5
|
-
widely-audited crypto libraries. The verifier
|
|
6
|
-
|
|
7
|
-
`reputation_*` provenance fields into the signed leaf; V5 left them unsigned).
|
|
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:
|
|
8
7
|
|
|
9
|
-
|
|
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) |
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
key
|
|
17
|
-
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.
|
|
18
21
|
|
|
19
22
|
## Where this fits: the agent trust loop
|
|
20
23
|
|
|
@@ -50,10 +53,79 @@ Also published, machine-readable, at:
|
|
|
50
53
|
> **Most paranoid mode:** pin the key out-of-band with `--pubkey` instead of
|
|
51
54
|
> fetching it, so you never trust the live endpoint to tell you which key to trust.
|
|
52
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`, or fetch the published copy with `--fetch-key`.
|
|
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
|
+
# fetch the key from the published descriptor instead of the built-in copy
|
|
95
|
+
# (cross-check, or pin to whatever the live domain publishes)
|
|
96
|
+
npx twzrd-receipt-verifier $W.json --fetch-key
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The key is published, machine-readable, at `https://api.twzrd.xyz/v1/receipts/pubkey`
|
|
100
|
+
(and `https://twzrd.xyz/.well-known/twzrd-receipt-pubkey`) with the full signing spec
|
|
101
|
+
(`public_key`, `signed_fields`, `scheme`, `tree`). It must equal the built-in key **and**
|
|
102
|
+
the on-chain verified creator of every cNFT in the tree - three independent sources.
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
mode : cNFT (Bubblegum anchor)
|
|
106
|
+
trusted pubkey : 2ELSDxLkb7dYrN6EUG69tNtULAq4Fo7WPvXyrZPmuFif [source: built-in genesis authority]
|
|
107
|
+
wallet : zoz7neLHXoaLwNBuckSqNqaMsacpqJsphtFuNNpQyt3 [source: filename]
|
|
108
|
+
signature_valid : true
|
|
109
|
+
RESULT : VALID (TWZRD-authored, untampered)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Only the `anchor` block is signed. The `live` block (current decayed reputation)
|
|
113
|
+
is informational and intentionally NOT covered by the signature. For full on-chain
|
|
114
|
+
binding, confirm the cNFT exists in the genesis tree with verified creator
|
|
115
|
+
`2ELSDx` via any DAS provider (`getAsset` / `getAssetProof`); the signature alone
|
|
116
|
+
already proves `2ELSDx` authorship of the at-mint snapshot.
|
|
117
|
+
|
|
53
118
|
## Get a receipt to verify
|
|
54
119
|
|
|
55
|
-
Any TWZRD V5 receipt works. To mint a fresh one, pay the trust endpoint
|
|
56
|
-
0.05 USDC on Solana mainnet)
|
|
120
|
+
Any TWZRD V5/v6 receipt works. To mint a fresh one, pay the trust endpoint
|
|
121
|
+
(x402, 0.05 USDC on Solana mainnet) with an x402 client that preserves TWZRD's
|
|
122
|
+
sponsored fee-payer slot semantics.
|
|
123
|
+
|
|
124
|
+
Current caveat (2026-06-23): `npx agentcash@latest fetch ...` is not a green
|
|
125
|
+
TWZRD paid-trust repro. It failed closed with `payment_invalid` /
|
|
126
|
+
`fee_payer_slot_already_signed`, and AgentCash balance stayed unchanged.
|
|
127
|
+
|
|
128
|
+
Known-bad compatibility command:
|
|
57
129
|
|
|
58
130
|
```bash
|
|
59
131
|
npx agentcash@latest fetch https://intel.twzrd.xyz/v1/intel/trust/<PUBKEY> > resp.json
|
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 and
|
|
3
|
+
"version": "1.2.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,7 +11,10 @@
|
|
|
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": {
|
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,14 @@ 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, or fetch the published copy with --fetch-key (cross-check).
|
|
48
|
+
// Matches `verify_pubkey` in every genesis anchor and the verified creator on every
|
|
49
|
+
// cNFT in tree 8QFdTqBkSeyuvp47dXdpwfWzXTuYSbAC64oT4soPGnXS.
|
|
50
|
+
const DEFAULT_CNFT_PUBKEY = '2ELSDxLkb7dYrN6EUG69tNtULAq4Fo7WPvXyrZPmuFif';
|
|
51
|
+
// Where --fetch-key looks for the published cNFT key descriptor.
|
|
52
|
+
const DEFAULT_CNFT_BASE_URL = 'https://api.twzrd.xyz';
|
|
35
53
|
|
|
36
54
|
function b58decode(s) { return Buffer.from(bs58.decode(s)); }
|
|
37
55
|
|
|
@@ -131,6 +149,32 @@ function fetchPublishedPubkey(baseUrl) {
|
|
|
131
149
|
return fetchPath(0);
|
|
132
150
|
}
|
|
133
151
|
|
|
152
|
+
// Fetch the published cNFT signing key descriptor (for --fetch-key). Returns the
|
|
153
|
+
// base58 pubkey. This trades package-trust for domain/TLS-trust; the built-in key is
|
|
154
|
+
// the default precisely because it needs no network. Use this to CROSS-CHECK the
|
|
155
|
+
// built-in, or to pin to whatever the live domain currently publishes.
|
|
156
|
+
function fetchCnftPubkey(baseUrl) {
|
|
157
|
+
const base = baseUrl.replace(/\/+$/, '');
|
|
158
|
+
const paths = ['/v1/receipts/pubkey', '/.well-known/twzrd-receipt-pubkey'];
|
|
159
|
+
const headers = { 'User-Agent': 'twzrd-receipt-verifier/cnft' };
|
|
160
|
+
function fetchPath(i) {
|
|
161
|
+
if (i >= paths.length) return Promise.reject(new Error('no cNFT pubkey endpoint responded'));
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
https.get(base + paths[i], { headers }, (res) => {
|
|
164
|
+
let body = '';
|
|
165
|
+
res.on('data', (c) => (body += c));
|
|
166
|
+
res.on('end', () => {
|
|
167
|
+
try {
|
|
168
|
+
const pk = JSON.parse(body).public_key;
|
|
169
|
+
if (pk) resolve(pk); else throw new Error('no public_key field');
|
|
170
|
+
} catch (e) { fetchPath(i + 1).then(resolve, reject); }
|
|
171
|
+
});
|
|
172
|
+
}).on('error', () => fetchPath(i + 1).then(resolve, reject));
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return fetchPath(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
134
178
|
function verify(receipt, trustedPubkey) {
|
|
135
179
|
const out = { leaf_valid: false, signature_valid: false, errors: [] };
|
|
136
180
|
const pre = receipt.preimage || {};
|
|
@@ -166,31 +210,124 @@ function verify(receipt, trustedPubkey) {
|
|
|
166
210
|
return out;
|
|
167
211
|
}
|
|
168
212
|
|
|
213
|
+
// ── cNFT (Bubblegum anchor) receipt ───────────────────────────────────────
|
|
214
|
+
// The genesis compressed-NFT receipts are NOT keccak-leaf receipts. Each is an
|
|
215
|
+
// Ed25519 signature made DIRECTLY over the UTF-8 bytes of a compact JSON object,
|
|
216
|
+
// in this EXACT key order (JSON.stringify defaults: no spaces). Do not reorder.
|
|
217
|
+
const CNFT_SIGNED_FIELDS = ['wallet', 'tier_at_mint', 'score_at_mint', 'verified_tx', 'behavior_proof', 'minted_at'];
|
|
218
|
+
|
|
219
|
+
// A cNFT receipt is the metadata JSON served at /r/<wallet>.json: it carries an
|
|
220
|
+
// `anchor` block (at-mint snapshot + signature) instead of a keccak `leaf`.
|
|
221
|
+
function isCnftReceipt(receipt) {
|
|
222
|
+
const a = receipt && receipt.anchor;
|
|
223
|
+
return !!(a && typeof a === 'object' && a.signature &&
|
|
224
|
+
(a.tier_at_mint !== undefined || a.score_at_mint !== undefined));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Reconstruct the exact bytes the issuer signed (airship.ts). `wallet` is the
|
|
228
|
+
// first signed field but is NOT stored in the anchor block (it is the leaf owner /
|
|
229
|
+
// the <wallet>.json filename), so it must be supplied by the caller.
|
|
230
|
+
function cnftSignedPayload(anchor, wallet) {
|
|
231
|
+
return Buffer.from(JSON.stringify({
|
|
232
|
+
wallet,
|
|
233
|
+
tier_at_mint: anchor.tier_at_mint,
|
|
234
|
+
score_at_mint: anchor.score_at_mint,
|
|
235
|
+
verified_tx: anchor.verified_tx,
|
|
236
|
+
behavior_proof: anchor.behavior_proof,
|
|
237
|
+
minted_at: anchor.minted_at,
|
|
238
|
+
}), 'utf8');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Resolve the wallet from (in priority): explicit --wallet, the receipt body (if a
|
|
242
|
+
// future format embeds it), or the <wallet>.json filename. Only accepts a filename
|
|
243
|
+
// stem that base58-decodes to a 32-byte pubkey, so a stray filename can't be passed
|
|
244
|
+
// off as the signed wallet.
|
|
245
|
+
function resolveWallet({ explicitWallet, receipt, receiptPath } = {}) {
|
|
246
|
+
if (explicitWallet) return { wallet: explicitWallet, src: '--wallet' };
|
|
247
|
+
if (receipt && typeof receipt.wallet === 'string') return { wallet: receipt.wallet, src: 'receipt.wallet' };
|
|
248
|
+
if (receipt && receipt.anchor && typeof receipt.anchor.wallet === 'string') return { wallet: receipt.anchor.wallet, src: 'anchor.wallet' };
|
|
249
|
+
if (receiptPath && receiptPath !== '-') {
|
|
250
|
+
const stem = path.basename(receiptPath).replace(/\.json$/i, '');
|
|
251
|
+
try { if (Buffer.from(bs58.decode(stem)).length === 32) return { wallet: stem, src: 'filename' }; } catch (_) {}
|
|
252
|
+
}
|
|
253
|
+
return { wallet: undefined, src: 'none' };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Authenticity for a cNFT receipt: Ed25519-verify the hex signature over the
|
|
257
|
+
// reconstructed compact-JSON payload against the trusted key. There is no keccak
|
|
258
|
+
// leaf to recompute - tamper-evidence is the signature itself: any change to a
|
|
259
|
+
// signed field (incl. wallet) invalidates it.
|
|
260
|
+
function verifyCnft(receipt, trustedPubkey, wallet) {
|
|
261
|
+
const out = { mode: 'cnft', signature_valid: false, errors: [] };
|
|
262
|
+
const a = (receipt && receipt.anchor) || {};
|
|
263
|
+
out.wallet = wallet;
|
|
264
|
+
if (!wallet) {
|
|
265
|
+
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.');
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
const embedded = a.verify_pubkey;
|
|
269
|
+
if (embedded && embedded !== trustedPubkey) {
|
|
270
|
+
out.errors.push(`anchor.verify_pubkey ${embedded} != trusted key ${trustedPubkey}`);
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
const sigHex = String(a.signature || '').toLowerCase().replace(/^0x/, '');
|
|
274
|
+
if (!sigHex) { out.errors.push('missing anchor.signature'); return out; }
|
|
275
|
+
if (!/^[0-9a-f]+$/.test(sigHex) || sigHex.length !== 128) {
|
|
276
|
+
out.errors.push(`anchor.signature must be 64 hex bytes (got ${sigHex.length / 2 | 0})`);
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
const sig = Buffer.from(sigHex, 'hex');
|
|
280
|
+
const msg = cnftSignedPayload(a, wallet);
|
|
281
|
+
out.signed_payload = msg.toString('utf8');
|
|
282
|
+
let pk;
|
|
283
|
+
try { pk = b58decode(trustedPubkey); }
|
|
284
|
+
catch (e) { out.errors.push('trusted pubkey not base58: ' + e.message); return out; }
|
|
285
|
+
try {
|
|
286
|
+
out.signature_valid = nacl.sign.detached.verify(
|
|
287
|
+
new Uint8Array(msg), new Uint8Array(sig), new Uint8Array(pk));
|
|
288
|
+
} catch (e) { out.errors.push('signature check error: ' + e.message); return out; }
|
|
289
|
+
if (!out.signature_valid) {
|
|
290
|
+
out.errors.push('signature not valid for the trusted key (payload tampered, or wrong --wallet / --pubkey)');
|
|
291
|
+
}
|
|
292
|
+
out.valid = out.signature_valid && out.errors.length === 0;
|
|
293
|
+
out.trusted_pubkey = trustedPubkey;
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
|
|
169
297
|
async function main() {
|
|
170
298
|
const args = process.argv.slice(2);
|
|
171
|
-
const HELP = `twzrd-receipt-verifier -- offline verifier for TWZRD
|
|
299
|
+
const HELP = `twzrd-receipt-verifier -- offline verifier for TWZRD receipts (Ed25519)
|
|
172
300
|
|
|
173
301
|
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.
|
|
302
|
+
TWZRD's published Ed25519 key and was not tampered with. Auto-detects two families:
|
|
303
|
+
- AO-Receipt V5/V6 (trust-API): keccak256 leaf, signed by ${'9V6Pn19...'} (default fetch)
|
|
304
|
+
- cNFT Receipt (genesis anchor): compact-JSON payload, signed by ${DEFAULT_CNFT_PUBKEY.slice(0, 7)}... (built-in)
|
|
175
305
|
|
|
176
306
|
usage:
|
|
177
|
-
twzrd-receipt-verifier <receipt.json|-> [--pubkey KEY] [--base-url URL] [--max-age SECS] [--self-test]
|
|
307
|
+
twzrd-receipt-verifier <receipt.json|-> [--pubkey KEY] [--fetch-key] [--wallet ADDR] [--base-url URL] [--max-age SECS] [--self-test]
|
|
178
308
|
|
|
179
309
|
arguments:
|
|
180
310
|
<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
|
-
|
|
311
|
+
--pubkey KEY trust this base58 Ed25519 pubkey (out-of-band) instead of fetching/built-in
|
|
312
|
+
--fetch-key (cNFT only) fetch the signing key from the published well-known descriptor
|
|
313
|
+
(--base-url or ${DEFAULT_CNFT_BASE_URL}) instead of the built-in copy. Trades
|
|
314
|
+
package-trust for domain/TLS-trust; default stays built-in (no network).
|
|
315
|
+
--wallet ADDR (cNFT only) the leaf-owner wallet, which is part of the signed payload but
|
|
316
|
+
not stored in the anchor block. Inferred from a <wallet>.json filename if omitted.
|
|
317
|
+
--base-url URL where to fetch the key: trust-API default ${DEFAULT_BASE_URL}; cNFT (--fetch-key)
|
|
318
|
+
default ${DEFAULT_CNFT_BASE_URL}
|
|
319
|
+
--max-age SECS replay-resistance policy: reject if the receipt's timestamp (preimage.timestamp_unix
|
|
320
|
+
for trust-API, anchor.minted_at for cNFT) is older than SECS, OR is missing.
|
|
321
|
+
Crypto is time-independent; this is opt-in relying-party policy.
|
|
186
322
|
--self-test additionally confirm a tampered copy FAILS (proves the check works)
|
|
187
323
|
-h, --help show this help
|
|
188
324
|
|
|
189
325
|
exit code: 0 = VALID, 1 = INVALID / error
|
|
190
|
-
key source: ${DEFAULT_BASE_URL}/.well-known/x402
|
|
326
|
+
trust-API key source: ${DEFAULT_BASE_URL}/.well-known/x402
|
|
327
|
+
cNFT key source: ${DEFAULT_CNFT_BASE_URL}/v1/receipts/pubkey`;
|
|
191
328
|
if (args.includes('-h') || args.includes('--help')) { console.log(HELP); process.exit(0); }
|
|
192
329
|
if (args.length === 0) {
|
|
193
|
-
console.error('usage: twzrd-receipt-verifier <receipt.json|-> [--pubkey KEY] [--base-url URL] [--max-age SECS] [--self-test]');
|
|
330
|
+
console.error('usage: twzrd-receipt-verifier <receipt.json|-> [--pubkey KEY] [--fetch-key] [--wallet ADDR] [--base-url URL] [--max-age SECS] [--self-test]');
|
|
194
331
|
console.error(' twzrd-receipt-verifier --help');
|
|
195
332
|
process.exit(1);
|
|
196
333
|
}
|
|
@@ -201,16 +338,71 @@ key source: ${DEFAULT_BASE_URL}/.well-known/x402`;
|
|
|
201
338
|
const maxAgeArg = getOpt('--max-age');
|
|
202
339
|
const maxAge = maxAgeArg ? parseInt(maxAgeArg, 10) : 0; // 0 = freshness check off (opt-in policy)
|
|
203
340
|
|
|
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
341
|
const raw = receiptArg === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(receiptArg, 'utf8');
|
|
208
342
|
const receipt = JSON.parse(raw);
|
|
209
343
|
|
|
344
|
+
// ── cNFT (Bubblegum anchor) receipt: Ed25519 over compact JSON, no keccak leaf ──
|
|
345
|
+
if (isCnftReceipt(receipt)) {
|
|
346
|
+
let trusted = getOpt('--pubkey'), keySrc;
|
|
347
|
+
if (trusted) {
|
|
348
|
+
keySrc = '--pubkey (out-of-band)';
|
|
349
|
+
} else if (args.includes('--fetch-key')) {
|
|
350
|
+
const fetchBase = getOpt('--base-url') || DEFAULT_CNFT_BASE_URL;
|
|
351
|
+
trusted = await fetchCnftPubkey(fetchBase);
|
|
352
|
+
keySrc = `fetched from ${fetchBase}`;
|
|
353
|
+
} else {
|
|
354
|
+
trusted = DEFAULT_CNFT_PUBKEY;
|
|
355
|
+
keySrc = 'built-in genesis authority';
|
|
356
|
+
}
|
|
357
|
+
const { wallet, src: walletSrc } = resolveWallet({
|
|
358
|
+
explicitWallet: getOpt('--wallet'), receipt, receiptPath: receiptArg,
|
|
359
|
+
});
|
|
360
|
+
console.log(`mode : cNFT (Bubblegum anchor)`);
|
|
361
|
+
console.log(`trusted pubkey : ${trusted} [source: ${keySrc}]`);
|
|
362
|
+
console.log(`wallet : ${wallet || '(unknown)'} [source: ${walletSrc}]`);
|
|
363
|
+
|
|
364
|
+
const res = verifyCnft(receipt, trusted, wallet);
|
|
365
|
+
res.errors = res.errors || [];
|
|
366
|
+
|
|
367
|
+
// Opt-in freshness gate (anchor.minted_at). cNFT receipts are long-lived by
|
|
368
|
+
// design, so this is rarely useful, but kept for parity with the trust-API path.
|
|
369
|
+
if (maxAge > 0) {
|
|
370
|
+
const ts = receipt.anchor ? Number(receipt.anchor.minted_at) : NaN;
|
|
371
|
+
if (!Number.isFinite(ts) || ts <= 0) {
|
|
372
|
+
res.errors.push(`--max-age ${maxAge}s set but anchor has no valid minted_at`);
|
|
373
|
+
res.valid = false;
|
|
374
|
+
} else {
|
|
375
|
+
const age = Math.abs(Math.floor(Date.now() / 1000) - ts);
|
|
376
|
+
if (age > maxAge) { res.errors.push(`receipt too old (age ${age}s > --max-age ${maxAge}s)`); res.valid = false; }
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
console.log(`signature_valid : ${res.signature_valid}`);
|
|
381
|
+
res.errors.forEach((e) => console.log(' - ' + e));
|
|
382
|
+
let ok = !!res.valid;
|
|
383
|
+
console.log(`RESULT : ${ok ? 'VALID (TWZRD-authored, untampered)' : 'INVALID'}`);
|
|
384
|
+
|
|
385
|
+
if (selfTest) {
|
|
386
|
+
const tampered = JSON.parse(raw);
|
|
387
|
+
tampered.anchor = tampered.anchor || {};
|
|
388
|
+
tampered.anchor.score_at_mint = (Number(tampered.anchor.score_at_mint) || 0) + 1;
|
|
389
|
+
const t = verifyCnft(tampered, trusted, wallet);
|
|
390
|
+
const passed = !t.valid;
|
|
391
|
+
console.log(`self-test (tampered score must FAIL): ${passed ? 'PASS' : 'BROKEN'}`);
|
|
392
|
+
ok = ok && passed;
|
|
393
|
+
}
|
|
394
|
+
process.exit(ok ? 0 : 1);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── trust-API receipt (V5/V6): keccak256 leaf, signed over the leaf bytes ──
|
|
398
|
+
// keccak self-test: refuse to run with a broken hash backend
|
|
399
|
+
if (keccak256('') !== KECCAK_EMPTY) { console.error('FATAL: keccak256 backend is wrong'); process.exit(1); }
|
|
400
|
+
|
|
210
401
|
let trusted = getOpt('--pubkey'), src;
|
|
211
402
|
if (trusted) { src = '--pubkey (out-of-band)'; }
|
|
212
403
|
else { trusted = await fetchPublishedPubkey(baseUrl); src = baseUrl + '/.well-known/x402'; }
|
|
213
|
-
console.log(`
|
|
404
|
+
console.log(`mode : AO-Receipt (trust-API)`);
|
|
405
|
+
console.log(`trusted pubkey : ${trusted} [source: ${src}]`);
|
|
214
406
|
|
|
215
407
|
const res = verify(receipt, trusted);
|
|
216
408
|
|
|
@@ -251,4 +443,15 @@ key source: ${DEFAULT_BASE_URL}/.well-known/x402`;
|
|
|
251
443
|
process.exit(ok ? 0 : 1);
|
|
252
444
|
}
|
|
253
445
|
|
|
254
|
-
|
|
446
|
+
// Export the pure verifiers for tests / programmatic use. Only run the CLI when
|
|
447
|
+
// invoked directly (so `require()` from the test suite does not trigger main()).
|
|
448
|
+
module.exports = {
|
|
449
|
+
verify, recomputeLeaf,
|
|
450
|
+
verifyCnft, cnftSignedPayload, isCnftReceipt, resolveWallet,
|
|
451
|
+
fetchCnftPubkey,
|
|
452
|
+
DEFAULT_CNFT_PUBKEY, DEFAULT_CNFT_BASE_URL, CNFT_SIGNED_FIELDS,
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
if (require.main === module) {
|
|
456
|
+
main().catch((e) => { console.error('error:', e.message); process.exit(1); });
|
|
457
|
+
}
|