protect-mcp 0.9.7 → 0.11.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/CHANGELOG.md +102 -0
- package/dist/{chunk-AUUAVWQM.mjs → chunk-3FSKOGGK.mjs} +28 -8
- package/dist/{chunk-MOXINIMB.mjs → chunk-BXZUS6HS.mjs} +2 -2
- package/dist/{chunk-G6X763MH.mjs → chunk-CIWIK6BT.mjs} +5 -251
- package/dist/{chunk-MWXDXYWH.mjs → chunk-FGCNKEEW.mjs} +86 -9
- package/dist/{chunk-UWB5ALVO.mjs → chunk-K7AEEQWW.mjs} +17 -94
- package/dist/{chunk-WWPQNIVF.mjs → chunk-KRKZ2YX7.mjs} +2 -7
- package/dist/{chunk-YM6SOJBR.mjs → chunk-NQOKPBSQ.mjs} +19 -6
- package/dist/{chunk-JQDVKZBN.mjs → chunk-SOJBLPPX.mjs} +7 -2
- package/dist/chunk-WMIWQ6TS.mjs +266 -0
- package/dist/chunk-XOP3PEBM.mjs +114 -0
- package/dist/{chunk-SETXVE2K.mjs → chunk-ZG6NAATA.mjs} +17 -14
- package/dist/{claim-RIXFOQM7.mjs → claim-MPXJRCFC.mjs} +4 -12
- package/dist/cli.js +1166 -3391
- package/dist/cli.mjs +131 -37
- package/dist/coverage-NB55FTYL.mjs +237 -0
- package/dist/demo-server.js +17 -14
- package/dist/demo-server.mjs +1 -1
- package/dist/hook-server.js +177 -436
- package/dist/hook-server.mjs +6 -7
- package/dist/{http-transport-VHD3YBT5.mjs → http-transport-BHHLPYL2.mjs} +8 -3
- package/dist/index.d.mts +98 -7
- package/dist/index.d.ts +98 -7
- package/dist/index.js +777 -4007
- package/dist/index.mjs +39 -1131
- package/dist/mcp-server.js +154 -14
- package/dist/mcp-server.mjs +24 -14
- package/dist/{report-5XCNW6FB.mjs → report-5B7EO3QB.mjs} +2 -1
- package/dist/{sample-6LRP73ZD.mjs → sample-O3KYZWOX.mjs} +4 -12
- package/dist/{signing-committed-TGWXSLAO.mjs → signing-committed-7VEKAJ4A.mjs} +1 -5
- package/package.json +5 -5
- package/dist/chunk-AYNQIEN7.mjs +0 -10
- package/dist/chunk-D733KAPG.mjs +0 -252
- package/dist/chunk-FFVJL3KQ.mjs +0 -2039
- package/dist/chunk-JIDDQUSQ.mjs +0 -568
- package/dist/ed25519-SQA3S2RV.mjs +0 -39
- package/dist/utils-6AYZFE5A.mjs +0 -77
|
@@ -1,22 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
} from "
|
|
4
|
-
import {
|
|
5
|
-
ed25519
|
|
6
|
-
} from "./chunk-FFVJL3KQ.mjs";
|
|
7
|
-
import {
|
|
8
|
-
Hash,
|
|
9
|
-
abytes,
|
|
10
|
-
aexists,
|
|
11
|
-
ahash,
|
|
12
|
-
bytesToHex,
|
|
13
|
-
clean,
|
|
14
|
-
hexToBytes,
|
|
15
|
-
randomBytes,
|
|
16
|
-
toBytes
|
|
17
|
-
} from "./chunk-D733KAPG.mjs";
|
|
1
|
+
// src/signing-committed.ts
|
|
2
|
+
import { ed25519 } from "@noble/curves/ed25519";
|
|
3
|
+
import { sha256 as sha2563 } from "@noble/hashes/sha256";
|
|
4
|
+
import { bytesToHex as bytesToHex3, hexToBytes as hexToBytes3, randomBytes as randomBytes2 } from "@noble/hashes/utils";
|
|
18
5
|
|
|
19
6
|
// src/commitments/merkle.ts
|
|
7
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
8
|
+
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
|
20
9
|
var DOMAIN_LEAF = 0;
|
|
21
10
|
var DOMAIN_INTERNAL = 1;
|
|
22
11
|
function hashLeaf(leafBytes) {
|
|
@@ -127,75 +116,10 @@ function largestPowerOfTwoLessThan(n) {
|
|
|
127
116
|
return k;
|
|
128
117
|
}
|
|
129
118
|
|
|
130
|
-
// node_modules/@noble/hashes/esm/hmac.js
|
|
131
|
-
var HMAC = class extends Hash {
|
|
132
|
-
constructor(hash, _key) {
|
|
133
|
-
super();
|
|
134
|
-
this.finished = false;
|
|
135
|
-
this.destroyed = false;
|
|
136
|
-
ahash(hash);
|
|
137
|
-
const key = toBytes(_key);
|
|
138
|
-
this.iHash = hash.create();
|
|
139
|
-
if (typeof this.iHash.update !== "function")
|
|
140
|
-
throw new Error("Expected instance of class which extends utils.Hash");
|
|
141
|
-
this.blockLen = this.iHash.blockLen;
|
|
142
|
-
this.outputLen = this.iHash.outputLen;
|
|
143
|
-
const blockLen = this.blockLen;
|
|
144
|
-
const pad = new Uint8Array(blockLen);
|
|
145
|
-
pad.set(key.length > blockLen ? hash.create().update(key).digest() : key);
|
|
146
|
-
for (let i = 0; i < pad.length; i++)
|
|
147
|
-
pad[i] ^= 54;
|
|
148
|
-
this.iHash.update(pad);
|
|
149
|
-
this.oHash = hash.create();
|
|
150
|
-
for (let i = 0; i < pad.length; i++)
|
|
151
|
-
pad[i] ^= 54 ^ 92;
|
|
152
|
-
this.oHash.update(pad);
|
|
153
|
-
clean(pad);
|
|
154
|
-
}
|
|
155
|
-
update(buf) {
|
|
156
|
-
aexists(this);
|
|
157
|
-
this.iHash.update(buf);
|
|
158
|
-
return this;
|
|
159
|
-
}
|
|
160
|
-
digestInto(out) {
|
|
161
|
-
aexists(this);
|
|
162
|
-
abytes(out, this.outputLen);
|
|
163
|
-
this.finished = true;
|
|
164
|
-
this.iHash.digestInto(out);
|
|
165
|
-
this.oHash.update(out);
|
|
166
|
-
this.oHash.digestInto(out);
|
|
167
|
-
this.destroy();
|
|
168
|
-
}
|
|
169
|
-
digest() {
|
|
170
|
-
const out = new Uint8Array(this.oHash.outputLen);
|
|
171
|
-
this.digestInto(out);
|
|
172
|
-
return out;
|
|
173
|
-
}
|
|
174
|
-
_cloneInto(to) {
|
|
175
|
-
to || (to = Object.create(Object.getPrototypeOf(this), {}));
|
|
176
|
-
const { oHash, iHash, finished, destroyed, blockLen, outputLen } = this;
|
|
177
|
-
to = to;
|
|
178
|
-
to.finished = finished;
|
|
179
|
-
to.destroyed = destroyed;
|
|
180
|
-
to.blockLen = blockLen;
|
|
181
|
-
to.outputLen = outputLen;
|
|
182
|
-
to.oHash = oHash._cloneInto(to.oHash);
|
|
183
|
-
to.iHash = iHash._cloneInto(to.iHash);
|
|
184
|
-
return to;
|
|
185
|
-
}
|
|
186
|
-
clone() {
|
|
187
|
-
return this._cloneInto();
|
|
188
|
-
}
|
|
189
|
-
destroy() {
|
|
190
|
-
this.destroyed = true;
|
|
191
|
-
this.oHash.destroy();
|
|
192
|
-
this.iHash.destroy();
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
var hmac = (hash, key, message) => new HMAC(hash, key).update(message).digest();
|
|
196
|
-
hmac.create = (hash, key) => new HMAC(hash, key);
|
|
197
|
-
|
|
198
119
|
// src/commitments/primitives.ts
|
|
120
|
+
import { sha256 as sha2562 } from "@noble/hashes/sha256";
|
|
121
|
+
import { hmac } from "@noble/hashes/hmac";
|
|
122
|
+
import { bytesToHex as bytesToHex2, hexToBytes as hexToBytes2, randomBytes } from "@noble/hashes/utils";
|
|
199
123
|
function jcs(value) {
|
|
200
124
|
if (value === null || value === void 0) return "null";
|
|
201
125
|
if (typeof value === "boolean" || typeof value === "number")
|
|
@@ -257,7 +181,7 @@ function leavesFromFields(fields) {
|
|
|
257
181
|
|
|
258
182
|
// src/signing-committed.ts
|
|
259
183
|
function freshSalt() {
|
|
260
|
-
return
|
|
184
|
+
return randomBytes2(32);
|
|
261
185
|
}
|
|
262
186
|
function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey, kid, issuer) {
|
|
263
187
|
const allFields = {
|
|
@@ -297,7 +221,7 @@ function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey
|
|
|
297
221
|
const { sorted, leafBytes } = leavesFromFields(committedFields);
|
|
298
222
|
const leafHashes = leafBytes.map(hashLeaf);
|
|
299
223
|
const root = merkleRoot(leafHashes);
|
|
300
|
-
committedFieldsRoot =
|
|
224
|
+
committedFieldsRoot = bytesToHex3(root);
|
|
301
225
|
sorted.forEach((f, i) => {
|
|
302
226
|
openings[f.name] = { name: f.name, value: f.value, salt: f.salt, index: i };
|
|
303
227
|
});
|
|
@@ -314,8 +238,8 @@ function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey
|
|
|
314
238
|
payload.committed_field_names = committedFields.map((f) => f.name);
|
|
315
239
|
}
|
|
316
240
|
const canonical = jcs(payload);
|
|
317
|
-
const messageHash =
|
|
318
|
-
const signatureBytes = ed25519.sign(messageHash,
|
|
241
|
+
const messageHash = sha2563(new TextEncoder().encode(canonical));
|
|
242
|
+
const signatureBytes = ed25519.sign(messageHash, hexToBytes3(signingKey));
|
|
319
243
|
const signedReceipt = {
|
|
320
244
|
...payload,
|
|
321
245
|
signature: {
|
|
@@ -328,7 +252,7 @@ function signCommittedDecision(entry, committedFieldNames, signingKey, publicKey
|
|
|
328
252
|
}
|
|
329
253
|
};
|
|
330
254
|
const signedJson = JSON.stringify(signedReceipt);
|
|
331
|
-
const receiptHash =
|
|
255
|
+
const receiptHash = bytesToHex3(sha2563(new TextEncoder().encode(jcs(signedReceipt))));
|
|
332
256
|
return {
|
|
333
257
|
signed: signedJson,
|
|
334
258
|
artifact_type: "decision_receipt_committed_v1",
|
|
@@ -457,7 +381,7 @@ function committedFieldNamesFromReceipt(receipt, openings) {
|
|
|
457
381
|
return Array.from(new Set(names)).sort();
|
|
458
382
|
}
|
|
459
383
|
function receiptHashHex(receipt) {
|
|
460
|
-
return
|
|
384
|
+
return bytesToHex3(sha2563(new TextEncoder().encode(jcs(receipt))));
|
|
461
385
|
}
|
|
462
386
|
function verifyCommittedReceiptSignature(receipt) {
|
|
463
387
|
const signature = receipt.signature;
|
|
@@ -467,16 +391,15 @@ function verifyCommittedReceiptSignature(receipt) {
|
|
|
467
391
|
return null;
|
|
468
392
|
}
|
|
469
393
|
const { signature: _signature, ...payloadWithoutSig } = receipt;
|
|
470
|
-
const messageHash =
|
|
394
|
+
const messageHash = sha2563(new TextEncoder().encode(jcs(payloadWithoutSig)));
|
|
471
395
|
try {
|
|
472
|
-
return ed25519.verify(base64urlDecode(sig.sig), messageHash,
|
|
396
|
+
return ed25519.verify(base64urlDecode(sig.sig), messageHash, hexToBytes3(sig.public_key));
|
|
473
397
|
} catch {
|
|
474
398
|
return false;
|
|
475
399
|
}
|
|
476
400
|
}
|
|
477
401
|
|
|
478
402
|
export {
|
|
479
|
-
hmac,
|
|
480
403
|
signCommittedDecision,
|
|
481
404
|
discloseField,
|
|
482
405
|
createSelectiveDisclosurePackage,
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
sha256
|
|
3
|
-
} from "./chunk-AYNQIEN7.mjs";
|
|
4
|
-
import {
|
|
5
|
-
bytesToHex
|
|
6
|
-
} from "./chunk-D733KAPG.mjs";
|
|
7
|
-
|
|
8
1
|
// src/receipt-enrichment.ts
|
|
2
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
3
|
+
import { bytesToHex } from "@noble/hashes/utils";
|
|
9
4
|
var ENRICHMENT_VERSION = 2;
|
|
10
5
|
function canonicalJson(value) {
|
|
11
6
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ReceiptBuffer,
|
|
3
3
|
buildActionReadback,
|
|
4
|
+
startStatusServer
|
|
5
|
+
} from "./chunk-CIWIK6BT.mjs";
|
|
6
|
+
import {
|
|
4
7
|
checkRateLimit,
|
|
5
8
|
getToolPolicy,
|
|
6
9
|
isSigningEnabled,
|
|
7
10
|
parseRateLimit,
|
|
8
|
-
signDecision
|
|
9
|
-
|
|
10
|
-
} from "./chunk-G6X763MH.mjs";
|
|
11
|
+
signDecision
|
|
12
|
+
} from "./chunk-WMIWQ6TS.mjs";
|
|
11
13
|
import {
|
|
12
14
|
evaluateCedar
|
|
13
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-FGCNKEEW.mjs";
|
|
16
|
+
import {
|
|
17
|
+
receiptHash
|
|
18
|
+
} from "./chunk-XOP3PEBM.mjs";
|
|
14
19
|
|
|
15
20
|
// src/evidence-store.ts
|
|
16
21
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
@@ -589,7 +594,7 @@ function parseNotificationConfigFromEnv() {
|
|
|
589
594
|
import { spawn } from "child_process";
|
|
590
595
|
import { randomUUID, randomBytes } from "crypto";
|
|
591
596
|
import { createInterface } from "readline";
|
|
592
|
-
import { appendFileSync } from "fs";
|
|
597
|
+
import { appendFileSync, readFileSync as readFileSync2 } from "fs";
|
|
593
598
|
import { join as join2 } from "path";
|
|
594
599
|
var LOG_FILE = ".protect-mcp-log.jsonl";
|
|
595
600
|
var RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
|
|
@@ -600,6 +605,8 @@ var ProtectGateway = class {
|
|
|
600
605
|
clientReader = null;
|
|
601
606
|
logFilePath;
|
|
602
607
|
receiptFilePath;
|
|
608
|
+
/** s5.7 hash of the last line appended to the receipt file (chain link) */
|
|
609
|
+
lastReceiptHash = null;
|
|
603
610
|
evidenceStore;
|
|
604
611
|
receiptBuffer;
|
|
605
612
|
/** Approval grants keyed by request_id (scoped to the specific action that was requested) */
|
|
@@ -619,6 +626,11 @@ var ProtectGateway = class {
|
|
|
619
626
|
this.config = config;
|
|
620
627
|
this.logFilePath = join2(process.cwd(), LOG_FILE);
|
|
621
628
|
this.receiptFilePath = join2(process.cwd(), RECEIPTS_FILE);
|
|
629
|
+
try {
|
|
630
|
+
const existing = readFileSync2(this.receiptFilePath, "utf-8").split("\n").filter((l) => l.trim());
|
|
631
|
+
if (existing.length > 0) this.lastReceiptHash = receiptHash(JSON.parse(existing[existing.length - 1]));
|
|
632
|
+
} catch {
|
|
633
|
+
}
|
|
622
634
|
this.evidenceStore = new EvidenceStore();
|
|
623
635
|
this.receiptBuffer = new ReceiptBuffer();
|
|
624
636
|
this.notificationConfig = parseNotificationConfigFromEnv();
|
|
@@ -965,12 +977,13 @@ var ProtectGateway = class {
|
|
|
965
977
|
} catch {
|
|
966
978
|
}
|
|
967
979
|
if (isSigningEnabled()) {
|
|
968
|
-
const signed = signDecision(log);
|
|
980
|
+
const signed = signDecision(log, this.lastReceiptHash || void 0);
|
|
969
981
|
if (signed.signed) {
|
|
970
982
|
process.stderr.write(`[PROTECT_MCP_RECEIPT] ${signed.signed}
|
|
971
983
|
`);
|
|
972
984
|
try {
|
|
973
985
|
appendFileSync(this.receiptFilePath, signed.signed + "\n");
|
|
986
|
+
if (signed.receipt_hash) this.lastReceiptHash = signed.receipt_hash;
|
|
974
987
|
} catch {
|
|
975
988
|
}
|
|
976
989
|
this.receiptBuffer.add(log.request_id, signed.signed);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
receiptIdentity
|
|
3
|
+
} from "./chunk-XOP3PEBM.mjs";
|
|
4
|
+
|
|
1
5
|
// src/report.ts
|
|
2
6
|
import { readFileSync, existsSync } from "fs";
|
|
3
7
|
function generateReport(logPath, receiptPath, periodDays) {
|
|
@@ -34,8 +38,9 @@ function generateReport(logPath, receiptPath, periodDays) {
|
|
|
34
38
|
const parsed = JSON.parse(trimmed);
|
|
35
39
|
if (parsed.signature) {
|
|
36
40
|
receiptsSigned++;
|
|
37
|
-
|
|
38
|
-
if (
|
|
41
|
+
const identity = receiptIdentity(parsed);
|
|
42
|
+
if (identity.kid && !signerKid) signerKid = identity.kid;
|
|
43
|
+
if (identity.issuer && !signerIssuer) signerIssuer = identity.issuer;
|
|
39
44
|
}
|
|
40
45
|
} catch {
|
|
41
46
|
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import {
|
|
2
|
+
digestBuiltinPolicy
|
|
3
|
+
} from "./chunk-FGCNKEEW.mjs";
|
|
4
|
+
import {
|
|
5
|
+
computeSbIssuerKid,
|
|
6
|
+
createReceiptEnvelope
|
|
7
|
+
} from "./chunk-XOP3PEBM.mjs";
|
|
8
|
+
|
|
9
|
+
// src/policy.ts
|
|
10
|
+
import { readFileSync } from "fs";
|
|
11
|
+
function loadPolicy(path) {
|
|
12
|
+
const raw = readFileSync(path, "utf-8");
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
if (!parsed.tools || typeof parsed.tools !== "object") {
|
|
15
|
+
throw new Error(`Invalid policy file: missing "tools" object in ${path}`);
|
|
16
|
+
}
|
|
17
|
+
const policy = {
|
|
18
|
+
tools: parsed.tools,
|
|
19
|
+
default_tier: parsed.default_tier || "unknown",
|
|
20
|
+
policy_engine: parsed.policy_engine || "built-in",
|
|
21
|
+
...parsed.external ? { external: parsed.external } : {}
|
|
22
|
+
};
|
|
23
|
+
const digest = computePolicyDigest(policy);
|
|
24
|
+
return {
|
|
25
|
+
policy,
|
|
26
|
+
digest,
|
|
27
|
+
credentials: parsed.credentials,
|
|
28
|
+
signing: parsed.signing
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function computePolicyDigest(policy) {
|
|
32
|
+
return digestBuiltinPolicy(policy).policy_digest;
|
|
33
|
+
}
|
|
34
|
+
function getToolPolicy(toolName, policy) {
|
|
35
|
+
if (!policy) {
|
|
36
|
+
return { require: "any" };
|
|
37
|
+
}
|
|
38
|
+
if (policy.tools[toolName]) {
|
|
39
|
+
return policy.tools[toolName];
|
|
40
|
+
}
|
|
41
|
+
if (policy.tools["*"]) {
|
|
42
|
+
return policy.tools["*"];
|
|
43
|
+
}
|
|
44
|
+
return { require: "any" };
|
|
45
|
+
}
|
|
46
|
+
function parseRateLimit(spec) {
|
|
47
|
+
const match = spec.match(/^(\d+)\/(second|minute|hour|day)$/);
|
|
48
|
+
if (!match) {
|
|
49
|
+
throw new Error(`Invalid rate limit format: "${spec}". Expected "N/unit" (e.g. "5/hour")`);
|
|
50
|
+
}
|
|
51
|
+
const count = parseInt(match[1], 10);
|
|
52
|
+
const unit = match[2];
|
|
53
|
+
const windowMs = {
|
|
54
|
+
second: 1e3,
|
|
55
|
+
minute: 6e4,
|
|
56
|
+
hour: 36e5,
|
|
57
|
+
day: 864e5
|
|
58
|
+
};
|
|
59
|
+
return { count, windowMs: windowMs[unit] };
|
|
60
|
+
}
|
|
61
|
+
function checkRateLimit(key, limit, store) {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const windowStart = now - limit.windowMs;
|
|
64
|
+
const timestamps = (store.get(key) || []).filter((t) => t > windowStart);
|
|
65
|
+
if (timestamps.length >= limit.count) {
|
|
66
|
+
store.set(key, timestamps);
|
|
67
|
+
return { allowed: false, remaining: 0 };
|
|
68
|
+
}
|
|
69
|
+
timestamps.push(now);
|
|
70
|
+
store.set(key, timestamps);
|
|
71
|
+
return { allowed: true, remaining: limit.count - timestamps.length };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/signing.ts
|
|
75
|
+
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
76
|
+
var signerState = null;
|
|
77
|
+
var signingConfigured = false;
|
|
78
|
+
var signingInitError = null;
|
|
79
|
+
async function initSigning(config) {
|
|
80
|
+
const warnings = [];
|
|
81
|
+
signerState = null;
|
|
82
|
+
signingConfigured = Boolean(config && config.enabled !== false);
|
|
83
|
+
signingInitError = null;
|
|
84
|
+
if (!config || config.enabled === false) {
|
|
85
|
+
return warnings;
|
|
86
|
+
}
|
|
87
|
+
if (!config.key_path) {
|
|
88
|
+
signingInitError = "signing enabled but key_path is not configured";
|
|
89
|
+
warnings.push(`signing: ${signingInitError}`);
|
|
90
|
+
return warnings;
|
|
91
|
+
}
|
|
92
|
+
if (!existsSync(config.key_path)) {
|
|
93
|
+
signingInitError = `key file not found at ${config.key_path}`;
|
|
94
|
+
warnings.push(`signing: ${signingInitError} \u2014 run "protect-mcp init" to generate`);
|
|
95
|
+
return warnings;
|
|
96
|
+
}
|
|
97
|
+
let keyData;
|
|
98
|
+
try {
|
|
99
|
+
keyData = JSON.parse(readFileSync2(config.key_path, "utf-8"));
|
|
100
|
+
if (!keyData.privateKey || !keyData.publicKey) {
|
|
101
|
+
signingInitError = "key file missing privateKey or publicKey fields";
|
|
102
|
+
warnings.push(`signing: ${signingInitError}`);
|
|
103
|
+
return warnings;
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
signingInitError = `failed to load key file: ${err instanceof Error ? err.message : err}`;
|
|
107
|
+
warnings.push(`signing: ${signingInitError}`);
|
|
108
|
+
return warnings;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
signerState = {
|
|
112
|
+
privateKey: keyData.privateKey,
|
|
113
|
+
publicKey: keyData.publicKey,
|
|
114
|
+
// kid is opaque per draft-02; existing key files keep their explicit kid,
|
|
115
|
+
// and keys without one get the s2.1.1 RECOMMENDED sb:issuer format.
|
|
116
|
+
kid: keyData.kid || computeSbIssuerKid(keyData.publicKey),
|
|
117
|
+
issuer: config.issuer || keyData.issuer || "protect-mcp"
|
|
118
|
+
};
|
|
119
|
+
} catch (err) {
|
|
120
|
+
signingInitError = `failed to initialize signer: ${err instanceof Error ? err.message : err}`;
|
|
121
|
+
warnings.push(`signing: ${signingInitError} \u2014 enforce mode will fail closed`);
|
|
122
|
+
}
|
|
123
|
+
return warnings;
|
|
124
|
+
}
|
|
125
|
+
function signDecision(entry, prevReceiptHash) {
|
|
126
|
+
const artifactType = entry.decision === "deny" ? "gateway_restraint" : "decision_receipt";
|
|
127
|
+
if (signingConfigured && signingInitError) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
signed: null,
|
|
131
|
+
artifact_type: artifactType,
|
|
132
|
+
warning: `signing initialization failed: ${signingInitError}`,
|
|
133
|
+
error: signingInitError
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (signingConfigured && !signerState) {
|
|
137
|
+
const error = "signing was configured but no signer is ready";
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
signed: null,
|
|
141
|
+
artifact_type: artifactType,
|
|
142
|
+
warning: error,
|
|
143
|
+
error
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (!signerState) {
|
|
147
|
+
return { ok: false, signed: null, artifact_type: "none" };
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const payload = {
|
|
151
|
+
// draft-02 s3.1 access-decision fields
|
|
152
|
+
type: "protectmcp:decision",
|
|
153
|
+
tool_name: entry.tool,
|
|
154
|
+
decision: entry.decision,
|
|
155
|
+
reason: entry.reason_code,
|
|
156
|
+
policy_digest: entry.policy_digest,
|
|
157
|
+
// Extension fields (signed alongside the s3.1 core)
|
|
158
|
+
scope: entry.request_id,
|
|
159
|
+
// request scope
|
|
160
|
+
mode: entry.mode,
|
|
161
|
+
request_id: entry.request_id,
|
|
162
|
+
// Spec version: ties every receipt to the IETF standard
|
|
163
|
+
spec: "draft-farley-acta-signed-receipts-02",
|
|
164
|
+
// Issuer certification: distinguishes VOPRF-backed receipts from self-signed ones
|
|
165
|
+
// - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
|
|
166
|
+
// - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
|
|
167
|
+
// - uncertified = unsigned receipt (shadow mode, no signing configured)
|
|
168
|
+
issuer_certification: signerState ? "self-signed" : "uncertified",
|
|
169
|
+
// The signer's PUBLIC key, inside the signed payload, so a receipt is
|
|
170
|
+
// self-contained: any verifier (including the record viewer, in-browser)
|
|
171
|
+
// can check the signature without a side channel. Binding the key inside
|
|
172
|
+
// the signature means it cannot be swapped without breaking the signature;
|
|
173
|
+
// authenticity (that the key is YOUR gate's) still comes from pinning it.
|
|
174
|
+
public_key: signerState.publicKey
|
|
175
|
+
};
|
|
176
|
+
if (signerState.issuer && signerState.issuer !== signerState.kid) {
|
|
177
|
+
payload.issuer_name = signerState.issuer;
|
|
178
|
+
}
|
|
179
|
+
if (prevReceiptHash) payload.previousReceiptHash = prevReceiptHash;
|
|
180
|
+
if (entry.tier) payload.tier = entry.tier;
|
|
181
|
+
if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
|
|
182
|
+
if (entry.rate_limit_remaining !== void 0) {
|
|
183
|
+
payload.rate_limit_remaining = entry.rate_limit_remaining;
|
|
184
|
+
}
|
|
185
|
+
if (entry.policy_engine) payload.policy_engine = entry.policy_engine;
|
|
186
|
+
if (entry.hook_event) payload.hook_event = entry.hook_event;
|
|
187
|
+
if (entry.sandbox_state) payload.sandbox_state = entry.sandbox_state;
|
|
188
|
+
if (entry.timing) payload.timing = entry.timing;
|
|
189
|
+
if (entry.swarm) payload.swarm = entry.swarm;
|
|
190
|
+
if (entry.payload_digest) payload.payload_digest = entry.payload_digest;
|
|
191
|
+
if (entry.enrichment) payload.enrichment = entry.enrichment;
|
|
192
|
+
if (entry.action_readback) payload.action_readback = entry.action_readback;
|
|
193
|
+
if (entry.deny_iteration) payload.deny_iteration = entry.deny_iteration;
|
|
194
|
+
const result = createReceiptEnvelope(
|
|
195
|
+
payload,
|
|
196
|
+
signerState.privateKey,
|
|
197
|
+
signerState.kid,
|
|
198
|
+
Number.isFinite(entry.timestamp) ? new Date(entry.timestamp).toISOString() : void 0
|
|
199
|
+
);
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
signed: JSON.stringify(result.envelope),
|
|
203
|
+
artifact_type: artifactType,
|
|
204
|
+
receipt_hash: result.hash
|
|
205
|
+
};
|
|
206
|
+
} catch (err) {
|
|
207
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
signed: null,
|
|
211
|
+
artifact_type: artifactType,
|
|
212
|
+
warning: `signing failed: ${message}`,
|
|
213
|
+
error: message
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function signGenericArtifact(_artifactType, payload) {
|
|
218
|
+
if (signingConfigured && signingInitError) {
|
|
219
|
+
return { ok: false, signed: null, warning: `signing initialization failed: ${signingInitError}`, error: signingInitError };
|
|
220
|
+
}
|
|
221
|
+
if (signingConfigured && !signerState) {
|
|
222
|
+
const error = "signing was configured but no signer is ready";
|
|
223
|
+
return { ok: false, signed: null, warning: error, error };
|
|
224
|
+
}
|
|
225
|
+
if (!signerState) {
|
|
226
|
+
return { ok: false, signed: null };
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
const full = {
|
|
230
|
+
...payload,
|
|
231
|
+
type: String(payload.type || "protectmcp:artifact"),
|
|
232
|
+
public_key: signerState.publicKey
|
|
233
|
+
};
|
|
234
|
+
if (signerState.issuer && signerState.issuer !== signerState.kid) {
|
|
235
|
+
full.issuer_name = signerState.issuer;
|
|
236
|
+
}
|
|
237
|
+
const result = createReceiptEnvelope(full, signerState.privateKey, signerState.kid);
|
|
238
|
+
return { ok: true, signed: JSON.stringify(result.envelope) };
|
|
239
|
+
} catch (err) {
|
|
240
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
241
|
+
return { ok: false, signed: null, warning: `signing failed: ${message}`, error: message };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function getSignerInfo() {
|
|
245
|
+
if (!signerState) return null;
|
|
246
|
+
return {
|
|
247
|
+
publicKey: signerState.publicKey,
|
|
248
|
+
kid: signerState.kid,
|
|
249
|
+
issuer: signerState.issuer
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function isSigningEnabled() {
|
|
253
|
+
return signingConfigured && signingInitError === null && signerState !== null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export {
|
|
257
|
+
loadPolicy,
|
|
258
|
+
getToolPolicy,
|
|
259
|
+
parseRateLimit,
|
|
260
|
+
checkRateLimit,
|
|
261
|
+
initSigning,
|
|
262
|
+
signDecision,
|
|
263
|
+
signGenericArtifact,
|
|
264
|
+
getSignerInfo,
|
|
265
|
+
isSigningEnabled
|
|
266
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// src/acta-envelope.ts
|
|
2
|
+
import { ed25519 } from "@noble/curves/ed25519";
|
|
3
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
4
|
+
import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/hashes/utils";
|
|
5
|
+
function canonicalize(obj) {
|
|
6
|
+
return JSON.stringify(obj, (_key, value) => {
|
|
7
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
8
|
+
const sorted = {};
|
|
9
|
+
for (const k of Object.keys(value).sort()) {
|
|
10
|
+
if (!/^[\x20-\x7E]*$/.test(k)) {
|
|
11
|
+
throw new Error(`Non-ASCII key "${k}" in receipt payload. Only ASCII keys are permitted.`);
|
|
12
|
+
}
|
|
13
|
+
sorted[k] = value[k];
|
|
14
|
+
}
|
|
15
|
+
return sorted;
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function receiptHash(obj) {
|
|
21
|
+
return bytesToHex(sha256(utf8ToBytes(canonicalize(obj))));
|
|
22
|
+
}
|
|
23
|
+
var B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
24
|
+
function base58(bytes) {
|
|
25
|
+
let n = BigInt("0x" + bytesToHex(bytes));
|
|
26
|
+
let out = "";
|
|
27
|
+
while (n > 0n) {
|
|
28
|
+
out = B58_ALPHABET[Number(n % 58n)] + out;
|
|
29
|
+
n /= 58n;
|
|
30
|
+
}
|
|
31
|
+
for (const b of bytes) {
|
|
32
|
+
if (b === 0) out = "1" + out;
|
|
33
|
+
else break;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function computeSbIssuerKid(publicKeyHex) {
|
|
38
|
+
return `sb:issuer:${base58(hexToBytes(publicKeyHex)).slice(0, 12)}`;
|
|
39
|
+
}
|
|
40
|
+
function createReceiptEnvelope(fields, privateKeyHex, kid, issuedAt) {
|
|
41
|
+
if (!fields.type) throw new Error("receipt payload requires a type");
|
|
42
|
+
if (!kid) throw new Error("kid is required");
|
|
43
|
+
const payload = {
|
|
44
|
+
...fields,
|
|
45
|
+
issued_at: fields.issued_at || issuedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
46
|
+
issuer_id: kid
|
|
47
|
+
};
|
|
48
|
+
const sig = bytesToHex(ed25519.sign(utf8ToBytes(canonicalize(payload)), hexToBytes(privateKeyHex)));
|
|
49
|
+
const envelope = { payload, signature: { alg: "EdDSA", kid, sig } };
|
|
50
|
+
return { envelope, hash: receiptHash(envelope) };
|
|
51
|
+
}
|
|
52
|
+
function verifyReceipt(envelope, publicKeyHex) {
|
|
53
|
+
try {
|
|
54
|
+
if (!envelope || typeof envelope !== "object") {
|
|
55
|
+
return { valid: false, shape: null, error: "not_an_object" };
|
|
56
|
+
}
|
|
57
|
+
const env = envelope;
|
|
58
|
+
const signature = env.signature;
|
|
59
|
+
if (signature && typeof signature === "object" && !Array.isArray(signature)) {
|
|
60
|
+
const sigObj = signature;
|
|
61
|
+
if (sigObj.alg !== "EdDSA") {
|
|
62
|
+
return { valid: false, shape: "acta-02", error: `unsupported_alg:${String(sigObj.alg)}` };
|
|
63
|
+
}
|
|
64
|
+
if (typeof sigObj.sig !== "string" || !env.payload || typeof env.payload !== "object") {
|
|
65
|
+
return { valid: false, shape: "acta-02", error: "malformed_envelope" };
|
|
66
|
+
}
|
|
67
|
+
const message = utf8ToBytes(canonicalize(env.payload));
|
|
68
|
+
const valid = ed25519.verify(hexToBytes(sigObj.sig), message, hexToBytes(publicKeyHex));
|
|
69
|
+
return valid ? { valid: true, shape: "acta-02", hash: receiptHash(env) } : { valid: false, shape: "acta-02", error: "invalid_signature" };
|
|
70
|
+
}
|
|
71
|
+
if (typeof signature === "string") {
|
|
72
|
+
const rest = {};
|
|
73
|
+
for (const k of Object.keys(env)) if (k !== "signature") rest[k] = env[k];
|
|
74
|
+
const message = utf8ToBytes(canonicalize(rest));
|
|
75
|
+
const valid = ed25519.verify(hexToBytes(signature), message, hexToBytes(publicKeyHex));
|
|
76
|
+
const shape = env.v === 2 ? "legacy-v2" : "legacy-v1";
|
|
77
|
+
return valid ? { valid: true, shape, hash: receiptHash(env) } : { valid: false, shape, error: "invalid_signature" };
|
|
78
|
+
}
|
|
79
|
+
return { valid: false, shape: null, error: "missing_signature" };
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return {
|
|
82
|
+
valid: false,
|
|
83
|
+
shape: null,
|
|
84
|
+
error: `verification_error:${err instanceof Error ? err.message : "unknown"}`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function receiptIdentity(envelope) {
|
|
89
|
+
if (!envelope || typeof envelope !== "object") return { kid: null, issuer: null, type: null };
|
|
90
|
+
const env = envelope;
|
|
91
|
+
if (env.signature && typeof env.signature === "object") {
|
|
92
|
+
const payload = env.payload || {};
|
|
93
|
+
const sig = env.signature;
|
|
94
|
+
return {
|
|
95
|
+
kid: typeof sig.kid === "string" ? sig.kid : null,
|
|
96
|
+
issuer: typeof payload.issuer_id === "string" ? payload.issuer_id : typeof payload.issuer_name === "string" ? payload.issuer_name : null,
|
|
97
|
+
type: typeof payload.type === "string" ? payload.type : null
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
kid: typeof env.kid === "string" ? env.kid : null,
|
|
102
|
+
issuer: typeof env.issuer === "string" ? env.issuer : null,
|
|
103
|
+
type: typeof env.type === "string" ? env.type : null
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export {
|
|
108
|
+
canonicalize,
|
|
109
|
+
receiptHash,
|
|
110
|
+
computeSbIssuerKid,
|
|
111
|
+
createReceiptEnvelope,
|
|
112
|
+
verifyReceipt,
|
|
113
|
+
receiptIdentity
|
|
114
|
+
};
|