@helm-protocol/ttt-mcp 0.3.4 → 0.4.1
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 +13 -5
- package/dist/index.js +73 -17
- package/dist/pot_record_v08.js +10 -1
- package/dist/pot_record_v2.js +174 -0
- package/dist/tls_exporter.js +43 -0
- package/dist/tools.js +235 -1
- package/dist/transport_context.js +37 -0
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -55,12 +55,12 @@ Claude workflow → [context compressed] → agents call pot_query(eventId)
|
|
|
55
55
|
### Claude Code
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
claude mcp add ttt -- npx -y @helm-protocol/ttt-mcp@0.
|
|
58
|
+
claude mcp add ttt -- npx -y @helm-protocol/ttt-mcp@0.4.0
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
With an API key (raises the free limit to your plan's monthly quota):
|
|
62
62
|
```bash
|
|
63
|
-
claude mcp add ttt -e TTT_API_KEY=your-key -- npx -y @helm-protocol/ttt-mcp@0.
|
|
63
|
+
claude mcp add ttt -e TTT_API_KEY=your-key -- npx -y @helm-protocol/ttt-mcp@0.4.0
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
### Claude Desktop
|
|
@@ -72,7 +72,7 @@ Add to `claude_desktop_config.json`:
|
|
|
72
72
|
"mcpServers": {
|
|
73
73
|
"ttt": {
|
|
74
74
|
"command": "npx",
|
|
75
|
-
"args": ["-y", "@helm-protocol/ttt-mcp@0.
|
|
75
|
+
"args": ["-y", "@helm-protocol/ttt-mcp@0.4.0"],
|
|
76
76
|
"env": { "TTT_API_KEY": "your-key" }
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -81,7 +81,7 @@ Add to `claude_desktop_config.json`:
|
|
|
81
81
|
|
|
82
82
|
### Cursor
|
|
83
83
|
|
|
84
|
-
[](https://cursor.com/install-mcp?name=ttt&config=
|
|
84
|
+
[](https://cursor.com/install-mcp?name=ttt&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBoZWxtLXByb3RvY29sL3R0dC1tY3BAMC40LjAiXX0=)
|
|
85
85
|
|
|
86
86
|
One-click install, or add the same `mcpServers` block above to `.cursor/mcp.json`.
|
|
87
87
|
|
|
@@ -127,12 +127,14 @@ pot_graph(eventId: "step-2", depth: 5)
|
|
|
127
127
|
|
|
128
128
|
---
|
|
129
129
|
|
|
130
|
-
##
|
|
130
|
+
## 10 Tools
|
|
131
131
|
|
|
132
132
|
| Tool | Purpose |
|
|
133
133
|
|------|---------|
|
|
134
134
|
| `pot_generate` | Stamp a workflow step with a cryptographic timestamp |
|
|
135
135
|
| `pot_verify` | Verify a PoT signature |
|
|
136
|
+
| `pot_generate_v2` | Generate the draft-11 180-octet PoT Record v2 core (canonical wire profile) |
|
|
137
|
+
| `pot_verify_v2` | Verify a v2 record; optional TLS 1.3 exporter binding proof (fails closed without TLS) |
|
|
136
138
|
| `pot_verify_v08` | Verify a draft-08 §3 Payload Digest record |
|
|
137
139
|
| `pot_query` | O(1) exact lookup by eventId — core amnesia recovery |
|
|
138
140
|
| `pot_graph` | Traverse causal DAG (backward + forward chain) |
|
|
@@ -168,6 +170,12 @@ Verify a draft-08 §3 record produced by `pot_generate`'s `potRecordV08` field:
|
|
|
168
170
|
| ctxId | string | No | Must match what `pot_generate` used, or verification fails |
|
|
169
171
|
| issuerPubKey | string | No | Hex-encoded 32-byte raw Ed25519 public key. Defaults to this server's own key. |
|
|
170
172
|
| content | string | No | Payload to check against the record's Payload Digest field |
|
|
173
|
+
| clientId | string | Required when `TTTPS_REQUIRE_REPLAY_LEDGER=1` | Stable caller identity for the replay key |
|
|
174
|
+
| sessionId | string | Required when `TTTPS_REQUIRE_REPLAY_LEDGER=1` | Active transport/session identifier for the replay key |
|
|
175
|
+
|
|
176
|
+
When `TTTPS_REQUIRE_V08_FRESHNESS=1`, `TTTPS_V08_MAX_SKEW_NS` MUST be set to a non-negative integer. The verifier rejects records outside `maxSkewNs + errorBoundUs`. When `TTTPS_REQUIRE_REPLAY_LEDGER=1`, a successful verification atomically claims `clientId/sessionId/nonce` in Redis with a 90-day TTL; Redis failure or a second claim is rejected. These flags are fail-closed controls.
|
|
177
|
+
|
|
178
|
+
The current MCP canonical wire profile is draft-11 PoT Record v2 (180 octets). `pot_generate_v2` and `pot_verify_v2` implement the fixed core record, including SHA-256 integrity over octets 0-79 and Ed25519 issuer authentication over octets 0-115. Draft-08 remains available through `pot_verify_v08` for legacy interoperability only. TLS binding proof is a separate 64/32-octet value; `TTTPS_V2_REQUIRE_BINDING=1` fails closed unless the server is running over direct TLS 1.3; stdio and plain HTTP have no TLS exporter. Configure `MCP_TLS_CERT_FILE` and `MCP_TLS_KEY_FILE` for the HTTPS mode. Replay claims use `(ctx_id, nonce)` with `TTTPS_REPLAY_TTL_SECONDS` (default 86400).
|
|
171
179
|
|
|
172
180
|
### pot_query
|
|
173
181
|
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,10 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
4
4
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
5
5
|
var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
6
6
|
var import_http = require("http");
|
|
7
|
+
var import_https = require("https");
|
|
8
|
+
var import_fs = require("fs");
|
|
9
|
+
var import_tls_exporter = require("./tls_exporter");
|
|
10
|
+
var import_transport_context = require("./transport_context");
|
|
7
11
|
var import_zod = require("zod");
|
|
8
12
|
var import_tools = require("./tools");
|
|
9
13
|
var import_auth = require("./auth");
|
|
@@ -101,8 +105,9 @@ function toolSuccess(result) {
|
|
|
101
105
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
102
106
|
}
|
|
103
107
|
function buildMcpServer() {
|
|
104
|
-
const s = new import_mcp.McpServer({ name: "ttt-mcp", version: "0.
|
|
105
|
-
s.tool(
|
|
108
|
+
const s = new import_mcp.McpServer({ name: "ttt-mcp", version: "0.4.1" });
|
|
109
|
+
const registerTool = s.tool.bind(s);
|
|
110
|
+
registerTool(
|
|
106
111
|
"pot_generate",
|
|
107
112
|
"Generate a cryptographic Proof of Time timestamp (draft-helmprotocol-tttps, https://datatracker.ietf.org/doc/draft-helmprotocol-tttps/). For Claude Code workflows: use eventId + prevEventId to build a causal chain. For DeFi: use txHash + chainId + poolAddress. For a spec-conformant draft-08 \xA73 record binding this attestation to a specific piece of content, also supply contentDigest. One of eventId, txHash, or contentDigest is required.",
|
|
108
113
|
{
|
|
@@ -126,7 +131,7 @@ function buildMcpServer() {
|
|
|
126
131
|
}
|
|
127
132
|
}
|
|
128
133
|
);
|
|
129
|
-
|
|
134
|
+
registerTool(
|
|
130
135
|
"pot_verify",
|
|
131
136
|
"Verify a Proof of Time using its hash and integrity shards. Returns validity, mode (turbo/full), and timestamp.",
|
|
132
137
|
{
|
|
@@ -145,16 +150,58 @@ function buildMcpServer() {
|
|
|
145
150
|
}
|
|
146
151
|
}
|
|
147
152
|
);
|
|
148
|
-
|
|
153
|
+
registerTool(
|
|
154
|
+
"pot_generate_v2",
|
|
155
|
+
"Generate the draft-11 180-octet Proof-of-Time Record v2 core. TLS binding proof is computed only after a live TLS session exists.",
|
|
156
|
+
{
|
|
157
|
+
tsTaiUs: import_zod.z.string().describe("TAI timestamp in microseconds as a decimal string"),
|
|
158
|
+
dispersionUs: import_zod.z.number().int().nonnegative().describe("Uncertainty bound in microseconds"),
|
|
159
|
+
ctxId: import_zod.z.string().length(32).describe("16-octet context identifier encoded as hex"),
|
|
160
|
+
holderAuthData: import_zod.z.string().length(64).describe("32-octet holder public key or PSK digest encoded as hex"),
|
|
161
|
+
holderAuthType: import_zod.z.number().int().optional().describe("0x01 Ed25519 holder key (default) or 0x02 shared secret")
|
|
162
|
+
},
|
|
163
|
+
{ title: "Generate draft-11 180-octet PoT v2", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
164
|
+
async (args) => {
|
|
165
|
+
try {
|
|
166
|
+
return toolSuccess(await (0, import_tools.potGenerateV2)(args));
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return toolError(err);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
registerTool(
|
|
173
|
+
"pot_verify_v2",
|
|
174
|
+
"Verify the draft-11 180-octet PoT Record v2 core. Binding-required mode fails closed in stdio because no TLS exporter session exists.",
|
|
175
|
+
{
|
|
176
|
+
potRecordV2: import_zod.z.string().length(360).describe("Hex-encoded 180-octet draft-11 v2 record"),
|
|
177
|
+
issuerPubKey: import_zod.z.string().length(64).optional().describe("Raw 32-octet issuer Ed25519 public key in hex"),
|
|
178
|
+
nowTaiUs: import_zod.z.string().optional().describe("Current TAI timestamp in microseconds"),
|
|
179
|
+
maxSkewUs: import_zod.z.string().optional().describe("Configured freshness allowance in microseconds"),
|
|
180
|
+
clientId: import_zod.z.string().optional(),
|
|
181
|
+
sessionId: import_zod.z.string().optional(),
|
|
182
|
+
bindingProof: import_zod.z.string().regex(/^(?:[0-9a-fA-F]{128}|[0-9a-fA-F]{64})$/).optional().describe("64-octet Ed25519 or 32-octet HMAC TLS binding proof")
|
|
183
|
+
},
|
|
184
|
+
{ title: "Verify draft-11 180-octet PoT v2", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
185
|
+
async (args) => {
|
|
186
|
+
try {
|
|
187
|
+
return toolSuccess(await (0, import_tools.potVerifyV2)(args));
|
|
188
|
+
} catch (err) {
|
|
189
|
+
return toolError(err);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
registerTool(
|
|
149
194
|
"pot_verify_v08",
|
|
150
|
-
"Verify a draft-helmprotocol-tttps-08 \xA73 Proof-of-Time record
|
|
195
|
+
"Verify and, when configured, admit a draft-helmprotocol-tttps-08 \xA73 Proof-of-Time record: recomputes the Commitment and Ed25519 signature, applies configured freshness, and atomically claims client/session/nonce in Redis before admission.",
|
|
151
196
|
{
|
|
152
197
|
potRecordV08: import_zod.z.string().describe("Hex-encoded 184 or 216-octet record from pot_generate's potRecordV08 field"),
|
|
153
198
|
ctxId: import_zod.z.string().max(255).optional().describe("Context identifier the record was generated under. Must match what pot_generate used, or verification fails."),
|
|
154
199
|
issuerPubKey: import_zod.z.string().optional().describe("Hex-encoded 32-byte raw Ed25519 issuer public key. Defaults to this server's own key."),
|
|
155
|
-
content: import_zod.z.string().optional().describe("The payload (utf8) to check against the record's Payload Digest field, if available")
|
|
200
|
+
content: import_zod.z.string().optional().describe("The payload (utf8) to check against the record's Payload Digest field, if available"),
|
|
201
|
+
clientId: import_zod.z.string().optional().describe("Stable caller identity for the durable replay ledger when TTTPS_REQUIRE_REPLAY_LEDGER=1"),
|
|
202
|
+
sessionId: import_zod.z.string().optional().describe("Active transport/session identifier for the durable replay ledger when TTTPS_REQUIRE_REPLAY_LEDGER=1")
|
|
156
203
|
},
|
|
157
|
-
{ title: "Verify draft-08 Proof-of-Time Record", readOnlyHint:
|
|
204
|
+
{ title: "Verify draft-08 Proof-of-Time Record", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
158
205
|
async (args) => {
|
|
159
206
|
try {
|
|
160
207
|
const result = await (0, import_tools.potVerifyV08)(args);
|
|
@@ -164,9 +211,9 @@ function buildMcpServer() {
|
|
|
164
211
|
}
|
|
165
212
|
}
|
|
166
213
|
);
|
|
167
|
-
|
|
214
|
+
registerTool(
|
|
168
215
|
"pot_query",
|
|
169
|
-
"Query Proof of Time records. Use eventId for exact
|
|
216
|
+
"Query Proof of Time records. Use eventId for exact lookup of a specific workflow step. Use startTime/endTime for time-range queries.",
|
|
170
217
|
{
|
|
171
218
|
eventId: import_zod.z.string().optional().describe("Exact eventId lookup \u2014 call this at workflow start to restore action history after context compression"),
|
|
172
219
|
startTime: import_zod.z.number().optional().describe("Start time (unix ms). Default: 24h ago"),
|
|
@@ -183,7 +230,7 @@ function buildMcpServer() {
|
|
|
183
230
|
}
|
|
184
231
|
}
|
|
185
232
|
);
|
|
186
|
-
|
|
233
|
+
registerTool(
|
|
187
234
|
"pot_graph",
|
|
188
235
|
"Traverse the causal chain of workflow steps. Given an eventId, returns the full backward chain (ancestors via prevEventId) and forward chain (steps that follow). Use after context compression to reconstruct the complete workflow timeline.",
|
|
189
236
|
{
|
|
@@ -200,7 +247,7 @@ function buildMcpServer() {
|
|
|
200
247
|
}
|
|
201
248
|
}
|
|
202
249
|
);
|
|
203
|
-
|
|
250
|
+
registerTool(
|
|
204
251
|
"pot_stats",
|
|
205
252
|
"Get PoT statistics: total swaps, turbo/full counts, and turbo ratio for a given period.",
|
|
206
253
|
{ period: import_zod.z.enum(["day", "week", "month"]).describe("Time period for statistics") },
|
|
@@ -214,7 +261,7 @@ function buildMcpServer() {
|
|
|
214
261
|
}
|
|
215
262
|
}
|
|
216
263
|
);
|
|
217
|
-
|
|
264
|
+
registerTool(
|
|
218
265
|
"pot_health",
|
|
219
266
|
"Check PoT system health: time source status, subgraph sync, server uptime, and current mode.",
|
|
220
267
|
{},
|
|
@@ -228,7 +275,7 @@ function buildMcpServer() {
|
|
|
228
275
|
}
|
|
229
276
|
}
|
|
230
277
|
);
|
|
231
|
-
|
|
278
|
+
registerTool(
|
|
232
279
|
"pot_checkpoint",
|
|
233
280
|
"Create a compressed rollup checkpoint of workflow history. Call this periodically to prevent token explosion when recovering from context compression. Returns checkpointId, compressed event history, chainIntact status, and nextCheckpointHint (recommended events before next checkpoint).",
|
|
234
281
|
{
|
|
@@ -254,10 +301,10 @@ async function main() {
|
|
|
254
301
|
await restoreDAGFromRedis();
|
|
255
302
|
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : null;
|
|
256
303
|
if (port) {
|
|
257
|
-
const
|
|
304
|
+
const requestHandler = async (req, res) => {
|
|
258
305
|
if (req.method === "GET" && (req.url === "/health" || req.url === "/ping")) {
|
|
259
306
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
260
|
-
res.end(JSON.stringify({ status: "ok", server: "ttt-mcp", version: "0.
|
|
307
|
+
res.end(JSON.stringify({ status: "ok", server: "ttt-mcp", version: "0.4.1" }));
|
|
261
308
|
return;
|
|
262
309
|
}
|
|
263
310
|
if (req.method === "POST") {
|
|
@@ -309,9 +356,18 @@ async function main() {
|
|
|
309
356
|
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
310
357
|
}
|
|
311
358
|
}
|
|
312
|
-
}
|
|
359
|
+
};
|
|
360
|
+
const certFile = process.env.MCP_TLS_CERT_FILE?.trim();
|
|
361
|
+
const keyFile = process.env.MCP_TLS_KEY_FILE?.trim();
|
|
362
|
+
if (certFile && !keyFile || !certFile && keyFile) {
|
|
363
|
+
throw new Error("MCP_TLS_CERT_FILE and MCP_TLS_KEY_FILE must be configured together");
|
|
364
|
+
}
|
|
365
|
+
const httpServer = certFile && keyFile ? (0, import_https.createServer)({ minVersion: "TLSv1.3", cert: (0, import_fs.readFileSync)(certFile), key: (0, import_fs.readFileSync)(keyFile) }, (req, res) => {
|
|
366
|
+
const exporter = (0, import_tls_exporter.extractTls13Exporter)(req);
|
|
367
|
+
return (0, import_transport_context.withTransportBinding)({ exporter, remoteAddress: req.socket.remoteAddress ?? "", clientId: String(req.headers["x-ttt-client-id"] ?? ""), sessionId: String(req.headers["x-ttt-session-id"] ?? "") }, () => requestHandler(req, res));
|
|
368
|
+
}) : (0, import_http.createServer)((req, res) => (0, import_transport_context.withTransportBinding)({ remoteAddress: req.socket.remoteAddress ?? "", clientId: String(req.headers["x-ttt-client-id"] ?? ""), sessionId: String(req.headers["x-ttt-session-id"] ?? "") }, () => requestHandler(req, res)));
|
|
313
369
|
httpServer.listen(port, () => {
|
|
314
|
-
console.error(`[ttt-mcp] OpenTTT MCP Server (HTTP) on port ${port}`);
|
|
370
|
+
console.error(`[ttt-mcp] OpenTTT MCP Server (${certFile ? "HTTPS/TLS1.3" : "HTTP"}) on port ${port}`);
|
|
315
371
|
});
|
|
316
372
|
} else {
|
|
317
373
|
const stdioServer = buildMcpServer();
|
package/dist/pot_record_v08.js
CHANGED
|
@@ -161,7 +161,7 @@ function decodePotRecordV08(record) {
|
|
|
161
161
|
p
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
|
-
function verifyPotRecordV08(record, ctxId, issuerPublicKeyRaw, content) {
|
|
164
|
+
function verifyPotRecordV08(record, ctxId, issuerPublicKeyRaw, content, freshness) {
|
|
165
165
|
let decoded;
|
|
166
166
|
try {
|
|
167
167
|
decoded = decodePotRecordV08(record);
|
|
@@ -177,6 +177,15 @@ function verifyPotRecordV08(record, ctxId, issuerPublicKeyRaw, content) {
|
|
|
177
177
|
if (decoded.errorBoundUs === RESERVED_ERROR_BOUND) {
|
|
178
178
|
return { verdict: "rejected", reason: "Error Bound carries the reserved value 0xFFFFFF" };
|
|
179
179
|
}
|
|
180
|
+
if (freshness !== void 0) {
|
|
181
|
+
if (freshness.maxSkewNs < 0n) {
|
|
182
|
+
return { verdict: "rejected", reason: "invalid freshness policy" };
|
|
183
|
+
}
|
|
184
|
+
const delta = decoded.timestampNs >= freshness.nowNs ? decoded.timestampNs - freshness.nowNs : freshness.nowNs - decoded.timestampNs;
|
|
185
|
+
if (delta > freshness.maxSkewNs + BigInt(decoded.errorBoundUs) * 1000n) {
|
|
186
|
+
return { verdict: "rejected", reason: "freshness window exceeded" };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
180
189
|
const expectedCommitment = computeCommitmentSha256(decoded.fieldsPre, ctxId);
|
|
181
190
|
if (!expectedCommitment.equals(decoded.commitment)) {
|
|
182
191
|
return { verdict: "rejected", reason: "commitment mismatch" };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var pot_record_v2_exports = {};
|
|
20
|
+
__export(pot_record_v2_exports, {
|
|
21
|
+
HOLDER_AUTH_ED25519: () => HOLDER_AUTH_ED25519,
|
|
22
|
+
HOLDER_AUTH_SHARED_SECRET: () => HOLDER_AUTH_SHARED_SECRET,
|
|
23
|
+
POT_V2_INTEGRITY_SHA256: () => POT_V2_INTEGRITY_SHA256,
|
|
24
|
+
POT_V2_SIZE: () => POT_V2_SIZE,
|
|
25
|
+
POT_V2_VERSION: () => POT_V2_VERSION,
|
|
26
|
+
computeV2BindingInput: () => computeV2BindingInput,
|
|
27
|
+
computeV2ExporterContext: () => computeV2ExporterContext,
|
|
28
|
+
decodePotRecordV2: () => decodePotRecordV2,
|
|
29
|
+
encodePotRecordV2: () => encodePotRecordV2,
|
|
30
|
+
verifyPotRecordV2: () => verifyPotRecordV2,
|
|
31
|
+
verifyV2BindingProof: () => verifyV2BindingProof
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(pot_record_v2_exports);
|
|
34
|
+
var import_crypto = require("crypto");
|
|
35
|
+
const POT_V2_SIZE = 180;
|
|
36
|
+
const POT_V2_VERSION = 2;
|
|
37
|
+
const POT_V2_INTEGRITY_SHA256 = 1;
|
|
38
|
+
const HOLDER_AUTH_ED25519 = 1;
|
|
39
|
+
const HOLDER_AUTH_SHARED_SECRET = 2;
|
|
40
|
+
const OFF = {
|
|
41
|
+
VERSION: 0,
|
|
42
|
+
HOLDER_AUTH_TYPE: 1,
|
|
43
|
+
ALG_ID: 2,
|
|
44
|
+
TS: 4,
|
|
45
|
+
DISPERSION: 12,
|
|
46
|
+
CTX_ID: 16,
|
|
47
|
+
NONCE: 32,
|
|
48
|
+
HOLDER_AUTH_DATA: 48,
|
|
49
|
+
INTEGRITY_TAG: 80,
|
|
50
|
+
ISSUER_KEY_ID: 112,
|
|
51
|
+
ISSUER_SIG: 116
|
|
52
|
+
};
|
|
53
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
54
|
+
function requireLength(name, value, length) {
|
|
55
|
+
if (value.length !== length) throw new Error(`${name} must be ${length} bytes`);
|
|
56
|
+
}
|
|
57
|
+
function issuerPublicKeyFromRaw(raw) {
|
|
58
|
+
requireLength("issuer public key", raw, 32);
|
|
59
|
+
return (0, import_crypto.createPublicKey)({ key: Buffer.concat([ED25519_SPKI_PREFIX, raw]), format: "der", type: "spki" });
|
|
60
|
+
}
|
|
61
|
+
function integrityTag(signedPrefix) {
|
|
62
|
+
return (0, import_crypto.createHash)("sha256").update(signedPrefix).digest();
|
|
63
|
+
}
|
|
64
|
+
function encodePotRecordV2(fields, issuerPrivateKey) {
|
|
65
|
+
if (fields.holderAuthType !== HOLDER_AUTH_ED25519 && fields.holderAuthType !== HOLDER_AUTH_SHARED_SECRET) {
|
|
66
|
+
throw new Error("unsupported holder_auth_type");
|
|
67
|
+
}
|
|
68
|
+
if (fields.algId !== POT_V2_INTEGRITY_SHA256) throw new Error("unsupported alg_id");
|
|
69
|
+
if (fields.tsTaiUs < 0n || fields.tsTaiUs > 0xffffffffffffffffn) throw new Error("ts out of range");
|
|
70
|
+
if (fields.dispersionUs < 0 || fields.dispersionUs > 4294967295) throw new Error("dispersion out of range");
|
|
71
|
+
if (fields.issuerKeyId < 0 || fields.issuerKeyId > 4294967295) throw new Error("issuer_key_id out of range");
|
|
72
|
+
requireLength("ctx_id", fields.ctxId, 16);
|
|
73
|
+
requireLength("nonce", fields.nonce, 16);
|
|
74
|
+
requireLength("holder_auth_data", fields.holderAuthData, 32);
|
|
75
|
+
const prefix = Buffer.alloc(116);
|
|
76
|
+
prefix.writeUInt8(POT_V2_VERSION, OFF.VERSION);
|
|
77
|
+
prefix.writeUInt8(fields.holderAuthType, OFF.HOLDER_AUTH_TYPE);
|
|
78
|
+
prefix.writeUInt16BE(fields.algId, OFF.ALG_ID);
|
|
79
|
+
prefix.writeBigUInt64BE(fields.tsTaiUs, OFF.TS);
|
|
80
|
+
prefix.writeUInt32BE(fields.dispersionUs, OFF.DISPERSION);
|
|
81
|
+
fields.ctxId.copy(prefix, OFF.CTX_ID);
|
|
82
|
+
fields.nonce.copy(prefix, OFF.NONCE);
|
|
83
|
+
fields.holderAuthData.copy(prefix, OFF.HOLDER_AUTH_DATA);
|
|
84
|
+
integrityTag(prefix.subarray(0, 80)).copy(prefix, OFF.INTEGRITY_TAG);
|
|
85
|
+
prefix.writeUInt32BE(fields.issuerKeyId, OFF.ISSUER_KEY_ID);
|
|
86
|
+
const signature = (0, import_crypto.sign)(null, prefix, issuerPrivateKey);
|
|
87
|
+
return Buffer.concat([prefix, signature]);
|
|
88
|
+
}
|
|
89
|
+
function decodePotRecordV2(record) {
|
|
90
|
+
if (record.length !== POT_V2_SIZE) throw new Error(`expected 180 octets, got ${record.length}`);
|
|
91
|
+
const signedPrefix = record.subarray(0, 116);
|
|
92
|
+
return {
|
|
93
|
+
holderAuthType: record.readUInt8(OFF.HOLDER_AUTH_TYPE),
|
|
94
|
+
algId: record.readUInt16BE(OFF.ALG_ID),
|
|
95
|
+
tsTaiUs: record.readBigUInt64BE(OFF.TS),
|
|
96
|
+
dispersionUs: record.readUInt32BE(OFF.DISPERSION),
|
|
97
|
+
ctxId: Buffer.from(record.subarray(OFF.CTX_ID, OFF.CTX_ID + 16)),
|
|
98
|
+
nonce: Buffer.from(record.subarray(OFF.NONCE, OFF.NONCE + 16)),
|
|
99
|
+
holderAuthData: Buffer.from(record.subarray(OFF.HOLDER_AUTH_DATA, OFF.HOLDER_AUTH_DATA + 32)),
|
|
100
|
+
issuerKeyId: record.readUInt32BE(OFF.ISSUER_KEY_ID),
|
|
101
|
+
integrityTag: Buffer.from(record.subarray(OFF.INTEGRITY_TAG, OFF.INTEGRITY_TAG + 32)),
|
|
102
|
+
issuerSig: Buffer.from(record.subarray(OFF.ISSUER_SIG, POT_V2_SIZE)),
|
|
103
|
+
signedPrefix: Buffer.from(signedPrefix)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function computeV2ExporterContext(record) {
|
|
107
|
+
const decoded = decodePotRecordV2(record);
|
|
108
|
+
const bindingPrefix = Buffer.from("tttps-binding-v2\0", "ascii");
|
|
109
|
+
const recordHash = (0, import_crypto.createHash)("sha256").update(record).digest();
|
|
110
|
+
return (0, import_crypto.createHash)("sha256").update(Buffer.concat([bindingPrefix, recordHash, decoded.ctxId, decoded.nonce])).digest();
|
|
111
|
+
}
|
|
112
|
+
function computeV2BindingInput(record, exporterOutput) {
|
|
113
|
+
decodePotRecordV2(record);
|
|
114
|
+
if (exporterOutput.length !== 32) throw new Error("TLS exporter output must be 32 octets");
|
|
115
|
+
return Buffer.concat([Buffer.from("tttps-binding-v2\0", "ascii"), exporterOutput, record.subarray(16, 32), record.subarray(32, 48)]);
|
|
116
|
+
}
|
|
117
|
+
function verifyV2BindingProof(record, bindingProof, exporterOutput, sharedSecret) {
|
|
118
|
+
const decoded = decodePotRecordV2(record);
|
|
119
|
+
const bindingInput = computeV2BindingInput(record, exporterOutput);
|
|
120
|
+
if (decoded.holderAuthType === HOLDER_AUTH_ED25519) {
|
|
121
|
+
if (bindingProof.length !== 64) return { verdict: "rejected", reason: "Ed25519 binding proof must be 64 octets" };
|
|
122
|
+
const holderPublicKey = issuerPublicKeyFromRaw(decoded.holderAuthData);
|
|
123
|
+
return (0, import_crypto.verify)(null, bindingInput, holderPublicKey, bindingProof) ? { verdict: "intact" } : { verdict: "rejected", reason: "TLS exporter holder proof mismatch" };
|
|
124
|
+
}
|
|
125
|
+
if (decoded.holderAuthType === HOLDER_AUTH_SHARED_SECRET) {
|
|
126
|
+
if (!sharedSecret) return { verdict: "rejected", reason: "shared-secret holder binding is not configured" };
|
|
127
|
+
if (bindingProof.length !== 32) return { verdict: "rejected", reason: "HMAC binding proof must be 32 octets" };
|
|
128
|
+
const expectedDigest = (0, import_crypto.createHash)("sha256").update(sharedSecret).digest();
|
|
129
|
+
if (!(0, import_crypto.timingSafeEqual)(expectedDigest, decoded.holderAuthData)) return { verdict: "rejected", reason: "shared-secret digest mismatch" };
|
|
130
|
+
const expected = (0, import_crypto.createHmac)("sha256", sharedSecret).update(bindingInput).digest();
|
|
131
|
+
return (0, import_crypto.timingSafeEqual)(expected, bindingProof) ? { verdict: "intact" } : { verdict: "rejected", reason: "TLS exporter HMAC proof mismatch" };
|
|
132
|
+
}
|
|
133
|
+
return { verdict: "rejected", reason: "unsupported holder authentication type" };
|
|
134
|
+
}
|
|
135
|
+
function verifyPotRecordV2(record, issuerPublicKeyRaw, freshness) {
|
|
136
|
+
let decoded;
|
|
137
|
+
try {
|
|
138
|
+
decoded = decodePotRecordV2(record);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return { verdict: "rejected", reason: e instanceof Error ? e.message : String(e) };
|
|
141
|
+
}
|
|
142
|
+
if (record[OFF.VERSION] !== POT_V2_VERSION) return { verdict: "rejected", reason: "unknown version" };
|
|
143
|
+
if (decoded.holderAuthType !== HOLDER_AUTH_ED25519 && decoded.holderAuthType !== HOLDER_AUTH_SHARED_SECRET) {
|
|
144
|
+
return { verdict: "rejected", reason: "unsupported holder_auth_type" };
|
|
145
|
+
}
|
|
146
|
+
if (decoded.algId !== POT_V2_INTEGRITY_SHA256) return { verdict: "rejected", reason: "unsupported alg_id" };
|
|
147
|
+
if (!integrityTag(decoded.signedPrefix.subarray(0, 80)).equals(decoded.integrityTag)) return { verdict: "rejected", reason: "integrity mismatch" };
|
|
148
|
+
if (freshness) {
|
|
149
|
+
const delta = decoded.tsTaiUs >= freshness.nowTaiUs ? decoded.tsTaiUs - freshness.nowTaiUs : freshness.nowTaiUs - decoded.tsTaiUs;
|
|
150
|
+
if (delta > freshness.maxSkewUs + BigInt(decoded.dispersionUs)) return { verdict: "rejected", reason: "freshness window exceeded" };
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
if (!(0, import_crypto.verify)(null, decoded.signedPrefix, issuerPublicKeyFromRaw(issuerPublicKeyRaw), decoded.issuerSig)) {
|
|
154
|
+
return { verdict: "rejected", reason: "issuer signature invalid" };
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {
|
|
157
|
+
return { verdict: "rejected", reason: e instanceof Error ? e.message : String(e) };
|
|
158
|
+
}
|
|
159
|
+
return { verdict: "intact", nonce: decoded.nonce };
|
|
160
|
+
}
|
|
161
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
162
|
+
0 && (module.exports = {
|
|
163
|
+
HOLDER_AUTH_ED25519,
|
|
164
|
+
HOLDER_AUTH_SHARED_SECRET,
|
|
165
|
+
POT_V2_INTEGRITY_SHA256,
|
|
166
|
+
POT_V2_SIZE,
|
|
167
|
+
POT_V2_VERSION,
|
|
168
|
+
computeV2BindingInput,
|
|
169
|
+
computeV2ExporterContext,
|
|
170
|
+
decodePotRecordV2,
|
|
171
|
+
encodePotRecordV2,
|
|
172
|
+
verifyPotRecordV2,
|
|
173
|
+
verifyV2BindingProof
|
|
174
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var tls_exporter_exports = {};
|
|
20
|
+
__export(tls_exporter_exports, {
|
|
21
|
+
TTTPS_V2_EXPORTER_LABEL: () => TTTPS_V2_EXPORTER_LABEL,
|
|
22
|
+
TTTPS_V2_EXPORTER_LENGTH: () => TTTPS_V2_EXPORTER_LENGTH,
|
|
23
|
+
exporterForTlsSocket: () => exporterForTlsSocket,
|
|
24
|
+
extractTls13Exporter: () => extractTls13Exporter
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(tls_exporter_exports);
|
|
27
|
+
const TTTPS_V2_EXPORTER_LABEL = "EXPORTER-TTTPS-v2-Binding";
|
|
28
|
+
const TTTPS_V2_EXPORTER_LENGTH = 32;
|
|
29
|
+
function exporterForTlsSocket(socket) {
|
|
30
|
+
if (!socket.encrypted || typeof socket.exportKeyingMaterial !== "function") return void 0;
|
|
31
|
+
if (typeof socket.getProtocol === "function" && socket.getProtocol() !== "TLSv1.3") return void 0;
|
|
32
|
+
return (contextValue) => socket.exportKeyingMaterial(TTTPS_V2_EXPORTER_LENGTH, TTTPS_V2_EXPORTER_LABEL, contextValue);
|
|
33
|
+
}
|
|
34
|
+
function extractTls13Exporter(req) {
|
|
35
|
+
return exporterForTlsSocket(req.socket);
|
|
36
|
+
}
|
|
37
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
38
|
+
0 && (module.exports = {
|
|
39
|
+
TTTPS_V2_EXPORTER_LABEL,
|
|
40
|
+
TTTPS_V2_EXPORTER_LENGTH,
|
|
41
|
+
exporterForTlsSocket,
|
|
42
|
+
extractTls13Exporter
|
|
43
|
+
});
|
package/dist/tools.js
CHANGED
|
@@ -30,12 +30,14 @@ var tools_exports = {};
|
|
|
30
30
|
__export(tools_exports, {
|
|
31
31
|
potCheckpoint: () => potCheckpoint,
|
|
32
32
|
potGenerate: () => potGenerate,
|
|
33
|
+
potGenerateV2: () => potGenerateV2,
|
|
33
34
|
potGraph: () => potGraph,
|
|
34
35
|
potHealth: () => potHealth,
|
|
35
36
|
potQuery: () => potQuery,
|
|
36
37
|
potStats: () => potStats,
|
|
37
38
|
potVerify: () => potVerify,
|
|
38
39
|
potVerifyV08: () => potVerifyV08,
|
|
40
|
+
potVerifyV2: () => potVerifyV2,
|
|
39
41
|
redis: () => redis,
|
|
40
42
|
restoreDagEntry: () => restoreDagEntry,
|
|
41
43
|
tttsFreshnessSeal: () => tttsFreshnessSeal,
|
|
@@ -46,6 +48,8 @@ var import_openttt = require("openttt");
|
|
|
46
48
|
var import_telemetry = require("./telemetry");
|
|
47
49
|
var import_server = require("./server");
|
|
48
50
|
var import_crypto = require("crypto");
|
|
51
|
+
var import_pot_record_v2 = require("./pot_record_v2");
|
|
52
|
+
var import_transport_context = require("./transport_context");
|
|
49
53
|
var import_pot_record_v08 = require("./pot_record_v08");
|
|
50
54
|
var import_ioredis = __toESM(require("ioredis"));
|
|
51
55
|
const GrgPipeline = (
|
|
@@ -71,6 +75,41 @@ const redis = new import_ioredis.default(process.env.REDIS_URL ?? "redis://127.0
|
|
|
71
75
|
});
|
|
72
76
|
redis.on("error", () => {
|
|
73
77
|
});
|
|
78
|
+
const REPLAY_TTL_SECONDS = Number.parseInt(process.env.TTTPS_REPLAY_TTL_SECONDS ?? "86400", 10);
|
|
79
|
+
function requiredFlag(name) {
|
|
80
|
+
return process.env[name]?.trim() === "1";
|
|
81
|
+
}
|
|
82
|
+
function configuredFreshnessPolicy() {
|
|
83
|
+
const raw = process.env.TTTPS_V08_MAX_SKEW_NS?.trim();
|
|
84
|
+
if (!raw) return void 0;
|
|
85
|
+
try {
|
|
86
|
+
const maxSkewNs = BigInt(raw);
|
|
87
|
+
if (maxSkewNs < 0n) return void 0;
|
|
88
|
+
return { nowNs: BigInt(Date.now()) * 1000000n, maxSkewNs };
|
|
89
|
+
} catch {
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function claimV2Replay(ctxIdHex, nonceHex) {
|
|
94
|
+
if (!ctxIdHex || !nonceHex) return "unavailable";
|
|
95
|
+
const key = `tttps:v2:replay:${ctxIdHex}:${nonceHex}`;
|
|
96
|
+
try {
|
|
97
|
+
const result = await redis.set(key, "1", "EX", REPLAY_TTL_SECONDS, "NX");
|
|
98
|
+
return result === "OK" ? "claimed" : "replay";
|
|
99
|
+
} catch {
|
|
100
|
+
return "unavailable";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function claimV08Replay(clientId, sessionId, nonceHex) {
|
|
104
|
+
if (!clientId || !sessionId || !nonceHex) return "unavailable";
|
|
105
|
+
const key = `tttps:v08:replay:${clientId}:${sessionId}:${nonceHex}`;
|
|
106
|
+
try {
|
|
107
|
+
const result = await redis.set(key, "1", "EX", REPLAY_TTL_SECONDS, "NX");
|
|
108
|
+
return result === "OK" ? "claimed" : "replay";
|
|
109
|
+
} catch {
|
|
110
|
+
return "unavailable";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
74
113
|
const timeSynth = new import_openttt.TimeSynthesis();
|
|
75
114
|
const adaptiveSwitch = new import_openttt.AdaptiveSwitch();
|
|
76
115
|
const potSigner = new import_openttt.PotSigner();
|
|
@@ -374,17 +413,210 @@ async function potVerify(args) {
|
|
|
374
413
|
verifiedAt: Date.now()
|
|
375
414
|
});
|
|
376
415
|
}
|
|
416
|
+
async function potGenerateV2(args) {
|
|
417
|
+
(0, import_telemetry.telemetryIncrement)("pot_generate_v2");
|
|
418
|
+
const ctxId = Buffer.from(args.ctxId, "hex");
|
|
419
|
+
const holderAuthData = Buffer.from(args.holderAuthData, "hex");
|
|
420
|
+
if (ctxId.length !== 16) throw new Error("ctxId must be exactly 16 bytes of hex");
|
|
421
|
+
if (holderAuthData.length !== 32) throw new Error("holderAuthData must be exactly 32 bytes of hex");
|
|
422
|
+
const tsTaiUs = BigInt(args.tsTaiUs);
|
|
423
|
+
const nonce = (0, import_crypto.randomBytes)(16);
|
|
424
|
+
const issuerKeyId = (0, import_crypto.createHash)("sha256").update(potSignerPublicKeyRawV08).digest().readUInt32BE(0);
|
|
425
|
+
const record = (0, import_pot_record_v2.encodePotRecordV2)({
|
|
426
|
+
holderAuthType: args.holderAuthType ?? 1,
|
|
427
|
+
algId: 1,
|
|
428
|
+
tsTaiUs,
|
|
429
|
+
dispersionUs: args.dispersionUs,
|
|
430
|
+
ctxId,
|
|
431
|
+
nonce,
|
|
432
|
+
holderAuthData,
|
|
433
|
+
issuerKeyId
|
|
434
|
+
}, potSignerPrivateKeyV08);
|
|
435
|
+
return serialize({
|
|
436
|
+
wireProfile: "draft-helmprotocol-tttps-11-v2",
|
|
437
|
+
potRecordV2: record.toString("hex"),
|
|
438
|
+
issuerPubKey: potSignerPublicKeyRawV08.toString("hex"),
|
|
439
|
+
issuerKeyId,
|
|
440
|
+
nonce: nonce.toString("hex"),
|
|
441
|
+
bindingProofRequired: process.env.TTTPS_V2_REQUIRE_BINDING === "1"
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
function trustedKeyList(envName) {
|
|
445
|
+
return (process.env[envName] ?? "").split(",").map((s) => s.trim()).filter((s) => /^[0-9a-fA-F]{64}$/.test(s)).map((h) => Buffer.from(h, "hex"));
|
|
446
|
+
}
|
|
447
|
+
let roughtimeAdmitCache = null;
|
|
448
|
+
async function roughtimeQuorumOk() {
|
|
449
|
+
const url = process.env.TTTPS_ADMISSION_STATUS_URL ?? "https://api.kenosian.com/pot/status";
|
|
450
|
+
const ttlMs = Number.parseInt(process.env.TTTPS_ADMISSION_TTL_MS ?? "5000", 10);
|
|
451
|
+
if (roughtimeAdmitCache && Date.now() - roughtimeAdmitCache.at < ttlMs) return roughtimeAdmitCache.ok;
|
|
452
|
+
try {
|
|
453
|
+
const timeoutMs = Number.parseInt(process.env.TTTPS_ADMISSION_TIMEOUT_MS ?? "1500", 10);
|
|
454
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
455
|
+
const j = await res.json();
|
|
456
|
+
const ok = j?.roughtime_ok === true;
|
|
457
|
+
roughtimeAdmitCache = { ok, at: Date.now() };
|
|
458
|
+
return ok;
|
|
459
|
+
} catch {
|
|
460
|
+
roughtimeAdmitCache = { ok: false, at: Date.now() };
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async function potVerifyV2Core(args) {
|
|
465
|
+
(0, import_telemetry.telemetryIncrement)("pot_verify_v2");
|
|
466
|
+
const bindingRequired = process.env.TTTPS_V2_REQUIRE_BINDING === "1";
|
|
467
|
+
if (bindingRequired && !args.bindingProof) {
|
|
468
|
+
return serialize({ verdict: "rejected", reason: "TLS exporter binding proof is required" });
|
|
469
|
+
}
|
|
470
|
+
const record = Buffer.from(args.potRecordV2, "hex");
|
|
471
|
+
if (process.env.TTTPS_REQUIRE_ROUGHTIME_QUORUM === "1") {
|
|
472
|
+
if (!await roughtimeQuorumOk()) {
|
|
473
|
+
return serialize({ verdict: "rejected", reason: "roughtime quorum unavailable" });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const freshness = process.env.TTTPS_ENFORCE_FRESHNESS === "1" ? { nowTaiUs: BigInt(Date.now()) * 1000n, maxSkewUs: BigInt(process.env.TTTPS_MAX_SKEW_US ?? "60000000") } : args.nowTaiUs !== void 0 && args.maxSkewUs !== void 0 ? { nowTaiUs: BigInt(args.nowTaiUs), maxSkewUs: BigInt(args.maxSkewUs) } : void 0;
|
|
477
|
+
const trustedIssuers = trustedKeyList("TTTPS_TRUSTED_ISSUERS");
|
|
478
|
+
const pinSelfIssuer = process.env.TTTPS_PIN_SELF_ISSUER === "1";
|
|
479
|
+
const candidates = trustedIssuers.length > 0 ? trustedIssuers : pinSelfIssuer ? [potSignerPublicKeyRawV08] : [args.issuerPubKey ? Buffer.from(args.issuerPubKey, "hex") : potSignerPublicKeyRawV08];
|
|
480
|
+
let result = (0, import_pot_record_v2.verifyPotRecordV2)(record, candidates[0], freshness);
|
|
481
|
+
for (let i = 1; i < candidates.length && result.verdict === "rejected" && result.reason === "issuer signature invalid"; i++) {
|
|
482
|
+
result = (0, import_pot_record_v2.verifyPotRecordV2)(record, candidates[i], freshness);
|
|
483
|
+
}
|
|
484
|
+
if (result.verdict === "rejected") {
|
|
485
|
+
if (trustedIssuers.length > 0 && result.reason === "issuer signature invalid") {
|
|
486
|
+
return serialize({ verdict: "rejected", reason: "issuer not trusted" });
|
|
487
|
+
}
|
|
488
|
+
return serialize(result);
|
|
489
|
+
}
|
|
490
|
+
const trustedHolders = trustedKeyList("TTTPS_TRUSTED_HOLDERS");
|
|
491
|
+
if (trustedHolders.length > 0) {
|
|
492
|
+
const holder = record.subarray(48, 80);
|
|
493
|
+
if (!trustedHolders.some((h) => h.equals(holder))) {
|
|
494
|
+
return serialize({ verdict: "rejected", reason: "holder not authorized" });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (args.bindingProof) {
|
|
498
|
+
const exporter = (0, import_transport_context.currentTransportBinding)()?.exporter;
|
|
499
|
+
if (!exporter) return serialize({ verdict: "rejected", reason: "live TLS exporter unavailable" });
|
|
500
|
+
let exporterOutput;
|
|
501
|
+
try {
|
|
502
|
+
exporterOutput = exporter((0, import_pot_record_v2.computeV2ExporterContext)(record));
|
|
503
|
+
} catch {
|
|
504
|
+
return serialize({ verdict: "rejected", reason: "TLS exporter derivation failed" });
|
|
505
|
+
}
|
|
506
|
+
const binding = (0, import_pot_record_v2.verifyV2BindingProof)(record, Buffer.from(args.bindingProof, "hex"), exporterOutput);
|
|
507
|
+
if (binding.verdict === "rejected") return serialize(binding);
|
|
508
|
+
}
|
|
509
|
+
if (process.env.TTTPS_REQUIRE_REPLAY_LEDGER === "1") {
|
|
510
|
+
const ctxId = record.subarray(16, 32).toString("hex");
|
|
511
|
+
const replay = await claimV2Replay(ctxId, result.nonce.toString("hex"));
|
|
512
|
+
if (replay !== "claimed") return serialize({ verdict: "rejected", reason: replay === "replay" ? "replay detected" : "replay ledger unavailable" });
|
|
513
|
+
}
|
|
514
|
+
return serialize({ verdict: "intact", wireProfile: "draft-helmprotocol-tttps-11-v2", nonce: result.nonce.toString("hex") });
|
|
515
|
+
}
|
|
516
|
+
const V2_AUDIT_STREAM = process.env.TTTPS_AUDIT_STREAM ?? "tttps:audit:v2";
|
|
517
|
+
const V2_AUDIT_MAXLEN = Number.parseInt(process.env.TTTPS_AUDIT_MAXLEN ?? "100000", 10);
|
|
518
|
+
const TTTPS_FORMAL_RECEIPT = {
|
|
519
|
+
spec: "draft-helmprotocol-tttps-11",
|
|
520
|
+
// Honest status. There is a TLA+ model (TLC model-checked) and a Lean 4 module whose
|
|
521
|
+
// theorems the kernel accepts (sorry 0, axiom propext), and this runtime. They are
|
|
522
|
+
// RELATED artifacts, not a machine-checked end-to-end 1:1 equivalence. Treat this as
|
|
523
|
+
// provenance the auditor can chase, not proof of the byte parser or the cryptography.
|
|
524
|
+
binding_status: "provenance-only; TLA+<->Lean<->runtime 1:1 equivalence is NOT machine-checked end-to-end (HOLD)",
|
|
525
|
+
lean: {
|
|
526
|
+
module: "KLean.TTTPS.Core",
|
|
527
|
+
theorems: [
|
|
528
|
+
"invalid_ingress_no_mutation",
|
|
529
|
+
"parse_is_deterministic",
|
|
530
|
+
"parsed_record_has_fixed_size",
|
|
531
|
+
"valid_ingress_commit_delta",
|
|
532
|
+
"zero_state_preservation"
|
|
533
|
+
],
|
|
534
|
+
axioms: ["propext"],
|
|
535
|
+
content_hash: "sha256:6116b82318d540a1e4e5b694e31978739851f658356330349b9d4289aeb5bae8",
|
|
536
|
+
abstraction: "models admission at frame-size (=180) granularity; does NOT model field offsets, Ed25519, SHA-256, or the TLS exporter"
|
|
537
|
+
},
|
|
538
|
+
tla: { spec: "TTTPS_Ingress_Gate.tla", method: "TLC bounded model check (not an unbounded theorem)" },
|
|
539
|
+
anchor: {
|
|
540
|
+
receipt_id: "33108706ed730e5296ae1b06",
|
|
541
|
+
time_source: "roughtime_chain",
|
|
542
|
+
verify_url: "https://kpp.kenosian.com/v1/verify?receipt_id=33108706ed730e5296ae1b06"
|
|
543
|
+
},
|
|
544
|
+
revision: { package_commit: "677a3b9", note: "live gate runs this revision with admission flags ON; published npm 0.4.0 bytes carry the v2 core only" },
|
|
545
|
+
scope: "Kernel-checks the 180-octet parse + admission state boundary (invalid ingress => zero state mutation). Does NOT prove Ed25519/SHA-256/TLS-exporter cryptographic security, nor a byte-exact parser equivalence."
|
|
546
|
+
};
|
|
547
|
+
async function potVerifyV2(args) {
|
|
548
|
+
const startedNs = process.hrtime.bigint();
|
|
549
|
+
let result;
|
|
550
|
+
try {
|
|
551
|
+
result = await potVerifyV2Core(args);
|
|
552
|
+
} catch (e) {
|
|
553
|
+
result = { verdict: "rejected", reason: `verifier exception: ${e instanceof Error ? e.message : String(e)}` };
|
|
554
|
+
}
|
|
555
|
+
const serverLatencyUs = Number((process.hrtime.bigint() - startedNs) / 1000n);
|
|
556
|
+
const binding = (0, import_transport_context.currentTransportBinding)();
|
|
557
|
+
const recordHex = typeof args.potRecordV2 === "string" ? args.potRecordV2 : "";
|
|
558
|
+
const record = Buffer.from(recordHex, "hex");
|
|
559
|
+
const fields = {
|
|
560
|
+
ts_ms: String(Date.now()),
|
|
561
|
+
verdict: String(result.verdict ?? "unknown"),
|
|
562
|
+
reason: String(result.reason ?? ""),
|
|
563
|
+
case: String(args.clientId ?? binding?.clientId ?? ""),
|
|
564
|
+
session: String(args.sessionId ?? binding?.sessionId ?? ""),
|
|
565
|
+
remote: String(binding?.remoteAddress ?? ""),
|
|
566
|
+
transport: binding?.exporter ? "tls13" : "none",
|
|
567
|
+
binding_proof: args.bindingProof ? "present" : "absent",
|
|
568
|
+
record_len: String(record.length),
|
|
569
|
+
record_sha16: record.length ? (0, import_crypto.createHash)("sha256").update(record).digest("hex").slice(0, 16) : "",
|
|
570
|
+
ctx_id: record.length >= 48 ? record.subarray(16, 32).toString("hex") : "",
|
|
571
|
+
nonce: record.length >= 48 ? record.subarray(32, 48).toString("hex") : "",
|
|
572
|
+
latency_us: String(serverLatencyUs)
|
|
573
|
+
};
|
|
574
|
+
const flat = [];
|
|
575
|
+
for (const [k, v] of Object.entries(fields)) flat.push(k, v);
|
|
576
|
+
redis.call("XADD", V2_AUDIT_STREAM, "MAXLEN", "~", String(V2_AUDIT_MAXLEN), "*", ...flat).catch(() => void 0);
|
|
577
|
+
const emitFormal = process.env.TTTPS_EMIT_FORMAL_RECEIPT !== "0";
|
|
578
|
+
return serialize({ ...result, serverLatencyUs, ...emitFormal ? { formal: TTTPS_FORMAL_RECEIPT } : {} });
|
|
579
|
+
}
|
|
377
580
|
async function potVerifyV08(args) {
|
|
378
581
|
(0, import_telemetry.telemetryIncrement)("pot_verify_v08");
|
|
379
582
|
const record = Buffer.from(args.potRecordV08, "hex");
|
|
380
583
|
const ctxId = args.ctxId ?? DEFAULT_CTX_ID_V08;
|
|
381
584
|
const issuerPubKey = args.issuerPubKey !== void 0 ? Buffer.from(args.issuerPubKey, "hex") : potSignerPublicKeyRawV08;
|
|
382
585
|
const content = args.content !== void 0 ? Buffer.from(args.content, "utf8") : void 0;
|
|
383
|
-
const
|
|
586
|
+
const requireFreshness = requiredFlag("TTTPS_REQUIRE_V08_FRESHNESS");
|
|
587
|
+
const freshness = configuredFreshnessPolicy();
|
|
588
|
+
if (requireFreshness && freshness === void 0) {
|
|
589
|
+
return serialize({ verdict: "rejected", reason: "freshness policy is not configured", ctxId, verifiedAt: Date.now() });
|
|
590
|
+
}
|
|
591
|
+
const result = (0, import_pot_record_v08.verifyPotRecordV08)(record, ctxId, issuerPubKey, content, freshness);
|
|
592
|
+
if (result.verdict === "rejected") {
|
|
593
|
+
return serialize({
|
|
594
|
+
verdict: result.verdict,
|
|
595
|
+
reason: result.reason ?? null,
|
|
596
|
+
payloadDigestMatchesContent: result.payloadDigestMatchesContent ?? null,
|
|
597
|
+
ctxId,
|
|
598
|
+
verifiedAt: Date.now()
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
let replayClaim = "not_required";
|
|
602
|
+
if (requiredFlag("TTTPS_REQUIRE_REPLAY_LEDGER")) {
|
|
603
|
+
const decoded = (0, import_pot_record_v08.decodePotRecordV08)(record);
|
|
604
|
+
replayClaim = await claimV08Replay(args.clientId ?? "", args.sessionId ?? "", decoded.nonce.toString("hex"));
|
|
605
|
+
if (replayClaim !== "claimed") {
|
|
606
|
+
return serialize({
|
|
607
|
+
verdict: "rejected",
|
|
608
|
+
reason: replayClaim === "replay" ? "replay detected" : "replay ledger unavailable",
|
|
609
|
+
replayClaim,
|
|
610
|
+
ctxId,
|
|
611
|
+
verifiedAt: Date.now()
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
384
615
|
return serialize({
|
|
385
616
|
verdict: result.verdict,
|
|
386
617
|
reason: result.reason ?? null,
|
|
387
618
|
payloadDigestMatchesContent: result.payloadDigestMatchesContent ?? null,
|
|
619
|
+
replayClaim,
|
|
388
620
|
ctxId,
|
|
389
621
|
verifiedAt: Date.now()
|
|
390
622
|
});
|
|
@@ -645,12 +877,14 @@ async function potCheckpoint(args) {
|
|
|
645
877
|
0 && (module.exports = {
|
|
646
878
|
potCheckpoint,
|
|
647
879
|
potGenerate,
|
|
880
|
+
potGenerateV2,
|
|
648
881
|
potGraph,
|
|
649
882
|
potHealth,
|
|
650
883
|
potQuery,
|
|
651
884
|
potStats,
|
|
652
885
|
potVerify,
|
|
653
886
|
potVerifyV08,
|
|
887
|
+
potVerifyV2,
|
|
654
888
|
redis,
|
|
655
889
|
restoreDagEntry,
|
|
656
890
|
tttsFreshnessSeal,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var transport_context_exports = {};
|
|
20
|
+
__export(transport_context_exports, {
|
|
21
|
+
currentTransportBinding: () => currentTransportBinding,
|
|
22
|
+
withTransportBinding: () => withTransportBinding
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(transport_context_exports);
|
|
25
|
+
var import_node_async_hooks = require("node:async_hooks");
|
|
26
|
+
const storage = new import_node_async_hooks.AsyncLocalStorage();
|
|
27
|
+
function withTransportBinding(context, fn) {
|
|
28
|
+
return storage.run(context, fn);
|
|
29
|
+
}
|
|
30
|
+
function currentTransportBinding() {
|
|
31
|
+
return storage.getStore();
|
|
32
|
+
}
|
|
33
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
34
|
+
0 && (module.exports = {
|
|
35
|
+
currentTransportBinding,
|
|
36
|
+
withTransportBinding
|
|
37
|
+
});
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helm-protocol/ttt-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Proof-of-Time attestation — Ed25519-signed timestamps with multi-source corroboration and explicit error bounds. IETF draft-helmprotocol-tttps",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"ttt-mcp": "
|
|
7
|
+
"ttt-mcp": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
11
|
"README.md"
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
|
-
"build": "esbuild index.ts tools.ts telemetry.ts auth.ts server.ts pot_record_v08.ts --platform=node --target=node18 --format=cjs --outdir=dist",
|
|
14
|
+
"build": "esbuild index.ts tools.ts telemetry.ts auth.ts server.ts pot_record_v08.ts pot_record_v2.ts tls_exporter.ts transport_context.ts --platform=node --target=node18 --format=cjs --outdir=dist",
|
|
15
15
|
"start": "node dist/index.js",
|
|
16
16
|
"dev": "npx ts-node index.ts",
|
|
17
17
|
"test": "jest --forceExit",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"license": "BSL-1.1",
|
|
51
51
|
"repository": {
|
|
52
52
|
"type": "git",
|
|
53
|
-
"url": "https://github.com/Helm-Protocol/openttt-mcp"
|
|
53
|
+
"url": "git+https://github.com/Helm-Protocol/openttt-mcp.git"
|
|
54
54
|
},
|
|
55
55
|
"homepage": "https://github.com/Helm-Protocol/openttt-mcp#readme",
|
|
56
56
|
"engines": {
|
|
@@ -70,5 +70,8 @@
|
|
|
70
70
|
"ts-jest": "^29.4.11",
|
|
71
71
|
"typescript": "^5.3.3"
|
|
72
72
|
},
|
|
73
|
-
"mcpName": "io.github.Helm-Protocol/openttt-pot"
|
|
73
|
+
"mcpName": "io.github.Helm-Protocol/openttt-pot",
|
|
74
|
+
"publishConfig": {
|
|
75
|
+
"access": "public"
|
|
76
|
+
}
|
|
74
77
|
}
|