@yantrixai/openproof-verify 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/README.md +114 -0
- package/index.d.ts +56 -0
- package/index.js +173 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# openproof-verify
|
|
2
|
+
|
|
3
|
+
Verify [OpenProof](https://openproof.io) cryptographic signatures on API responses.
|
|
4
|
+
|
|
5
|
+
OpenProof is an open protocol that adds ECDSA signatures to every API response — so AI agents can cryptographically verify that data came from the claimed source and hasn't been tampered with.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install openproof-verify
|
|
11
|
+
# or
|
|
12
|
+
npm install openproof-verify ethers # recommended for full signature recovery
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
const { verify, verifyResponse } = require('openproof-verify')
|
|
19
|
+
|
|
20
|
+
// From a Yantrix API response:
|
|
21
|
+
const response = await fetch('https://agent-registry.yantrix.ai/v1/agents/agt_abc123')
|
|
22
|
+
const json = await response.json()
|
|
23
|
+
|
|
24
|
+
// Verify the signature
|
|
25
|
+
const result = verifyResponse(json)
|
|
26
|
+
|
|
27
|
+
if (result.valid) {
|
|
28
|
+
console.log('✓ Response verified')
|
|
29
|
+
console.log(' Signed by:', result.signer)
|
|
30
|
+
console.log(' Signed at:', new Date(result.signed_at * 1000).toISOString())
|
|
31
|
+
} else {
|
|
32
|
+
console.error('✗ Verification failed:', result.reason)
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### With trusted signers
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
const result = verify(json.data, json._proof, {
|
|
40
|
+
trustedSigners: ['0x41a024c1c89fd30122c8b184de99cbe751eac970'],
|
|
41
|
+
maxAgeSeconds: 60, // reject signatures older than 1 minute
|
|
42
|
+
})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Remote verification
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
const { verifyRemote } = require('openproof-verify')
|
|
49
|
+
|
|
50
|
+
const result = await verifyRemote(
|
|
51
|
+
'https://agent-registry.yantrix.ai',
|
|
52
|
+
json.data,
|
|
53
|
+
json._proof
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## What is OpenProof?
|
|
58
|
+
|
|
59
|
+
OpenProof adds a `_proof` envelope to every API response:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"data": { "price": 2081.41, "symbol": "ETH" },
|
|
64
|
+
"_proof": {
|
|
65
|
+
"call_id": "ytx_abc123def456",
|
|
66
|
+
"endpoint": "/v1/price/ETH",
|
|
67
|
+
"payload_hash": "sha256:9f86d081...",
|
|
68
|
+
"timestamp": 1711234567,
|
|
69
|
+
"signer": "0x41A024c1C89Fd30122c8b184de99cbE751eaC970",
|
|
70
|
+
"signature": "0x3ad7f1..."
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The signature proves:
|
|
76
|
+
1. The response came from the owner of `0x41A024...`
|
|
77
|
+
2. The `data` field hasn't been modified since signing
|
|
78
|
+
3. The response was signed at `timestamp`
|
|
79
|
+
|
|
80
|
+
## API
|
|
81
|
+
|
|
82
|
+
### `verify(data, trust, options?)`
|
|
83
|
+
Verify locally. Returns `{ valid, signer, signed_at, reason }`.
|
|
84
|
+
|
|
85
|
+
### `verifyResponse(response, options?)`
|
|
86
|
+
Verify a full response object with `data` and `_proof` fields.
|
|
87
|
+
|
|
88
|
+
### `verifyRemote(apiBaseUrl, data, trust)`
|
|
89
|
+
Verify via the API's own `/v1/verify/signature` endpoint. Returns a Promise.
|
|
90
|
+
|
|
91
|
+
### `canonical(obj)`
|
|
92
|
+
Serialize an object to canonical JSON (sorted keys, no whitespace).
|
|
93
|
+
|
|
94
|
+
## Options
|
|
95
|
+
|
|
96
|
+
| Option | Type | Default | Description |
|
|
97
|
+
|--------|------|---------|-------------|
|
|
98
|
+
| `trustedSigners` | `string[]` | `undefined` | Allowlist of trusted signer addresses |
|
|
99
|
+
| `maxAgeSeconds` | `number` | `300` | Reject signatures older than this. Set to `0` to disable. |
|
|
100
|
+
|
|
101
|
+
## OpenProof-compliant APIs
|
|
102
|
+
|
|
103
|
+
- [Yantrix Agent Registry](https://agent-registry.yantrix.ai)
|
|
104
|
+
- [Yantrix Agent Session](https://agent-session.yantrix.ai)
|
|
105
|
+
- [Yantrix Relay](https://relay.yantrix.ai)
|
|
106
|
+
- [Full list at openproof.io/registry](https://openproof.io/registry)
|
|
107
|
+
|
|
108
|
+
## Protocol Spec
|
|
109
|
+
|
|
110
|
+
[SPEC.md](https://github.com/yantrix-ai/openproof/blob/main/SPEC.md)
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface TrustEnvelope {
|
|
2
|
+
call_id: string
|
|
3
|
+
endpoint: string
|
|
4
|
+
payload_hash: string
|
|
5
|
+
timestamp: number
|
|
6
|
+
signer: string
|
|
7
|
+
signature: string
|
|
8
|
+
receipt_url?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface VerifyResult {
|
|
12
|
+
valid: boolean
|
|
13
|
+
signer?: string
|
|
14
|
+
signed_at?: number
|
|
15
|
+
call_id?: string
|
|
16
|
+
endpoint?: string
|
|
17
|
+
reason?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface VerifyOptions {
|
|
21
|
+
/** List of trusted signer addresses (lowercase). If provided, signer must be in this list. */
|
|
22
|
+
trustedSigners?: string[]
|
|
23
|
+
/** Maximum age of signature in seconds. 0 = skip age check. Default: 300 (5 minutes) */
|
|
24
|
+
maxAgeSeconds?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Verify a OpenProof signature locally.
|
|
29
|
+
*/
|
|
30
|
+
export function verify(
|
|
31
|
+
data: object,
|
|
32
|
+
trust: TrustEnvelope,
|
|
33
|
+
options?: VerifyOptions
|
|
34
|
+
): VerifyResult
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Verify a OpenProof signature remotely via the API's /v1/verify/signature endpoint.
|
|
38
|
+
*/
|
|
39
|
+
export function verifyRemote(
|
|
40
|
+
apiBaseUrl: string,
|
|
41
|
+
data: object,
|
|
42
|
+
trust: TrustEnvelope
|
|
43
|
+
): Promise<VerifyResult>
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extract and verify OpenProof from a full API response containing `data` and `_proof`.
|
|
47
|
+
*/
|
|
48
|
+
export function verifyResponse(
|
|
49
|
+
response: { data: object; _proof: TrustEnvelope; [key: string]: any },
|
|
50
|
+
options?: VerifyOptions
|
|
51
|
+
): VerifyResult
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Canonicalize an object to deterministic JSON (sorted keys, no whitespace).
|
|
55
|
+
*/
|
|
56
|
+
export function canonical(obj: object): string
|
package/index.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* openproof-verify
|
|
5
|
+
* Verify OpenProof signatures on API responses.
|
|
6
|
+
* https://openproof.io
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const crypto = require('crypto')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Canonicalize an object to deterministic JSON.
|
|
13
|
+
* Sorted keys, no whitespace.
|
|
14
|
+
*/
|
|
15
|
+
function canonical(obj) {
|
|
16
|
+
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
17
|
+
return JSON.stringify(obj)
|
|
18
|
+
}
|
|
19
|
+
const sorted = Object.keys(obj).sort().reduce((acc, key) => {
|
|
20
|
+
acc[key] = obj[key]
|
|
21
|
+
return acc
|
|
22
|
+
}, {})
|
|
23
|
+
return JSON.stringify(sorted, (_, v) => {
|
|
24
|
+
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
25
|
+
return Object.keys(v).sort().reduce((acc, k) => {
|
|
26
|
+
acc[k] = v[k]
|
|
27
|
+
return acc
|
|
28
|
+
}, {})
|
|
29
|
+
}
|
|
30
|
+
return v
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Recover Ethereum address from a personal_sign signature.
|
|
36
|
+
* Works in Node.js without ethers.js dependency.
|
|
37
|
+
*/
|
|
38
|
+
function recoverAddress(message, signature) {
|
|
39
|
+
try {
|
|
40
|
+
// Try using ethers if available
|
|
41
|
+
const ethers = require('ethers')
|
|
42
|
+
return ethers.verifyMessage(message, signature).toLowerCase()
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// Fallback: manual recovery using secp256k1
|
|
45
|
+
try {
|
|
46
|
+
const { ecdsaRecover, publicKeyConvert } = require('secp256k1')
|
|
47
|
+
const prefix = `\x19Ethereum Signed Message:\n${Buffer.byteLength(message)}`
|
|
48
|
+
const msgHash = crypto.createHash('sha256')
|
|
49
|
+
.update(Buffer.from(prefix + message))
|
|
50
|
+
.digest()
|
|
51
|
+
const sigBuf = Buffer.from(signature.replace('0x', ''), 'hex')
|
|
52
|
+
const r = sigBuf.slice(0, 32)
|
|
53
|
+
const s = sigBuf.slice(32, 64)
|
|
54
|
+
let v = sigBuf[64]
|
|
55
|
+
if (v >= 27) v -= 27
|
|
56
|
+
const pubKey = ecdsaRecover(Buffer.concat([r, s]), v, msgHash, false)
|
|
57
|
+
const pubKeyHash = crypto.createHash('sha256').update(pubKeyConvert(pubKey, false).slice(1)).digest()
|
|
58
|
+
return '0x' + pubKeyHash.slice(-20).toString('hex').toLowerCase()
|
|
59
|
+
} catch (e2) {
|
|
60
|
+
throw new Error(`Cannot recover address: no ethers or secp256k1 available. Install ethers: npm install ethers`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Verify a OpenProof signature.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} data - The `data` field from the API response
|
|
69
|
+
* @param {object} trust - The `_proof` envelope from the API response
|
|
70
|
+
* @param {object} [options] - Verification options
|
|
71
|
+
* @param {string[]} [options.trustedSigners] - List of trusted signer addresses (lowercase)
|
|
72
|
+
* @param {number} [options.maxAgeSeconds=300] - Max age of signature in seconds (0 = skip)
|
|
73
|
+
* @returns {{ valid: boolean, signer?: string, signed_at?: number, reason?: string }}
|
|
74
|
+
*/
|
|
75
|
+
function verify(data, trust, options = {}) {
|
|
76
|
+
const { trustedSigners, maxAgeSeconds = 300 } = options
|
|
77
|
+
|
|
78
|
+
if (!trust || typeof trust !== 'object') {
|
|
79
|
+
return { valid: false, reason: 'Missing _proof envelope' }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const { call_id, endpoint, payload_hash, timestamp, signer, signature } = trust
|
|
83
|
+
|
|
84
|
+
if (!call_id || !endpoint || !payload_hash || !timestamp || !signer || !signature) {
|
|
85
|
+
return { valid: false, reason: 'Incomplete _proof envelope — missing required fields' }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Step 1: Verify payload hash
|
|
89
|
+
const canonicalData = canonical(data)
|
|
90
|
+
const computedHash = crypto.createHash('sha256').update(canonicalData).digest('hex')
|
|
91
|
+
const expectedHash = payload_hash.replace('sha256:', '')
|
|
92
|
+
|
|
93
|
+
if (computedHash !== expectedHash) {
|
|
94
|
+
return { valid: false, reason: 'Payload hash mismatch — data may have been tampered with' }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Step 2: Check timestamp freshness
|
|
98
|
+
if (maxAgeSeconds > 0) {
|
|
99
|
+
const now = Math.floor(Date.now() / 1000)
|
|
100
|
+
const age = now - timestamp
|
|
101
|
+
if (age > maxAgeSeconds) {
|
|
102
|
+
return { valid: false, reason: `Signature expired — ${age}s old, max ${maxAgeSeconds}s` }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Step 3: Recover signer from signature
|
|
107
|
+
const message = `${call_id}:${endpoint}:sha256:${computedHash}:${timestamp}`
|
|
108
|
+
let recovered
|
|
109
|
+
try {
|
|
110
|
+
recovered = recoverAddress(message, signature)
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return { valid: false, reason: `Signature recovery failed: ${e.message}` }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (recovered !== signer.toLowerCase()) {
|
|
116
|
+
return { valid: false, reason: `Signer mismatch — expected ${signer}, got ${recovered}` }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Step 4: Check trusted signers (optional)
|
|
120
|
+
if (trustedSigners && trustedSigners.length > 0) {
|
|
121
|
+
const normalizedTrusted = trustedSigners.map(s => s.toLowerCase())
|
|
122
|
+
if (!normalizedTrusted.includes(recovered)) {
|
|
123
|
+
return { valid: false, reason: `Signer ${recovered} not in trusted signers list` }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
valid: true,
|
|
129
|
+
signer: recovered,
|
|
130
|
+
signed_at: timestamp,
|
|
131
|
+
call_id,
|
|
132
|
+
endpoint,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Verify a OpenProof signature asynchronously (remote verification).
|
|
138
|
+
* Calls the API's /v1/verify/signature endpoint.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} apiBaseUrl - Base URL of the API (e.g. https://agent-registry.yantrix.ai)
|
|
141
|
+
* @param {object} data - The `data` field from the response
|
|
142
|
+
* @param {object} trust - The `_proof` envelope from the response
|
|
143
|
+
* @returns {Promise<{ valid: boolean, signer?: string, signed_at?: number }>}
|
|
144
|
+
*/
|
|
145
|
+
async function verifyRemote(apiBaseUrl, data, trust) {
|
|
146
|
+
const url = `${apiBaseUrl.replace(/\/$/, '')}/v1/verify/signature`
|
|
147
|
+
const response = await fetch(url, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: { 'Content-Type': 'application/json' },
|
|
150
|
+
body: JSON.stringify({ envelope: trust, payload: data }),
|
|
151
|
+
})
|
|
152
|
+
const result = await response.json()
|
|
153
|
+
return result.data || result
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Extract and verify OpenProof from a full API response object.
|
|
158
|
+
*
|
|
159
|
+
* @param {object} response - Full API response containing `data` and `_proof`
|
|
160
|
+
* @param {object} [options] - Same options as verify()
|
|
161
|
+
*/
|
|
162
|
+
function verifyResponse(response, options = {}) {
|
|
163
|
+
if (!response || typeof response !== 'object') {
|
|
164
|
+
return { valid: false, reason: 'Invalid response object' }
|
|
165
|
+
}
|
|
166
|
+
const { data, _proof } = response
|
|
167
|
+
if (!_proof) {
|
|
168
|
+
return { valid: false, reason: 'Response does not include _proof envelope — not OpenProof compliant' }
|
|
169
|
+
}
|
|
170
|
+
return verify(data, _proof, options)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = { verify, verifyRemote, verifyResponse, canonical }
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yantrixai/openproof-verify",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Verify OpenProof cryptographic signatures on API responses. For AI agents that need to trust external data.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node test.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"openproof",
|
|
12
|
+
"api",
|
|
13
|
+
"signature",
|
|
14
|
+
"verification",
|
|
15
|
+
"ecdsa",
|
|
16
|
+
"ethereum",
|
|
17
|
+
"ai-agents",
|
|
18
|
+
"x402",
|
|
19
|
+
"yantrix"
|
|
20
|
+
],
|
|
21
|
+
"author": "Yantrix <hello@yantrix.ai>",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/yantrix-ai/openproof"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://openproof.io",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"ethers": ">=5.0.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependenciesMeta": {
|
|
32
|
+
"ethers": {
|
|
33
|
+
"optional": true
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|