twzrd-receipt-verifier 1.0.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/LICENSE +21 -0
- package/README.md +119 -0
- package/package.json +33 -0
- package/verify_twzrd_receipt.js +159 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TWZRD
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# TWZRD Receipt Verifier (standalone)
|
|
2
|
+
|
|
3
|
+
Verify a TWZRD **AO-Receipt V5** offline, trusting **nothing from TWZRD's servers
|
|
4
|
+
or codebase** - only the receipt, TWZRD's published public key, and two
|
|
5
|
+
widely-audited crypto libraries.
|
|
6
|
+
|
|
7
|
+
A TWZRD trust receipt has two layers:
|
|
8
|
+
|
|
9
|
+
1. **Tamper-evidence** - a `keccak256` leaf over the receipt's preimage fields.
|
|
10
|
+
2. **Authenticity** - an **Ed25519 signature** over the leaf bytes, made with
|
|
11
|
+
TWZRD's dedicated receipt-signing key.
|
|
12
|
+
|
|
13
|
+
This tool recomputes the leaf **and** checks the signature against the published
|
|
14
|
+
key. If it says `VALID`, the receipt was authored by TWZRD and was not altered.
|
|
15
|
+
Unsigned, wrong-key, or tampered receipts fail.
|
|
16
|
+
|
|
17
|
+
## The published signing key
|
|
18
|
+
|
|
19
|
+
| field | value |
|
|
20
|
+
|-------|-------|
|
|
21
|
+
| algorithm | `ed25519` |
|
|
22
|
+
| key_id | `twzrd-receipt-ed25519-v1` |
|
|
23
|
+
| public key (base58) | `9V6Pn19kiUA5Rn6JpQfNduanvGt2aXGwsarosNfa2Ldf` |
|
|
24
|
+
|
|
25
|
+
Also published, machine-readable, at:
|
|
26
|
+
- `https://intel.twzrd.xyz/.well-known/x402` → `receipt.signature.public_key`
|
|
27
|
+
- `https://intel.twzrd.xyz/openapi.json` → `x402.receipt.signature.public_key`
|
|
28
|
+
- the MCP card `agent-intel-mcp-card.json` → `receipt_signing.public_key`
|
|
29
|
+
|
|
30
|
+
> **Most paranoid mode:** pin the key out-of-band with `--pubkey` instead of
|
|
31
|
+
> fetching it, so you never trust the live endpoint to tell you which key to trust.
|
|
32
|
+
|
|
33
|
+
## Get a receipt to verify
|
|
34
|
+
|
|
35
|
+
Any TWZRD V5 receipt works. To mint a fresh one, pay the trust endpoint (x402,
|
|
36
|
+
0.05 USDC on Solana mainnet) - e.g. via AgentCash:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx agentcash@latest fetch https://intel.twzrd.xyz/v1/intel/trust/<PUBKEY> > resp.json
|
|
40
|
+
# the receipt is the `twzrd_receipt` object in the response
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The receipt object looks like:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"version": "v5",
|
|
48
|
+
"leaf": "0x...",
|
|
49
|
+
"preimage": { "domain": "TWZRD:AO_REPUTATION_RECEIPT_V5", "agent_id": "...", "score": 15, "...": "..." },
|
|
50
|
+
"signature": "base58 ed25519 sig",
|
|
51
|
+
"signing_pubkey": "9V6Pn19kiUA5Rn6JpQfNduanvGt2aXGwsarosNfa2Ldf",
|
|
52
|
+
"key_id": "twzrd-receipt-ed25519-v1",
|
|
53
|
+
"signing_alg": "ed25519"
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Python
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install pynacl pycryptodome # libsodium Ed25519 + original Keccak-256
|
|
61
|
+
|
|
62
|
+
# fetch the published key and verify:
|
|
63
|
+
python verify_twzrd_receipt.py receipt.json
|
|
64
|
+
|
|
65
|
+
# pin the key out-of-band (recommended):
|
|
66
|
+
python verify_twzrd_receipt.py receipt.json --pubkey 9V6Pn19kiUA5Rn6JpQfNduanvGt2aXGwsarosNfa2Ldf
|
|
67
|
+
|
|
68
|
+
# also confirm a tampered copy FAILS:
|
|
69
|
+
python verify_twzrd_receipt.py receipt.json --self-test
|
|
70
|
+
|
|
71
|
+
# from stdin:
|
|
72
|
+
cat receipt.json | python verify_twzrd_receipt.py -
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Node
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install # tweetnacl + js-sha3 + bs58
|
|
79
|
+
|
|
80
|
+
node verify_twzrd_receipt.js receipt.json
|
|
81
|
+
node verify_twzrd_receipt.js receipt.json --pubkey 9V6Pn19kiUA5Rn6JpQfNduanvGt2aXGwsarosNfa2Ldf --self-test
|
|
82
|
+
cat receipt.json | node verify_twzrd_receipt.js -
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Both exit `0` on `VALID`, `1` on `INVALID`.
|
|
86
|
+
|
|
87
|
+
## What it checks (and the exact layout)
|
|
88
|
+
|
|
89
|
+
The keccak256 leaf preimage is a strict little-endian, length-prefixed concat
|
|
90
|
+
(reproducible in any language):
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
domain = "TWZRD:AO_REPUTATION_RECEIPT_V5" (or ...ATTENTION... for attention receipts)
|
|
94
|
+
agent_id = u16_le(len(utf8)) || utf8 bytes
|
|
95
|
+
score = u16_le
|
|
96
|
+
confidence_bps = u16_le
|
|
97
|
+
timestamp_unix = u64_le
|
|
98
|
+
payer = 32 bytes (base58-decoded pubkey, or sha256(marker) for synthetic payers)
|
|
99
|
+
settlement_anchor = 32 bytes (last 32 bytes of the utf-8 settlement_tx string, or 32 zero bytes)
|
|
100
|
+
|
|
101
|
+
leaf = keccak256(domain || agent_id || score || confidence_bps || timestamp_unix || payer || settlement_anchor)
|
|
102
|
+
signature = Ed25519_sign(receipt_signing_key, leaf_bytes)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The verifier:
|
|
106
|
+
1. recomputes `leaf` from the preimage and compares it to `receipt.leaf`,
|
|
107
|
+
2. confirms `receipt.signing_pubkey` (if present) equals the trusted key,
|
|
108
|
+
3. verifies the Ed25519 `signature` over the 32 leaf bytes against the trusted key.
|
|
109
|
+
|
|
110
|
+
`VALID` requires all three. The `settlement_tx` in the preimage is an on-chain
|
|
111
|
+
Solana signature you can independently check for ground truth.
|
|
112
|
+
|
|
113
|
+
## Trust assumptions
|
|
114
|
+
|
|
115
|
+
You trust: the receipt you were given, the published public key (ideally pinned),
|
|
116
|
+
and the crypto libraries (`PyNaCl`/libsodium, `pycryptodome`; `tweetnacl`,
|
|
117
|
+
`js-sha3`). You do **not** trust TWZRD's API, database, or this repository's other
|
|
118
|
+
code. Swap the libraries for your own if you prefer - the byte layout above is the
|
|
119
|
+
whole spec.
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "twzrd-receipt-verifier",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Standalone offline verifier for TWZRD AO-Receipt V5 (Ed25519-signed keccak256 leaf). No trust in TWZRD servers or code.",
|
|
5
|
+
"keywords": ["twzrd", "x402", "solana", "ed25519", "keccak256", "receipt", "verifier", "agent", "attestation"],
|
|
6
|
+
"homepage": "https://intel.twzrd.xyz",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/twzrd-sol/wzrd-final.git",
|
|
10
|
+
"directory": "packages/twzrd-agent-intel/verifier"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "TWZRD",
|
|
14
|
+
"bin": {
|
|
15
|
+
"verify-twzrd-receipt": "./verify_twzrd_receipt.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"verify_twzrd_receipt.js",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"verify": "node verify_twzrd_receipt.js"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=16"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"bs58": "^4.0.1",
|
|
30
|
+
"js-sha3": "^0.8.0",
|
|
31
|
+
"tweetnacl": "^1.0.3"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Standalone offline verifier for TWZRD AO-Receipt V5 (Node).
|
|
4
|
+
*
|
|
5
|
+
* Verifies, with NO trust in TWZRD's servers or codebase, that a receipt was
|
|
6
|
+
* authored by TWZRD's published Ed25519 key and was not tampered with:
|
|
7
|
+
* 1. TAMPER-EVIDENCE - recompute the keccak256 leaf from the preimage,
|
|
8
|
+
* confirm it equals receipt.leaf.
|
|
9
|
+
* 2. AUTHENTICITY - verify the Ed25519 signature over the leaf bytes
|
|
10
|
+
* against TWZRD's PUBLISHED public key (you supply / fetch it).
|
|
11
|
+
*
|
|
12
|
+
* Crypto comes from audited libs (tweetnacl = ref Ed25519, js-sha3 = Keccak),
|
|
13
|
+
* not from this script. base58 + the TWZRD byte layout are the only logic here.
|
|
14
|
+
*
|
|
15
|
+
* npm install tweetnacl js-sha3 bs58
|
|
16
|
+
*
|
|
17
|
+
* node verify_twzrd_receipt.js receipt.json
|
|
18
|
+
* node verify_twzrd_receipt.js receipt.json --pubkey 9V6Pn19kiUA5Rn6JpQfNduanvGt2aXGwsarosNfa2Ldf
|
|
19
|
+
* cat receipt.json | node verify_twzrd_receipt.js - # stdin
|
|
20
|
+
* node verify_twzrd_receipt.js receipt.json --self-test # tamper must fail
|
|
21
|
+
*
|
|
22
|
+
* Exit code 0 = VALID, 1 = INVALID / error.
|
|
23
|
+
*/
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const crypto = require('crypto');
|
|
28
|
+
const https = require('https');
|
|
29
|
+
const nacl = require('tweetnacl');
|
|
30
|
+
const { keccak256 } = require('js-sha3');
|
|
31
|
+
const bs58 = require('bs58');
|
|
32
|
+
|
|
33
|
+
const DEFAULT_BASE_URL = 'https://intel.twzrd.xyz';
|
|
34
|
+
const KECCAK_EMPTY = 'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470';
|
|
35
|
+
|
|
36
|
+
function b58decode(s) { return Buffer.from(bs58.decode(s)); }
|
|
37
|
+
|
|
38
|
+
function u16le(n) { const b = Buffer.alloc(2); b.writeUInt16LE(n & 0xffff, 0); return b; }
|
|
39
|
+
function u64le(n) { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(n), 0); return b; }
|
|
40
|
+
|
|
41
|
+
function payer32(payer) {
|
|
42
|
+
try { const raw = b58decode(payer); if (raw.length === 32) return raw; } catch (_) {}
|
|
43
|
+
return crypto.createHash('sha256').update(payer, 'utf8').digest();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function anchor32(tx) {
|
|
47
|
+
if (!tx) return Buffer.alloc(32);
|
|
48
|
+
const raw = Buffer.from(tx, 'utf8');
|
|
49
|
+
if (raw.length >= 32) return raw.subarray(raw.length - 32);
|
|
50
|
+
return Buffer.concat([Buffer.alloc(32 - raw.length), raw]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function recomputeLeaf(pre) {
|
|
54
|
+
const dom = String(pre.domain || '').toUpperCase();
|
|
55
|
+
const isAttention = dom.includes('ATTENTION');
|
|
56
|
+
const domain = Buffer.from(isAttention ? 'TWZRD:AO_ATTENTION_RECEIPT_V5' : 'TWZRD:AO_REPUTATION_RECEIPT_V5', 'ascii');
|
|
57
|
+
const score = isAttention ? (pre.attention_score || 0) : (pre.score || 0);
|
|
58
|
+
const agent = Buffer.from(pre.agent_id, 'utf8');
|
|
59
|
+
const msg = Buffer.concat([
|
|
60
|
+
domain,
|
|
61
|
+
u16le(agent.length), agent,
|
|
62
|
+
u16le(score),
|
|
63
|
+
u16le(pre.confidence_bps),
|
|
64
|
+
u64le(pre.timestamp_unix),
|
|
65
|
+
payer32(pre.payer),
|
|
66
|
+
anchor32(pre.settlement_tx || pre.settlement_anchor),
|
|
67
|
+
]);
|
|
68
|
+
return Buffer.from(keccak256.arrayBuffer(msg));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function fetchPublishedPubkey(baseUrl) {
|
|
72
|
+
const url = baseUrl.replace(/\/+$/, '') + '/.well-known/x402';
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
https.get(url, { headers: { 'User-Agent': 'twzrd-receipt-verifier/1.0' } }, (res) => {
|
|
75
|
+
let body = '';
|
|
76
|
+
res.on('data', (c) => (body += c));
|
|
77
|
+
res.on('end', () => {
|
|
78
|
+
try { resolve(JSON.parse(body).receipt.signature.public_key); }
|
|
79
|
+
catch (e) { reject(e); }
|
|
80
|
+
});
|
|
81
|
+
}).on('error', reject);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function verify(receipt, trustedPubkey) {
|
|
86
|
+
const out = { leaf_valid: false, signature_valid: false, errors: [] };
|
|
87
|
+
const pre = receipt.preimage || {};
|
|
88
|
+
const leafHex = String(receipt.leaf || '').toLowerCase().replace(/^0x/, '');
|
|
89
|
+
|
|
90
|
+
let recomputed;
|
|
91
|
+
try { recomputed = recomputeLeaf(pre); }
|
|
92
|
+
catch (e) { out.errors.push('could not recompute leaf: ' + e.message); return out; }
|
|
93
|
+
out.recomputed_leaf = '0x' + recomputed.toString('hex');
|
|
94
|
+
out.leaf_valid = recomputed.toString('hex') === leafHex;
|
|
95
|
+
if (!out.leaf_valid) out.errors.push('leaf mismatch: preimage does not hash to receipt.leaf');
|
|
96
|
+
|
|
97
|
+
const sig = receipt.signature;
|
|
98
|
+
if (!sig) { out.errors.push('missing signature (unsigned receipts are rejected)'); return out; }
|
|
99
|
+
|
|
100
|
+
const embedded = receipt.signing_pubkey;
|
|
101
|
+
if (embedded && embedded !== trustedPubkey) {
|
|
102
|
+
out.errors.push(`signing_pubkey ${embedded} != trusted published key ${trustedPubkey}`);
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
out.signature_valid = nacl.sign.detached.verify(
|
|
108
|
+
new Uint8Array(recomputed),
|
|
109
|
+
new Uint8Array(b58decode(sig)),
|
|
110
|
+
new Uint8Array(b58decode(trustedPubkey)),
|
|
111
|
+
);
|
|
112
|
+
} catch (e) { out.errors.push('signature check error: ' + e.message); return out; }
|
|
113
|
+
if (!out.signature_valid) out.errors.push('signature not valid for the trusted published key');
|
|
114
|
+
|
|
115
|
+
out.valid = out.leaf_valid && out.signature_valid && out.errors.length === 0;
|
|
116
|
+
out.trusted_pubkey = trustedPubkey;
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function main() {
|
|
121
|
+
const args = process.argv.slice(2);
|
|
122
|
+
if (args.length === 0) { console.error('usage: verify_twzrd_receipt.js <receipt.json|-> [--pubkey KEY] [--base-url URL] [--self-test]'); process.exit(1); }
|
|
123
|
+
const receiptArg = args[0];
|
|
124
|
+
const getOpt = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
|
|
125
|
+
const selfTest = args.includes('--self-test');
|
|
126
|
+
const baseUrl = getOpt('--base-url') || DEFAULT_BASE_URL;
|
|
127
|
+
|
|
128
|
+
// keccak self-test: refuse to run with a broken hash backend
|
|
129
|
+
if (keccak256('') !== KECCAK_EMPTY) { console.error('FATAL: keccak256 backend is wrong'); process.exit(1); }
|
|
130
|
+
|
|
131
|
+
const raw = receiptArg === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(receiptArg, 'utf8');
|
|
132
|
+
const receipt = JSON.parse(raw);
|
|
133
|
+
|
|
134
|
+
let trusted = getOpt('--pubkey'), src;
|
|
135
|
+
if (trusted) { src = '--pubkey (out-of-band)'; }
|
|
136
|
+
else { trusted = await fetchPublishedPubkey(baseUrl); src = baseUrl + '/.well-known/x402'; }
|
|
137
|
+
console.log(`trusted pubkey: ${trusted} [source: ${src}]`);
|
|
138
|
+
|
|
139
|
+
const res = verify(receipt, trusted);
|
|
140
|
+
console.log(`leaf_valid : ${res.leaf_valid}`);
|
|
141
|
+
console.log(`signature_valid : ${res.signature_valid}`);
|
|
142
|
+
res.errors.forEach((e) => console.log(' - ' + e));
|
|
143
|
+
let ok = !!res.valid;
|
|
144
|
+
console.log(`RESULT : ${ok ? 'VALID (TWZRD-authored, untampered)' : 'INVALID'}`);
|
|
145
|
+
|
|
146
|
+
if (selfTest) {
|
|
147
|
+
const tampered = JSON.parse(raw);
|
|
148
|
+
tampered.preimage = tampered.preimage || {};
|
|
149
|
+
tampered.preimage.score = (tampered.preimage.score || 0) + 1;
|
|
150
|
+
const t = verify(tampered, trusted);
|
|
151
|
+
const passed = !t.valid;
|
|
152
|
+
console.log(`self-test (tampered score must FAIL): ${passed ? 'PASS' : 'BROKEN'}`);
|
|
153
|
+
ok = ok && passed;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
process.exit(ok ? 0 : 1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main().catch((e) => { console.error('error:', e.message); process.exit(1); });
|