ccs-mcp-server 1.1.2 → 1.2.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/LICENSE ADDED
@@ -0,0 +1,82 @@
1
+ Elastic License v2
2
+
3
+ Copyright (c) 2026 Correctover. All rights reserved.
4
+
5
+ ## Elastic License
6
+
7
+ ### Acceptance
8
+
9
+ By using the software, you agree to all of the terms and conditions below.
10
+
11
+ ### Copyright License
12
+
13
+ The licensor grants you a free of charge, non-exclusive, worldwide,
14
+ non-sublicensable, non-transferable, royalty-free license under its
15
+ copyright and database rights to: use, copy, distribute, make
16
+ available, and prepare derivative works of, the software; and
17
+ permit persons to whom the software is furnished to do the same;
18
+ in each case to provide services to your own internal users and to
19
+ third parties; all provided, however, that you and any such persons
20
+ do not (and have no right to):
21
+ 1. provide the software as a managed service to third parties;
22
+ or
23
+ 2. circumvent the capacity or functionality limits of any
24
+ commercial/enterprise features in the software, or unlock any
25
+ functionality locked by a license key; or
26
+ 3. remove or hide any software source code, functionality,
27
+ license terms, or other technical or legal restrictions
28
+ (including any copyright, trademark, or other proprietary
29
+ rights notices).
30
+
31
+ If use of the software is under a trial license or for evaluation
32
+ purposes, you may use the software only for evaluation and
33
+ non-production uses during the trial period.
34
+
35
+ ### Patent License
36
+
37
+ The licensor grants you a free of charge, non-exclusive,
38
+ non-sublicensable, non-transferable, royalty-free license under
39
+ its patent claims that are embodied in the software, to make,
40
+ have made, use, import, and otherwise transfer the software,
41
+ provided that you do not (and have no right to) provide the
42
+ software as a managed service to third parties. This license
43
+ terminates automatically when you commence patent action
44
+ concerning the software.
45
+
46
+ ### Trademark License
47
+
48
+ No permission is granted to use the trade names, trademarks,
49
+ service marks, or product names of the licensor or its contributors
50
+ except as required for reasonable and customary use in describing
51
+ and as necessary to comply with the notice and attribution
52
+ requirements above.
53
+
54
+ ### No Other Rights
55
+
56
+ The terms and conditions of this license do not grant any other
57
+ permission to the software or any other intellectual property.
58
+
59
+ ### No Warranty
60
+
61
+ The software is provided "as is" without warranty, to the maximum
62
+ extent permitted by law. The licensor disclaims and has no liability
63
+ of any kind for any and all warranties, conditions, and other terms
64
+ that would otherwise exist, including: merchantability, satisfactory
65
+ quality, fitness for a particular purpose, title, non-infringement,
66
+ or any warranties that the software is error-free or that any defects
67
+ will be corrected. The licensor disclaims and has no liability for
68
+ loss or damage to any data or information stored or processed
69
+ through the software. Nothing in this license applies to any
70
+ software that is not the software.
71
+
72
+ ### Limitation of Liability
73
+
74
+ To the maximum extent permitted by law, in no event shall the
75
+ licensor be liable for any direct, indirect, incidental, special,
76
+ punitive, or consequential damages, including damages for loss of
77
+ profits, goodwill, use, data, or other intangible losses, whether
78
+ based on warranty, contract, tort, or any other legal theory,
79
+ regardless of whether the licensor has been advised of the
80
+ possibility of such damages. If any court refuses to grant the
81
+ foregoing limitations, then liability shall be limited to the
82
+ greatest extent permitted by law.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ccs-mcp-server",
3
- "version": "1.1.2",
4
- "description": "CCS Runtime Evidence MCP Server — verify AI agent tool calls, issue tamper-evident evidence, audit MCP configs, and bind execution to declared intent (cross-model amount drift protection). For Claude Desktop, Cursor, Windsurf, and any MCP client.",
3
+ "version": "1.2.1",
4
+ "description": "CCS Runtime Evidence MCP Server — verify AI agent tool calls, issue Ed25519-signed evidence receipts, audit MCP configs, and bind execution to declared intent (cross-model amount drift protection). For Claude Desktop, Cursor, Windsurf, and any MCP client.",
5
5
  "type": "commonjs",
6
6
  "bin": {
7
7
  "ccs-mcp-server": "src/index.js"
@@ -23,6 +23,7 @@
23
23
  "correctover",
24
24
  "audit",
25
25
  "evidence",
26
+ "ed25519",
26
27
  "supply-chain-security",
27
28
  "agent-security",
28
29
  "security",
@@ -38,9 +39,12 @@
38
39
  "node": ">=18"
39
40
  },
40
41
  "dependencies": {},
42
+ "scripts": {
43
+ "test": "node test/run-tests.js"
44
+ },
41
45
  "mcp": {
42
46
  "name": "ccs-runtime-evidence",
43
47
  "displayName": "CCS Runtime Evidence",
44
- "description": "Verify AI agent tool calls and issue tamper-evident CCS evidence records. Blocks command injection, path traversal, SSRF, SQL injection; validates caller identity, schema, cost/latency budgets; produces cryptographically bound evidence."
48
+ "description": "Verify AI agent tool calls and issue Ed25519-signed CCS evidence receipts. Blocks command injection, path traversal, SSRF, SQL injection; validates caller identity, schema, cost/latency budgets; produces cryptographically signed and independently verifiable evidence."
45
49
  }
46
50
  }
package/src/index.js CHANGED
@@ -26,12 +26,13 @@
26
26
 
27
27
  const crypto = require("crypto");
28
28
  const readline = require("readline");
29
+ const receipts = require("./receipts");
29
30
 
30
31
  // ---------------------------------------------------------------------------
31
32
  // CCS Verifier Core (pure stdlib Node.js)
32
33
  // ---------------------------------------------------------------------------
33
34
 
34
- const CCS_VERSION = "1.1.2";
35
+ const CCS_VERSION = "1.2.1";
35
36
 
36
37
  const DEFAULT_POLICY = {
37
38
  mode: "block",
@@ -283,7 +284,12 @@ function verifyCall(call, policyOverride) {
283
284
  // Finalize evidence_hash
284
285
  const hashBody = { ...evidence };
285
286
  evidence.evidence_hash = sha256(canonical(hashBody));
286
- return evidence;
287
+
288
+ // Sign the full evidence record with Ed25519. This makes the receipt
289
+ // independently verifiable offline: any party holding the signer's
290
+ // public key can verify that this evidence record was produced by the
291
+ // holder of the corresponding private key and has not been tampered with.
292
+ return receipts.signReceipt(evidence);
287
293
  }
288
294
 
289
295
  function auditMcpConfig(config) {
@@ -340,15 +346,24 @@ function verifyIntentBinding(intent, toolName, toolArgs, now = Date.now()) {
340
346
  const intentHash = "sha256:" + crypto.createHash("sha256").update(canonical(intent)).digest("hex");
341
347
  const argsHash = "sha256:" + crypto.createHash("sha256").update(canonical(toolArgs)).digest("hex");
342
348
 
349
+ let body;
350
+
343
351
  // 1. TTL check
344
352
  if (intent.ttl_ms != null && now - intent.issued_at > intent.ttl_ms) {
345
- return {
353
+ body = {
354
+ evidence_type: "ccs.intent_binding",
355
+ evidence_version: CCS_VERSION,
356
+ evidence_id: crypto.randomUUID(),
346
357
  allowed: false,
347
358
  reason: { type: "intent_expired", expected: intent.issued_at + intent.ttl_ms, actual: now },
359
+ intent_id: intent.intent_id,
360
+ intent_type: intent.intent_type,
348
361
  intent_hash: intentHash,
349
362
  args_hash: argsHash,
350
363
  tool: toolName,
364
+ issued_at: new Date().toISOString(),
351
365
  };
366
+ return receipts.signReceipt(body);
352
367
  }
353
368
 
354
369
  // 2. Field-by-field comparison
@@ -370,26 +385,40 @@ function verifyIntentBinding(intent, toolName, toolArgs, now = Date.now()) {
370
385
  }
371
386
  }
372
387
  if (!match) {
373
- return {
388
+ body = {
389
+ evidence_type: "ccs.intent_binding",
390
+ evidence_version: CCS_VERSION,
391
+ evidence_id: crypto.randomUUID(),
374
392
  allowed: false,
375
393
  reason: {
376
394
  type: "intent_arg_mismatch",
377
395
  field, binding_mode: mode,
378
396
  expected, actual,
379
397
  },
398
+ intent_id: intent.intent_id,
399
+ intent_type: intent.intent_type,
380
400
  intent_hash: intentHash,
381
401
  args_hash: argsHash,
382
402
  tool: toolName,
403
+ issued_at: new Date().toISOString(),
383
404
  };
405
+ return receipts.signReceipt(body);
384
406
  }
385
407
  }
386
408
 
387
- return {
409
+ body = {
410
+ evidence_type: "ccs.intent_binding",
411
+ evidence_version: CCS_VERSION,
412
+ evidence_id: crypto.randomUUID(),
388
413
  allowed: true,
414
+ intent_id: intent.intent_id,
415
+ intent_type: intent.intent_type,
389
416
  intent_hash: intentHash,
390
417
  args_hash: argsHash,
391
418
  tool: toolName,
419
+ issued_at: new Date().toISOString(),
392
420
  };
421
+ return receipts.signReceipt(body);
393
422
  }
394
423
 
395
424
  // ---------------------------------------------------------------------------
@@ -480,6 +509,25 @@ const TOOLS = [
480
509
  required: ["intent", "tool", "arguments"],
481
510
  },
482
511
  },
512
+ {
513
+ name: "verify_receipt",
514
+ description: "Offline-verify a CCS Ed25519-signed receipt. Auditors call this with a receipt JSON produced by issue_evidence or verify_intent_binding; it checks the signature against the embedded signer_public_key and reports whether the body was tampered with. Optionally pin an expected signer public key (PEM string or sha256: fingerprint) to reject receipts from untrusted signers.",
515
+ inputSchema: {
516
+ type: "object",
517
+ properties: {
518
+ receipt: {
519
+ type: "object",
520
+ description: "The signed receipt object (must include signer_public_key, signature_alg, signature, and the signed body fields).",
521
+ additionalProperties: true,
522
+ },
523
+ expected_signer: {
524
+ type: "string",
525
+ description: "Optional. Either the expected PEM public key, or a sha256:<hex> fingerprint of the DER SPKI public key.",
526
+ },
527
+ },
528
+ required: ["receipt"],
529
+ },
530
+ },
483
531
  ];
484
532
 
485
533
  function handleRequest(req) {
@@ -522,19 +570,16 @@ function handleRequest(req) {
522
570
  } else if (name === "verify_intent_binding") {
523
571
  const verdict = verifyIntentBinding(args.intent, args.tool, args.arguments || {});
524
572
  result = {
525
- content: [{ type: "text", text: JSON.stringify({
526
- allowed: verdict.allowed,
527
- intent_id: args.intent.intent_id,
528
- intent_hash: verdict.intent_hash,
529
- args_hash: verdict.args_hash,
530
- tool: args.tool,
531
- ...(verdict.reason ? { reason: verdict.reason } : {}),
532
- blocked: !verdict.allowed,
533
- message: verdict.allowed
534
- ? "Intent binding verified — actual arguments match declared intent"
535
- : `Intent binding DENIED: ${verdict.reason?.type}`,
536
- }, null, 2) }],
537
- isError: !verdict.allowed,
573
+ content: [{ type: "text", text: JSON.stringify(verdict, null, 2) }],
574
+ isError: verdict.allowed === false,
575
+ };
576
+ } else if (name === "verify_receipt") {
577
+ const v = args.expected_signer
578
+ ? receipts.verifyReceiptWithKey(args.receipt, args.expected_signer)
579
+ : receipts.verifyReceipt(args.receipt);
580
+ result = {
581
+ content: [{ type: "text", text: JSON.stringify(v, null, 2) }],
582
+ isError: v.valid === false,
538
583
  };
539
584
  } else {
540
585
  result = { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
@@ -550,7 +595,13 @@ function handleRequest(req) {
550
595
  }
551
596
 
552
597
  function main() {
553
- process.stderr.write(`[ccs-mcp-server] v${CCS_VERSION} starting on stdio\n`);
598
+ const kp = receipts.getKeypair();
599
+ const fp = receipts.publicKeyFingerprint();
600
+ process.stderr.write(
601
+ `[ccs-mcp-server] v${CCS_VERSION} starting on stdio\n` +
602
+ `[ccs-mcp-server] signing key source: ${kp.source}\n` +
603
+ `[ccs-mcp-server] signer fingerprint: ${fp}\n`
604
+ );
554
605
  const rl = readline.createInterface({ input: process.stdin });
555
606
  rl.on("line", (line) => {
556
607
  if (!line.trim()) return;
@@ -563,4 +614,4 @@ function main() {
563
614
  }
564
615
 
565
616
  if (require.main === module) main();
566
- module.exports = { verifyCall, auditMcpConfig };
617
+ module.exports = { verifyCall, auditMcpConfig, verifyIntentBinding, receipts };
@@ -0,0 +1,219 @@
1
+ /**
2
+ * CCS Ed25519 receipt signing and verification (pure Node.js stdlib).
3
+ *
4
+ * Key sourcing order:
5
+ * 1. CCS_PRIVATE_KEY / CCS_PUBLIC_KEY env vars (PEM strings)
6
+ * 2. CCS_KEY_DIR / ~/.ccs/ed25519-private.pem + ed25519-public.pem
7
+ * 3. If none found, generate a new keypair and persist it to the key dir.
8
+ *
9
+ * Receipts include:
10
+ * - all original evidence fields (verdict, dimensions, hashes, ...)
11
+ * - signer_public_key (SPKI PEM, multiline preserved)
12
+ * - signature_alg "Ed25519"
13
+ * - signature base64(crypto.sign over canonical receipt_body)
14
+ *
15
+ * The signed body excludes signer_public_key/signature_alg/signature/verified.
16
+ * Verification uses only the signer_public_key embedded in the receipt itself;
17
+ * auditors can additionally pin a known pubkey out-of-band.
18
+ */
19
+
20
+ "use strict";
21
+
22
+ const crypto = require("crypto");
23
+ const fs = require("fs");
24
+ const os = require("os");
25
+ const path = require("path");
26
+
27
+ const ALG = "Ed25519";
28
+ const ALG_INTERNAL = "ed25519";
29
+ const SIGNATURE_FIELDS = ["signer_public_key", "signature_alg", "signature", "verified"];
30
+
31
+ function canonical(obj) {
32
+ return JSON.stringify(obj, Object.keys(obj).sort());
33
+ }
34
+
35
+ function defaultKeyDir() {
36
+ if (process.env.CCS_KEY_DIR) return process.env.CCS_KEY_DIR;
37
+ return path.join(os.homedir(), ".ccs");
38
+ }
39
+
40
+ function ensureKeypair() {
41
+ // 1. Env vars
42
+ if (process.env.CCS_PRIVATE_KEY && process.env.CCS_PUBLIC_KEY) {
43
+ return {
44
+ privateKey: crypto.createPrivateKey(
45
+ process.env.CCS_PRIVATE_KEY.includes("-----BEGIN")
46
+ ? process.env.CCS_PRIVATE_KEY
47
+ : Buffer.from(process.env.CCS_PRIVATE_KEY, "base64").toString("utf8")
48
+ ),
49
+ publicKey: crypto.createPublicKey(
50
+ process.env.CCS_PUBLIC_KEY.includes("-----BEGIN")
51
+ ? process.env.CCS_PUBLIC_KEY
52
+ : Buffer.from(process.env.CCS_PUBLIC_KEY, "base64").toString("utf8")
53
+ ),
54
+ source: "env",
55
+ };
56
+ }
57
+
58
+ // 2. / 3. Disk
59
+ const dir = defaultKeyDir();
60
+ const privPath = path.join(dir, "ed25519-private.pem");
61
+ const pubPath = path.join(dir, "ed25519-public.pem");
62
+
63
+ if (fs.existsSync(privPath) && fs.existsSync(pubPath)) {
64
+ return {
65
+ privateKey: crypto.createPrivateKey(fs.readFileSync(privPath, "utf8")),
66
+ publicKey: crypto.createPublicKey(fs.readFileSync(pubPath, "utf8")),
67
+ source: "file",
68
+ };
69
+ }
70
+
71
+ // 3. Generate
72
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
73
+ const { privateKey, publicKey } = crypto.generateKeyPairSync(ALG_INTERNAL, {
74
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
75
+ publicKeyEncoding: { type: "spki", format: "pem" },
76
+ });
77
+ // Atomic-ish writes with restrictive perms
78
+ fs.writeFileSync(privPath, privateKey, { mode: 0o600 });
79
+ fs.writeFileSync(pubPath, publicKey, { mode: 0o644 });
80
+ return {
81
+ privateKey: crypto.createPrivateKey(privateKey),
82
+ publicKey: crypto.createPublicKey(publicKey),
83
+ source: "generated",
84
+ };
85
+ }
86
+
87
+ let _keypair = null;
88
+ function getKeypair() {
89
+ if (!_keypair) _keypair = ensureKeypair();
90
+ return _keypair;
91
+ }
92
+
93
+ function publicKeyPem() {
94
+ const { publicKey } = getKeypair();
95
+ return publicKey.export({ type: "spki", format: "pem" });
96
+ }
97
+
98
+ /**
99
+ * Sign a receipt body. Returns a new object containing all original fields
100
+ * plus signer_public_key / signature_alg / signature. The original object
101
+ * is not mutated.
102
+ */
103
+ function signReceipt(body) {
104
+ const { privateKey, publicKey } = getKeypair();
105
+
106
+ // Strip any existing signature fields so re-signing is idempotent.
107
+ const cleaned = { ...body };
108
+ for (const k of SIGNATURE_FIELDS) delete cleaned[k];
109
+
110
+ const pubPem = publicKey.export({ type: "spki", format: "pem" });
111
+ const payload = canonical(cleaned);
112
+ const sigBuf = crypto.sign(null, Buffer.from(payload, "utf8"), privateKey);
113
+
114
+ return {
115
+ ...cleaned,
116
+ signer_public_key: pubPem,
117
+ signature_alg: ALG,
118
+ signature: sigBuf.toString("base64"),
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Verify a signed receipt. Returns { valid, reason?, signer_public_key }.
124
+ * Does NOT require a trusted key — it verifies that the signature matches
125
+ * the body under the embedded signer_public_key. Callers who need to pin
126
+ * a known pubkey should compare signer_public_key themselves after this
127
+ * returns valid=true.
128
+ */
129
+ function verifyReceipt(receipt) {
130
+ if (!receipt || typeof receipt !== "object") {
131
+ return { valid: false, reason: "receipt is not an object" };
132
+ }
133
+ const sig = receipt.signature;
134
+ const alg = receipt.signature_alg;
135
+ const pubPem = receipt.signer_public_key;
136
+ if (!sig || !pubPem || !alg) {
137
+ return { valid: false, reason: "missing signature/signature_alg/signer_public_key" };
138
+ }
139
+ if (alg !== ALG) {
140
+ return { valid: false, reason: `unsupported signature_alg: ${alg}` };
141
+ }
142
+
143
+ const cleaned = { ...receipt };
144
+ for (const k of SIGNATURE_FIELDS) delete cleaned[k];
145
+
146
+ let key;
147
+ try {
148
+ key = crypto.createPublicKey(pubPem);
149
+ } catch (e) {
150
+ return { valid: false, reason: `invalid signer_public_key: ${e.message}` };
151
+ }
152
+ if (key.asymmetricKeyType !== ALG_INTERNAL) {
153
+ return { valid: false, reason: `signer_public_key is not ${ALG}` };
154
+ }
155
+
156
+ const payload = canonical(cleaned);
157
+ let sigBuf;
158
+ try {
159
+ sigBuf = Buffer.from(sig, "base64");
160
+ } catch (e) {
161
+ return { valid: false, reason: "signature is not valid base64" };
162
+ }
163
+
164
+ const ok = crypto.verify(null, Buffer.from(payload, "utf8"), key, sigBuf);
165
+ if (!ok) {
166
+ return {
167
+ valid: false,
168
+ reason: "signature verification failed (body was tampered or signed by a different key)",
169
+ signer_public_key: pubPem,
170
+ };
171
+ }
172
+ return { valid: true, signer_public_key: pubPem };
173
+ }
174
+
175
+ /**
176
+ * Helper: verify a receipt AND pin it to an expected public key fingerprint.
177
+ * expectedPem can be a PEM string, or the string "sha256:<hex>" fingerprint.
178
+ */
179
+ function verifyReceiptWithKey(receipt, expectedPemOrFp) {
180
+ const v = verifyReceipt(receipt);
181
+ if (!v.valid) return v;
182
+
183
+ if (!expectedPemOrFp) return v;
184
+
185
+ if (expectedPemOrFp.startsWith("sha256:")) {
186
+ // Fingerprint: sha256 of DER-encoded SPKI public key
187
+ const der = crypto.createPublicKey(v.signer_public_key).export({ type: "spki", format: "der" });
188
+ const fp = "sha256:" + crypto.createHash("sha256").update(der).digest("hex");
189
+ if (fp !== expectedPemOrFp) {
190
+ return { valid: false, reason: `signer fingerprint mismatch: got ${fp}, expected ${expectedPemOrFp}` };
191
+ }
192
+ return { valid: true, signer_public_key: v.signer_public_key, signer_fingerprint: fp };
193
+ }
194
+
195
+ // PEM string compare (normalize whitespace)
196
+ const normA = v.signer_public_key.replace(/\s+/g, "");
197
+ const normB = String(expectedPemOrFp).replace(/\s+/g, "");
198
+ if (normA !== normB) {
199
+ return { valid: false, reason: "signer_public_key does not match the pinned key" };
200
+ }
201
+ return { valid: true, signer_public_key: v.signer_public_key };
202
+ }
203
+
204
+ function publicKeyFingerprint() {
205
+ const { publicKey } = getKeypair();
206
+ const der = publicKey.export({ type: "spki", format: "der" });
207
+ return "sha256:" + crypto.createHash("sha256").update(der).digest("hex");
208
+ }
209
+
210
+ module.exports = {
211
+ ALG,
212
+ canonical,
213
+ getKeypair,
214
+ publicKeyPem,
215
+ publicKeyFingerprint,
216
+ signReceipt,
217
+ verifyReceipt,
218
+ verifyReceiptWithKey,
219
+ };