yihe-verify 0.0.0-stage → 0.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 +91 -2
- package/index.js +150 -0
- package/package.json +34 -4
package/README.md
CHANGED
|
@@ -1,3 +1,92 @@
|
|
|
1
|
-
#
|
|
1
|
+
# yihe-verify
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
验证一个「AI 能力节点」的**能力声明是否被篡改**,并列出一眼看清它能做什么、入口在哪、多少钱。
|
|
4
|
+
|
|
5
|
+
**零依赖**(只用 Node 内置 `crypto` / `fetch`),Node >= 18。
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx yihe-verify https://<节点地址>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
输出示例:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
节点 : https://node.example.com
|
|
15
|
+
验签结果 : ✅ 通过 (ok)
|
|
16
|
+
算法/keyId : ed25519 / 8c34dda1
|
|
17
|
+
被签字节 : 11113 sha256=089432d8b35f3483…
|
|
18
|
+
|
|
19
|
+
该节点声明的能力(8 个):
|
|
20
|
+
------------------------------------------------------------------------
|
|
21
|
+
[1] broadcast (live)
|
|
22
|
+
广播节点(broadcast)
|
|
23
|
+
https://node.example.com/ai-broadcast.md
|
|
24
|
+
[3] core (live)
|
|
25
|
+
认知核心(core / YiHe 认知核心)
|
|
26
|
+
https://node.example.com/ask
|
|
27
|
+
...
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 为什么需要验签
|
|
31
|
+
|
|
32
|
+
AI 能力节点把「我能干什么、入口在哪、怎么计费」写成一份**机器可读的能力声明**(`ai-broadcast.md`),
|
|
33
|
+
任何 AI 读完就能直接调用。
|
|
34
|
+
|
|
35
|
+
问题在于:**如果这份声明能被改写,攻击者就能把入口换成自己的** ——
|
|
36
|
+
调用方的请求(将来还有凭据和钱)就送到攻击者手里。
|
|
37
|
+
|
|
38
|
+
所以节点用 **ed25519** 给声明签名,并公开公钥:**任何人**都能离线验签,
|
|
39
|
+
证明「这份声明确实是这个节点发出的、且一个字节都没被改」。
|
|
40
|
+
|
|
41
|
+
## 它验证了什么
|
|
42
|
+
|
|
43
|
+
| 步骤 | 请求 |
|
|
44
|
+
|---|---|
|
|
45
|
+
| 1 | `GET /ai-broadcast.md` —— 能力声明(被签名的正文) |
|
|
46
|
+
| 2 | `GET /ai-broadcast.md.sig` —— 分离式签名(base64) |
|
|
47
|
+
| 3 | `GET /pubkey` —— 公钥(PEM)与 `keyId` |
|
|
48
|
+
| 4 | 用公钥对「**签名区块之前**的正文」做 ed25519 验签 |
|
|
49
|
+
|
|
50
|
+
> **签名范围**刻意不做任何 trim:正文一个字节变了就验不过。这正是防篡改要的。
|
|
51
|
+
|
|
52
|
+
## 作为库使用
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import { verifyNode } from 'yihe-verify';
|
|
56
|
+
|
|
57
|
+
const r = await verifyNode('https://node.example.com');
|
|
58
|
+
if (!r.ok) throw new Error(`广播验签失败: ${r.reason}`);
|
|
59
|
+
|
|
60
|
+
console.log(r.keyId, r.services.map((s) => s.name));
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
返回:
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
{
|
|
67
|
+
ok: true, base, algorithm: 'ed25519', keyId: '8c34dda1',
|
|
68
|
+
payloadBytes, sha256, services: [{ index, name, kind, title, description, entry, call, returns, status }]
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 安全提示(别过度信任)
|
|
73
|
+
|
|
74
|
+
1. **验签通过 ≠ 合规、≠ 服务质量**:只说明「是这把钥签的、内容没被改」。
|
|
75
|
+
2. **首次获取仍有 TOFU 问题**:公钥也是从该节点取的。若攻击者能在你首次获取时同时替换清单与公钥,首次仍可能被骗。
|
|
76
|
+
**建议固定 `keyId`**:同一节点 `keyId` 变了就该警觉。
|
|
77
|
+
3. **签名保护内容,TLS 保护传输**:两者互补 —— 签名让声明**被镜像/转发到别处**也照样能验真。
|
|
78
|
+
|
|
79
|
+
## 自己运营一个节点?
|
|
80
|
+
|
|
81
|
+
这套东西是「AI 能力网络」的一部分:任何人都能把自己的服务变成一个**AI 可直接调用的入口**,
|
|
82
|
+
调用方充值走**运营者自己的收款账户**(平台不在调用上抽成),平台费 ¥1/天(满月封顶 ¥29)。
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
curl -fsSL https://<平台地址>/install.sh | sudo bash
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
谈清楚再决定:`https://<平台地址>/join`(含**不承诺流量与收入**等如实说明)。
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* yihe-verify · 验证「AI 能力节点」广播清单的 ed25519 签名,并列出它声明的能力。
|
|
4
|
+
*
|
|
5
|
+
* 为什么需要它:AI 能力节点把「我能干什么、入口在哪、多少钱」写在广播清单(ai-broadcast.md)里。
|
|
6
|
+
* 如果这份声明能被改写,攻击者就能把入口换成自己的 —— 调用方的请求(甚至凭据)就送到别人手里。
|
|
7
|
+
* 所以节点用 ed25519 给清单签名;**任何人**都能用它的公钥离线验签。
|
|
8
|
+
*
|
|
9
|
+
* 零依赖(只用 Node 内置 crypto / fetch),Node >= 18。
|
|
10
|
+
*
|
|
11
|
+
* 用法:
|
|
12
|
+
* npx yihe-verify https://<节点地址>
|
|
13
|
+
* npx yihe-verify https://<节点地址> --json
|
|
14
|
+
*
|
|
15
|
+
* 作为库:
|
|
16
|
+
* import { verifyNode } from 'yihe-verify';
|
|
17
|
+
* const r = await verifyNode('https://<节点地址>');
|
|
18
|
+
* if (!r.ok) throw new Error('广播验签失败');
|
|
19
|
+
*/
|
|
20
|
+
import crypto from 'node:crypto';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
/** 清单里签名区块的起始标题(签名范围 = 该行**之前**的全部文本) */
|
|
24
|
+
export const SIG_HEADING = '## 签名(出生即带)';
|
|
25
|
+
|
|
26
|
+
/** 把清单切成「被签名的正文」与「签名区块」。 */
|
|
27
|
+
export function splitManifest(markdown) {
|
|
28
|
+
const i = markdown.indexOf(SIG_HEADING);
|
|
29
|
+
return { body: i >= 0 ? markdown.slice(0, i) : markdown, signed: i >= 0 };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 从正文里取出机器可读的能力清单(```json 区块)。 */
|
|
33
|
+
export function parseServices(body) {
|
|
34
|
+
const m = body.match(/```json\s*([\s\S]*?)```/);
|
|
35
|
+
if (!m) return [];
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(m[1]).services || [];
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function get(url, timeout) {
|
|
44
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(timeout) });
|
|
45
|
+
if (!res.ok) throw new Error(`${url} -> HTTP ${res.status}`);
|
|
46
|
+
return res.text();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 验证一个节点的广播清单。
|
|
51
|
+
* @param {string} base 节点地址,如 https://node.example.com
|
|
52
|
+
* @param {{timeout?: number}} [opts]
|
|
53
|
+
* @returns {Promise<{ok: boolean, reason?: string, base: string, keyId?: string,
|
|
54
|
+
* payloadBytes?: number, sha256?: string, services?: any[]}>}
|
|
55
|
+
*/
|
|
56
|
+
export async function verifyNode(base, opts = {}) {
|
|
57
|
+
const timeout = opts.timeout ?? 10000;
|
|
58
|
+
const b = String(base).replace(/\/+$/, '');
|
|
59
|
+
if (!/^https?:\/\//.test(b)) return { ok: false, base: b, reason: '地址必须以 http(s):// 开头' };
|
|
60
|
+
let md, sigText, key;
|
|
61
|
+
try {
|
|
62
|
+
md = await get(b + '/ai-broadcast.md', timeout);
|
|
63
|
+
sigText = (await get(b + '/ai-broadcast.md.sig', timeout)).trim();
|
|
64
|
+
key = JSON.parse(await get(b + '/pubkey', timeout));
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return { ok: false, base: b, reason: `取清单/签名/公钥失败: ${e.message}` };
|
|
67
|
+
}
|
|
68
|
+
const { body, signed } = splitManifest(md);
|
|
69
|
+
if (!signed) return { ok: false, base: b, reason: '清单没有签名区块(该节点未签名)' };
|
|
70
|
+
if (!key.publicKeyPem) return { ok: false, base: b, reason: '公钥缺失' };
|
|
71
|
+
|
|
72
|
+
let ok = false;
|
|
73
|
+
try {
|
|
74
|
+
const sig = Buffer.from(sigText, 'base64');
|
|
75
|
+
ok = crypto.verify(null, Buffer.from(body), crypto.createPublicKey(key.publicKeyPem), sig);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return { ok: false, base: b, reason: `验签异常: ${e.message}` };
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
ok,
|
|
81
|
+
base: b,
|
|
82
|
+
reason: ok ? 'ok' : '签名不匹配(清单被改过,或公钥不是签发者)',
|
|
83
|
+
algorithm: key.algorithm || 'ed25519',
|
|
84
|
+
keyId: key.keyId || '',
|
|
85
|
+
payloadBytes: Buffer.byteLength(body, 'utf8'),
|
|
86
|
+
sha256: crypto.createHash('sha256').update(body).digest('hex'),
|
|
87
|
+
services: parseServices(body),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ------------------------------- CLI -------------------------------
|
|
92
|
+
function usage() {
|
|
93
|
+
console.log(`yihe-verify · 验证 AI 能力节点广播签名(ed25519,零依赖)
|
|
94
|
+
|
|
95
|
+
用法:
|
|
96
|
+
npx yihe-verify <节点地址> [--json]
|
|
97
|
+
|
|
98
|
+
例:
|
|
99
|
+
npx yihe-verify https://node.example.com
|
|
100
|
+
npx yihe-verify https://node.example.com --json
|
|
101
|
+
|
|
102
|
+
它验证什么:
|
|
103
|
+
1. GET <节点>/ai-broadcast.md 能力声明(被签名)
|
|
104
|
+
2. GET <节点>/ai-broadcast.md.sig 分离式签名
|
|
105
|
+
3. GET <节点>/pubkey 公钥
|
|
106
|
+
4. 用公钥对「签名区块之前的正文」做 ed25519 验签 → 一个字节改动即失败
|
|
107
|
+
|
|
108
|
+
安全提示: 验签通过只说明「是这把钥签的、内容没被改」,
|
|
109
|
+
不代表内容合规,也不代表服务质量。首次获取公钥仍有 TOFU 问题 ——
|
|
110
|
+
建议固定 keyId(变了就该警觉)。`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function main() {
|
|
114
|
+
const argv = process.argv.slice(2);
|
|
115
|
+
if (!argv.length || argv.includes('-h') || argv.includes('--help')) {
|
|
116
|
+
usage();
|
|
117
|
+
process.exit(argv.length ? 0 : 2);
|
|
118
|
+
}
|
|
119
|
+
const json = argv.includes('--json');
|
|
120
|
+
const target = argv.find((a) => !a.startsWith('-'));
|
|
121
|
+
const r = await verifyNode(target);
|
|
122
|
+
if (json) {
|
|
123
|
+
console.log(JSON.stringify(r, null, 2));
|
|
124
|
+
process.exit(r.ok ? 0 : 1);
|
|
125
|
+
}
|
|
126
|
+
console.log(`\n 节点 : ${r.base}`);
|
|
127
|
+
console.log(` 验签结果 : ${r.ok ? '✅ 通过' : '❌ 失败'} (${r.reason})`);
|
|
128
|
+
if (r.keyId) {
|
|
129
|
+
console.log(` 算法/keyId : ${r.algorithm} / ${r.keyId}`);
|
|
130
|
+
console.log(` 被签字节 : ${r.payloadBytes} sha256=${String(r.sha256).slice(0, 16)}…`);
|
|
131
|
+
}
|
|
132
|
+
if (r.services?.length) {
|
|
133
|
+
console.log(`\n 该节点声明的能力(${r.services.length} 个):`);
|
|
134
|
+
console.log(' ' + '-'.repeat(72));
|
|
135
|
+
for (const s of r.services) {
|
|
136
|
+
console.log(` [${s.index}] ${s.name} (${s.status})`);
|
|
137
|
+
console.log(` ${String(s.title || '').slice(0, 60)}`);
|
|
138
|
+
console.log(` ${s.entry || ''}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (r.ok) console.log('\n 提示: 验签通过只说明「是这把钥签的、内容没被改」,不代表服务质量。\n');
|
|
142
|
+
process.exit(r.ok ? 0 : 1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
146
|
+
main().catch((e) => {
|
|
147
|
+
console.error('出错:', e.message);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
});
|
|
150
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yihe-verify",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "验证 AI 能力节点(Agent 互联网)广播清单的 ed25519 签名,并列出其声明的能力、入口与定价。零依赖,Node >= 18。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"yihe-verify": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"ai-agent",
|
|
22
|
+
"agent",
|
|
23
|
+
"agents",
|
|
24
|
+
"capability-discovery",
|
|
25
|
+
"agent-internet",
|
|
26
|
+
"ed25519",
|
|
27
|
+
"signature",
|
|
28
|
+
"verify",
|
|
29
|
+
"llm-tools"
|
|
30
|
+
],
|
|
31
|
+
"author": "YiHe",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
}
|
|
36
|
+
}
|