@kya-os/verifier 1.3.4-canary.9 → 1.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/package.json +2 -2
- package/dist/core.d.ts.map +0 -1
- package/dist/core.js +0 -736
- package/dist/core.js.map +0 -1
- package/dist/express.d.ts.map +0 -1
- package/dist/express.js +0 -204
- package/dist/express.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -103
- package/dist/index.js.map +0 -1
- package/dist/worker.d.ts +0 -160
- package/dist/worker.d.ts.map +0 -1
- package/dist/worker.js +0 -302
- package/dist/worker.js.map +0 -1
package/dist/core.js
DELETED
|
@@ -1,736 +0,0 @@
|
|
|
1
|
-
import { importJWK, jwtVerify } from "jose";
|
|
2
|
-
import { canonicalize } from "json-canonicalize";
|
|
3
|
-
import { AGENT_HEADERS, ERROR_HTTP_STATUS, VERIFIER_ERROR_CODES } from "@kya-os/contracts/verifier";
|
|
4
|
-
/**
|
|
5
|
-
* Isomorphic verifier core for XMCP-I proof validation
|
|
6
|
-
*
|
|
7
|
-
* This is the heart of the trust system - it verifies that AI agents
|
|
8
|
-
* are who they claim to be and have the authority to perform actions.
|
|
9
|
-
*/
|
|
10
|
-
export class VerifierCore {
|
|
11
|
-
constructor(config = {}) {
|
|
12
|
-
this.didCache = new Map();
|
|
13
|
-
this.delegationCache = new Map();
|
|
14
|
-
this.config = {
|
|
15
|
-
ktaBaseUrl: config.ktaBaseUrl || "https://knowthat.ai",
|
|
16
|
-
enableDelegationCheck: config.enableDelegationCheck ?? true,
|
|
17
|
-
clockSkewTolerance: config.clockSkewTolerance ?? 120,
|
|
18
|
-
sessionTimeout: config.sessionTimeout ?? 1800, // 30 minutes
|
|
19
|
-
proofMaxAge: config.proofMaxAge ?? 300, // 5 minutes
|
|
20
|
-
allowMockData: config.allowMockData ?? false,
|
|
21
|
-
didCacheTtl: config.didCacheTtl ?? 300, // 5 minutes
|
|
22
|
-
delegationCacheTtl: config.delegationCacheTtl ?? 60, // 1 minute
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Verify a detached proof and return verification result
|
|
27
|
-
*
|
|
28
|
-
* This is the main entry point for proof verification. It performs
|
|
29
|
-
* a comprehensive validation of the agent's identity and authorization.
|
|
30
|
-
*/
|
|
31
|
-
async verify(context) {
|
|
32
|
-
const startTime = Date.now();
|
|
33
|
-
try {
|
|
34
|
-
// 1. Validate proof structure
|
|
35
|
-
const structureError = this.validateProofStructure(context.proof);
|
|
36
|
-
if (structureError) {
|
|
37
|
-
this.logVerificationAttempt(context, false, structureError.code);
|
|
38
|
-
return this.createErrorResult(structureError);
|
|
39
|
-
}
|
|
40
|
-
// 2. Verify timestamp and session validity
|
|
41
|
-
const timestampError = this.validateTimestamp(context.proof.meta, context.timestamp);
|
|
42
|
-
if (timestampError) {
|
|
43
|
-
this.logVerificationAttempt(context, false, timestampError.code);
|
|
44
|
-
return this.createErrorResult(timestampError);
|
|
45
|
-
}
|
|
46
|
-
// 3. Verify audience matches
|
|
47
|
-
const audienceError = this.validateAudience(context.proof.meta, context.audience);
|
|
48
|
-
if (audienceError) {
|
|
49
|
-
this.logVerificationAttempt(context, false, audienceError.code);
|
|
50
|
-
return this.createErrorResult(audienceError);
|
|
51
|
-
}
|
|
52
|
-
// 4. Verify Ed25519 signature
|
|
53
|
-
const signatureError = await this.verifySignature(context.proof);
|
|
54
|
-
if (signatureError) {
|
|
55
|
-
this.logVerificationAttempt(context, false, signatureError.code);
|
|
56
|
-
return this.createErrorResult(signatureError);
|
|
57
|
-
}
|
|
58
|
-
// 5. Check delegation if enabled and present
|
|
59
|
-
if (this.config.enableDelegationCheck &&
|
|
60
|
-
context.proof.meta.delegationRef) {
|
|
61
|
-
const delegationError = await this.verifyDelegation(context.proof.meta.delegationRef, context.proof.meta.did, context.proof.meta.kid);
|
|
62
|
-
if (delegationError) {
|
|
63
|
-
this.logVerificationAttempt(context, false, delegationError.code);
|
|
64
|
-
return this.createErrorResult(delegationError);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
// 6. Generate trusted headers and context
|
|
68
|
-
const headers = this.generateHeaders(context.proof.meta);
|
|
69
|
-
const agentContext = this.generateAgentContext(context.proof.meta);
|
|
70
|
-
const duration = Date.now() - startTime;
|
|
71
|
-
this.logVerificationAttempt(context, true, "SUCCESS", duration);
|
|
72
|
-
return {
|
|
73
|
-
success: true,
|
|
74
|
-
headers,
|
|
75
|
-
agentContext,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
catch (error) {
|
|
79
|
-
const duration = Date.now() - startTime;
|
|
80
|
-
this.logVerificationAttempt(context, false, "UNEXPECTED_ERROR", duration);
|
|
81
|
-
return this.createErrorResult({
|
|
82
|
-
code: "XMCP_I_EVERIFY",
|
|
83
|
-
message: error instanceof Error ? error.message : "Verification failed",
|
|
84
|
-
httpStatus: 500,
|
|
85
|
-
details: {
|
|
86
|
-
reason: "Unexpected error during verification",
|
|
87
|
-
remediation: "Check proof format and try again",
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Validate proof structure with comprehensive checks
|
|
94
|
-
*/
|
|
95
|
-
validateProofStructure(proof) {
|
|
96
|
-
if (!proof.jws || typeof proof.jws !== "string") {
|
|
97
|
-
return {
|
|
98
|
-
code: "XMCP_I_EBADPROOF",
|
|
99
|
-
message: "Invalid proof: missing or invalid JWS",
|
|
100
|
-
httpStatus: 400,
|
|
101
|
-
details: {
|
|
102
|
-
reason: "JWS field is required and must be a string",
|
|
103
|
-
expected: "string",
|
|
104
|
-
received: typeof proof.jws,
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
if (!proof.meta || typeof proof.meta !== "object") {
|
|
109
|
-
return {
|
|
110
|
-
code: "XMCP_I_EBADPROOF",
|
|
111
|
-
message: "Invalid proof: missing or invalid meta",
|
|
112
|
-
httpStatus: 400,
|
|
113
|
-
details: {
|
|
114
|
-
reason: "Meta field is required and must be an object",
|
|
115
|
-
expected: "object",
|
|
116
|
-
received: typeof proof.meta,
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
const requiredFields = [
|
|
121
|
-
"did",
|
|
122
|
-
"kid",
|
|
123
|
-
"ts",
|
|
124
|
-
"nonce",
|
|
125
|
-
"audience",
|
|
126
|
-
"sessionId",
|
|
127
|
-
"requestHash",
|
|
128
|
-
"responseHash",
|
|
129
|
-
];
|
|
130
|
-
for (const field of requiredFields) {
|
|
131
|
-
if (!proof.meta[field]) {
|
|
132
|
-
return {
|
|
133
|
-
code: "XMCP_I_EBADPROOF",
|
|
134
|
-
message: `Invalid proof: missing required field '${field}'`,
|
|
135
|
-
httpStatus: 400,
|
|
136
|
-
details: {
|
|
137
|
-
reason: `Field '${field}' is required in proof meta`,
|
|
138
|
-
expected: "non-empty value",
|
|
139
|
-
received: proof.meta[field],
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
// Validate hash format
|
|
145
|
-
const hashRegex = /^sha256:[a-f0-9]{64}$/;
|
|
146
|
-
if (!hashRegex.test(proof.meta.requestHash)) {
|
|
147
|
-
return {
|
|
148
|
-
code: "XMCP_I_EBADPROOF",
|
|
149
|
-
message: "Invalid proof: malformed requestHash",
|
|
150
|
-
httpStatus: 400,
|
|
151
|
-
details: {
|
|
152
|
-
reason: "requestHash must be in format 'sha256:<64-char-hex>'",
|
|
153
|
-
expected: "sha256:[a-f0-9]{64}",
|
|
154
|
-
received: proof.meta.requestHash,
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
if (!hashRegex.test(proof.meta.responseHash)) {
|
|
159
|
-
return {
|
|
160
|
-
code: "XMCP_I_EBADPROOF",
|
|
161
|
-
message: "Invalid proof: malformed responseHash",
|
|
162
|
-
httpStatus: 400,
|
|
163
|
-
details: {
|
|
164
|
-
reason: "responseHash must be in format 'sha256:<64-char-hex>'",
|
|
165
|
-
expected: "sha256:[a-f0-9]{64}",
|
|
166
|
-
received: proof.meta.responseHash,
|
|
167
|
-
},
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
// Validate DID format
|
|
171
|
-
if (!proof.meta.did.startsWith("did:")) {
|
|
172
|
-
return {
|
|
173
|
-
code: "XMCP_I_EBADPROOF",
|
|
174
|
-
message: "Invalid proof: malformed DID",
|
|
175
|
-
httpStatus: 400,
|
|
176
|
-
details: {
|
|
177
|
-
reason: "DID must start with 'did:'",
|
|
178
|
-
expected: "did:*",
|
|
179
|
-
received: proof.meta.did,
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Validate timestamp with configurable clock skew tolerance
|
|
187
|
-
*/
|
|
188
|
-
validateTimestamp(meta, currentTimestamp) {
|
|
189
|
-
// Validate meta.ts is not NaN (prevents client-controlled NaN attacks)
|
|
190
|
-
if (!Number.isFinite(meta.ts)) {
|
|
191
|
-
return {
|
|
192
|
-
code: VERIFIER_ERROR_CODES.PROOF_INVALID_TS,
|
|
193
|
-
message: "Invalid proof: timestamp is not a valid number",
|
|
194
|
-
httpStatus: ERROR_HTTP_STATUS[VERIFIER_ERROR_CODES.PROOF_INVALID_TS],
|
|
195
|
-
details: {
|
|
196
|
-
reason: "Proof timestamp must be a finite number",
|
|
197
|
-
received: meta.ts,
|
|
198
|
-
remediation: "Ensure proof timestamp is properly formatted",
|
|
199
|
-
},
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
const now = currentTimestamp || Math.floor(Date.now() / 1000);
|
|
203
|
-
// Validate server timestamp is valid (defense in depth)
|
|
204
|
-
if (!Number.isFinite(now)) {
|
|
205
|
-
return {
|
|
206
|
-
code: VERIFIER_ERROR_CODES.SERVER_TIME_INVALID,
|
|
207
|
-
message: "Server timestamp validation failed",
|
|
208
|
-
httpStatus: ERROR_HTTP_STATUS[VERIFIER_ERROR_CODES.SERVER_TIME_INVALID],
|
|
209
|
-
details: {
|
|
210
|
-
reason: "Server time is not valid",
|
|
211
|
-
remediation: "Contact system administrator",
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
const skew = this.config.clockSkewTolerance;
|
|
216
|
-
// Check for future timestamps
|
|
217
|
-
if (meta.ts > now + skew) {
|
|
218
|
-
return {
|
|
219
|
-
code: VERIFIER_ERROR_CODES.PROOF_FUTURE_TS,
|
|
220
|
-
message: "Invalid proof: timestamp is in the future",
|
|
221
|
-
httpStatus: ERROR_HTTP_STATUS[VERIFIER_ERROR_CODES.PROOF_FUTURE_TS],
|
|
222
|
-
details: {
|
|
223
|
-
reason: `Timestamp ${meta.ts} is ${meta.ts - now}s in the future (skew: ${skew}s)`,
|
|
224
|
-
expected: `≤ ${now + skew}`,
|
|
225
|
-
received: meta.ts,
|
|
226
|
-
remediation: "Check client clock sync",
|
|
227
|
-
},
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
// Check for past timestamps (clock skew exceeded)
|
|
231
|
-
if (meta.ts < now - skew) {
|
|
232
|
-
return {
|
|
233
|
-
code: VERIFIER_ERROR_CODES.PROOF_SKEW_EXCEEDED,
|
|
234
|
-
message: "Invalid proof: timestamp outside acceptable range",
|
|
235
|
-
httpStatus: ERROR_HTTP_STATUS[VERIFIER_ERROR_CODES.PROOF_SKEW_EXCEEDED],
|
|
236
|
-
details: {
|
|
237
|
-
reason: `Timestamp ${meta.ts} is ${now - meta.ts}s in the past (skew: ${skew}s)`,
|
|
238
|
-
expected: `≥ ${now - skew}`,
|
|
239
|
-
received: meta.ts,
|
|
240
|
-
remediation: "Check NTP sync; adjust XMCP_I_TS_SKEW_SEC if needed",
|
|
241
|
-
},
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
// Check proof age if configured
|
|
245
|
-
if (this.config.proofMaxAge) {
|
|
246
|
-
const age = now - meta.ts;
|
|
247
|
-
if (age > this.config.proofMaxAge) {
|
|
248
|
-
return {
|
|
249
|
-
code: VERIFIER_ERROR_CODES.PROOF_TOO_OLD,
|
|
250
|
-
message: "Invalid proof: proof is too old",
|
|
251
|
-
httpStatus: ERROR_HTTP_STATUS[VERIFIER_ERROR_CODES.PROOF_TOO_OLD],
|
|
252
|
-
details: {
|
|
253
|
-
reason: `Proof age ${age}s exceeds maximum ${this.config.proofMaxAge}s`,
|
|
254
|
-
expected: `Less than ${this.config.proofMaxAge}s old`,
|
|
255
|
-
received: `${age}s old`,
|
|
256
|
-
remediation: "Generate a fresh proof",
|
|
257
|
-
},
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return null;
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Validate audience matches expected value
|
|
265
|
-
*/
|
|
266
|
-
validateAudience(meta, expectedAudience) {
|
|
267
|
-
if (meta.audience !== expectedAudience) {
|
|
268
|
-
return {
|
|
269
|
-
code: "XMCP_I_EHANDSHAKE",
|
|
270
|
-
message: "Invalid proof: audience mismatch",
|
|
271
|
-
httpStatus: 401,
|
|
272
|
-
details: {
|
|
273
|
-
reason: "Proof audience does not match request audience",
|
|
274
|
-
expected: expectedAudience,
|
|
275
|
-
received: meta.audience,
|
|
276
|
-
},
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Verify Ed25519 signature using JOSE with proper detached JWS handling
|
|
283
|
-
*
|
|
284
|
-
* This is the cryptographic heart of the verification process.
|
|
285
|
-
* It ensures the proof was signed by the claimed identity.
|
|
286
|
-
*/
|
|
287
|
-
async verifySignature(proof) {
|
|
288
|
-
try {
|
|
289
|
-
// For testing with mock data, skip actual signature verification
|
|
290
|
-
if (this.config.allowMockData && proof.meta.did.startsWith("did:test:")) {
|
|
291
|
-
// Parse JWS components for basic validation
|
|
292
|
-
const jwsParts = proof.jws.split(".");
|
|
293
|
-
if (jwsParts.length !== 3 || jwsParts[1] !== "") {
|
|
294
|
-
return {
|
|
295
|
-
code: "XMCP_I_EBADPROOF",
|
|
296
|
-
message: "Invalid detached JWS format",
|
|
297
|
-
httpStatus: 403,
|
|
298
|
-
details: {
|
|
299
|
-
reason: "Detached JWS must have format 'header..signature'",
|
|
300
|
-
expected: "header..signature",
|
|
301
|
-
received: `${jwsParts.length} parts, payload: '${jwsParts[1]}'`,
|
|
302
|
-
},
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
const [headerB64] = jwsParts;
|
|
306
|
-
const header = JSON.parse(Buffer.from(headerB64, "base64url").toString());
|
|
307
|
-
if (header.alg !== "EdDSA") {
|
|
308
|
-
return {
|
|
309
|
-
code: "XMCP_I_EBADPROOF",
|
|
310
|
-
message: "Invalid JWS: unsupported algorithm",
|
|
311
|
-
httpStatus: 403,
|
|
312
|
-
details: {
|
|
313
|
-
reason: "Only EdDSA algorithm is supported",
|
|
314
|
-
expected: "EdDSA",
|
|
315
|
-
received: header.alg,
|
|
316
|
-
},
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
if (header.kid !== proof.meta.kid) {
|
|
320
|
-
return {
|
|
321
|
-
code: "XMCP_I_EBADPROOF",
|
|
322
|
-
message: "Invalid JWS: key ID mismatch",
|
|
323
|
-
httpStatus: 403,
|
|
324
|
-
details: {
|
|
325
|
-
reason: "JWS header kid must match proof meta kid",
|
|
326
|
-
expected: proof.meta.kid,
|
|
327
|
-
received: header.kid,
|
|
328
|
-
},
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
// Mock signature verification passes for test DIDs
|
|
332
|
-
return null;
|
|
333
|
-
}
|
|
334
|
-
// Parse JWS components - detached JWS has format: header..signature
|
|
335
|
-
const jwsParts = proof.jws.split(".");
|
|
336
|
-
if (jwsParts.length !== 3 || jwsParts[1] !== "") {
|
|
337
|
-
return {
|
|
338
|
-
code: "XMCP_I_EBADPROOF",
|
|
339
|
-
message: "Invalid detached JWS format",
|
|
340
|
-
httpStatus: 403,
|
|
341
|
-
details: {
|
|
342
|
-
reason: "Detached JWS must have format 'header..signature'",
|
|
343
|
-
expected: "header..signature",
|
|
344
|
-
received: `${jwsParts.length} parts, payload: '${jwsParts[1]}'`,
|
|
345
|
-
},
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
const [headerB64, , signatureB64] = jwsParts;
|
|
349
|
-
// Parse and validate header
|
|
350
|
-
const header = JSON.parse(Buffer.from(headerB64, "base64url").toString());
|
|
351
|
-
if (header.alg !== "EdDSA") {
|
|
352
|
-
return {
|
|
353
|
-
code: "XMCP_I_EBADPROOF",
|
|
354
|
-
message: "Invalid JWS: unsupported algorithm",
|
|
355
|
-
httpStatus: 403,
|
|
356
|
-
details: {
|
|
357
|
-
reason: "Only EdDSA algorithm is supported",
|
|
358
|
-
expected: "EdDSA",
|
|
359
|
-
received: header.alg,
|
|
360
|
-
},
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
if (header.kid !== proof.meta.kid) {
|
|
364
|
-
return {
|
|
365
|
-
code: "XMCP_I_EBADPROOF",
|
|
366
|
-
message: "Invalid JWS: key ID mismatch",
|
|
367
|
-
httpStatus: 403,
|
|
368
|
-
details: {
|
|
369
|
-
reason: "JWS header kid must match proof meta kid",
|
|
370
|
-
expected: proof.meta.kid,
|
|
371
|
-
received: header.kid,
|
|
372
|
-
},
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
// Fetch public key from DID document with caching
|
|
376
|
-
const publicKey = await this.fetchPublicKeyWithCache(proof.meta.did, proof.meta.kid);
|
|
377
|
-
if (!publicKey) {
|
|
378
|
-
return {
|
|
379
|
-
code: "XMCP_I_EBADPROOF",
|
|
380
|
-
message: "Unable to resolve public key",
|
|
381
|
-
httpStatus: 403,
|
|
382
|
-
details: {
|
|
383
|
-
reason: "Could not fetch public key from DID document",
|
|
384
|
-
remediation: "Ensure DID document is accessible and contains the key",
|
|
385
|
-
},
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
// Create canonical payload for detached signature verification
|
|
389
|
-
// This MUST match the exact format used by the runtime
|
|
390
|
-
const canonicalPayload = this.createCanonicalPayload(proof.meta);
|
|
391
|
-
const payloadB64 = Buffer.from(canonicalPayload, "utf8").toString("base64url");
|
|
392
|
-
// Reconstruct complete JWS for verification
|
|
393
|
-
const completeJWS = `${headerB64}.${payloadB64}.${signatureB64}`;
|
|
394
|
-
// Verify signature using JOSE
|
|
395
|
-
const jwk = await importJWK(publicKey);
|
|
396
|
-
await jwtVerify(completeJWS, jwk, {
|
|
397
|
-
algorithms: ["EdDSA"],
|
|
398
|
-
});
|
|
399
|
-
return null;
|
|
400
|
-
}
|
|
401
|
-
catch (error) {
|
|
402
|
-
return {
|
|
403
|
-
code: "XMCP_I_EBADPROOF",
|
|
404
|
-
message: "Signature verification failed",
|
|
405
|
-
httpStatus: 403,
|
|
406
|
-
details: {
|
|
407
|
-
reason: error instanceof Error ? error.message : "Unknown error",
|
|
408
|
-
remediation: "Check proof signature and public key",
|
|
409
|
-
},
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
/**
|
|
414
|
-
* Create canonical payload that matches runtime implementation
|
|
415
|
-
* Uses JSON Canonicalization Scheme (JCS) RFC 8785 for deterministic ordering
|
|
416
|
-
*/
|
|
417
|
-
createCanonicalPayload(meta) {
|
|
418
|
-
// Create payload object with required fields
|
|
419
|
-
const payload = {
|
|
420
|
-
audience: meta.audience,
|
|
421
|
-
nonce: meta.nonce,
|
|
422
|
-
requestHash: meta.requestHash,
|
|
423
|
-
responseHash: meta.responseHash,
|
|
424
|
-
sessionId: meta.sessionId,
|
|
425
|
-
ts: meta.ts,
|
|
426
|
-
};
|
|
427
|
-
// Add optional fields
|
|
428
|
-
if (meta.scopeId) {
|
|
429
|
-
payload.scopeId = meta.scopeId;
|
|
430
|
-
}
|
|
431
|
-
if (meta.delegationRef) {
|
|
432
|
-
payload.delegationRef = meta.delegationRef;
|
|
433
|
-
}
|
|
434
|
-
// Use RFC 8785 compliant JCS canonicalization
|
|
435
|
-
return canonicalize(payload);
|
|
436
|
-
}
|
|
437
|
-
/**
|
|
438
|
-
* Verify delegation status via KTA with caching
|
|
439
|
-
*/
|
|
440
|
-
async verifyDelegation(delegationRef, did, kid) {
|
|
441
|
-
try {
|
|
442
|
-
// Check cache first
|
|
443
|
-
const cached = this.delegationCache.get(delegationRef);
|
|
444
|
-
if (cached && cached.expiresAt > Date.now()) {
|
|
445
|
-
return this.validateDelegationResponse(cached.response, did, kid);
|
|
446
|
-
}
|
|
447
|
-
if (this.config.allowMockData) {
|
|
448
|
-
// Mock delegation for testing
|
|
449
|
-
if (delegationRef.startsWith("mock:")) {
|
|
450
|
-
const mockResponse = {
|
|
451
|
-
active: delegationRef !== "mock:revoked",
|
|
452
|
-
did,
|
|
453
|
-
kid,
|
|
454
|
-
scopes: ["*"],
|
|
455
|
-
};
|
|
456
|
-
return this.validateDelegationResponse(mockResponse, did, kid);
|
|
457
|
-
}
|
|
458
|
-
// For other delegation refs in test mode, assume they're valid
|
|
459
|
-
const mockResponse = {
|
|
460
|
-
active: true,
|
|
461
|
-
did,
|
|
462
|
-
kid,
|
|
463
|
-
scopes: ["*"],
|
|
464
|
-
};
|
|
465
|
-
return this.validateDelegationResponse(mockResponse, did, kid);
|
|
466
|
-
}
|
|
467
|
-
const response = await fetch(`${this.config.ktaBaseUrl}/api/v1/delegations/${encodeURIComponent(delegationRef)}`, {
|
|
468
|
-
method: "GET",
|
|
469
|
-
headers: {
|
|
470
|
-
"Content-Type": "application/json",
|
|
471
|
-
"User-Agent": "XMCP-I-Verifier/1.0",
|
|
472
|
-
},
|
|
473
|
-
// Add timeout for production reliability
|
|
474
|
-
signal: AbortSignal.timeout(5000),
|
|
475
|
-
});
|
|
476
|
-
if (!response || !response.ok) {
|
|
477
|
-
if (response && response.status === 404) {
|
|
478
|
-
return {
|
|
479
|
-
code: "XMCP_I_EBADPROOF",
|
|
480
|
-
message: "Delegation not found",
|
|
481
|
-
httpStatus: 403,
|
|
482
|
-
details: {
|
|
483
|
-
reason: "Delegation reference not found in KTA",
|
|
484
|
-
remediation: "Check delegation reference and ensure it exists",
|
|
485
|
-
},
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
throw new Error(`KTA API error: ${response.status}`);
|
|
489
|
-
}
|
|
490
|
-
const delegation = (await response.json());
|
|
491
|
-
// Cache the response
|
|
492
|
-
this.delegationCache.set(delegationRef, {
|
|
493
|
-
response: delegation,
|
|
494
|
-
expiresAt: Date.now() + this.config.delegationCacheTtl * 1000,
|
|
495
|
-
});
|
|
496
|
-
return this.validateDelegationResponse(delegation, did, kid);
|
|
497
|
-
}
|
|
498
|
-
catch (error) {
|
|
499
|
-
// Treat delegation check failures as verification failures
|
|
500
|
-
console.warn("Delegation verification failed:", error);
|
|
501
|
-
return {
|
|
502
|
-
code: "XMCP_I_EBADPROOF",
|
|
503
|
-
message: "Delegation verification failed",
|
|
504
|
-
httpStatus: 403,
|
|
505
|
-
details: {
|
|
506
|
-
reason: error instanceof Error ? error.message : "Unknown error",
|
|
507
|
-
remediation: "Check KTA connectivity and delegation status",
|
|
508
|
-
},
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
/**
|
|
513
|
-
* Validate delegation response
|
|
514
|
-
*/
|
|
515
|
-
validateDelegationResponse(delegation, expectedDid, expectedKeyId) {
|
|
516
|
-
if (!delegation.active) {
|
|
517
|
-
return {
|
|
518
|
-
code: "XMCP_I_EBADPROOF",
|
|
519
|
-
message: "Delegation revoked or inactive",
|
|
520
|
-
httpStatus: 403,
|
|
521
|
-
details: {
|
|
522
|
-
reason: "Delegation is not active",
|
|
523
|
-
remediation: "Renew delegation or use direct identity",
|
|
524
|
-
},
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
if (delegation.did !== expectedDid || delegation.kid !== expectedKeyId) {
|
|
528
|
-
return {
|
|
529
|
-
code: "XMCP_I_EBADPROOF",
|
|
530
|
-
message: "Delegation identity mismatch",
|
|
531
|
-
httpStatus: 403,
|
|
532
|
-
details: {
|
|
533
|
-
reason: "Delegation does not match proof identity",
|
|
534
|
-
expected: `${expectedDid}#${expectedKeyId}`,
|
|
535
|
-
received: `${delegation.did}#${delegation.kid}`,
|
|
536
|
-
},
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
if (delegation.expiresAt && delegation.expiresAt < Date.now() / 1000) {
|
|
540
|
-
return {
|
|
541
|
-
code: "XMCP_I_EBADPROOF",
|
|
542
|
-
message: "Delegation expired",
|
|
543
|
-
httpStatus: 403,
|
|
544
|
-
details: {
|
|
545
|
-
reason: "Delegation has expired",
|
|
546
|
-
remediation: "Renew delegation",
|
|
547
|
-
},
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
return null;
|
|
551
|
-
}
|
|
552
|
-
/**
|
|
553
|
-
* Fetch public key from DID document with caching
|
|
554
|
-
*/
|
|
555
|
-
async fetchPublicKeyWithCache(did, kid) {
|
|
556
|
-
const cacheKey = `${did}#${kid}`;
|
|
557
|
-
const cached = this.didCache.get(cacheKey);
|
|
558
|
-
if (cached && cached.expiresAt > Date.now()) {
|
|
559
|
-
return this.extractPublicKey(cached.document, kid);
|
|
560
|
-
}
|
|
561
|
-
const didDoc = await this.fetchDIDDocument(did);
|
|
562
|
-
if (!didDoc) {
|
|
563
|
-
return null;
|
|
564
|
-
}
|
|
565
|
-
// Cache the DID document
|
|
566
|
-
this.didCache.set(cacheKey, {
|
|
567
|
-
document: didDoc,
|
|
568
|
-
expiresAt: Date.now() + this.config.didCacheTtl * 1000,
|
|
569
|
-
});
|
|
570
|
-
return this.extractPublicKey(didDoc, kid);
|
|
571
|
-
}
|
|
572
|
-
/**
|
|
573
|
-
* Fetch DID document from well-known endpoint
|
|
574
|
-
*/
|
|
575
|
-
async fetchDIDDocument(did) {
|
|
576
|
-
try {
|
|
577
|
-
if (this.config.allowMockData && did.startsWith("did:test:")) {
|
|
578
|
-
// Mock DID document for testing
|
|
579
|
-
return {
|
|
580
|
-
id: did,
|
|
581
|
-
verificationMethod: [
|
|
582
|
-
{
|
|
583
|
-
id: `#key-test-1`,
|
|
584
|
-
type: "Ed25519VerificationKey2020",
|
|
585
|
-
controller: did,
|
|
586
|
-
publicKeyJwk: {
|
|
587
|
-
kty: "OKP",
|
|
588
|
-
crv: "Ed25519",
|
|
589
|
-
x: "mock-public-key-data",
|
|
590
|
-
},
|
|
591
|
-
},
|
|
592
|
-
],
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
// Convert DID to well-known URL for did:web
|
|
596
|
-
if (!did.startsWith("did:web:")) {
|
|
597
|
-
throw new Error("Only did:web is supported");
|
|
598
|
-
}
|
|
599
|
-
const domain = did.replace("did:web:", "").replace(/:/g, "/");
|
|
600
|
-
const didDocUrl = `https://${domain}/.well-known/did.json`;
|
|
601
|
-
const response = await fetch(didDocUrl, {
|
|
602
|
-
headers: {
|
|
603
|
-
Accept: "application/did+json, application/json",
|
|
604
|
-
"User-Agent": "XMCP-I-Verifier/1.0",
|
|
605
|
-
},
|
|
606
|
-
// Add timeout for production reliability
|
|
607
|
-
signal: AbortSignal.timeout(5000),
|
|
608
|
-
});
|
|
609
|
-
if (!response.ok) {
|
|
610
|
-
throw new Error(`Failed to fetch DID document: ${response.status}`);
|
|
611
|
-
}
|
|
612
|
-
return await response.json();
|
|
613
|
-
}
|
|
614
|
-
catch (error) {
|
|
615
|
-
console.warn("Failed to fetch DID document:", error);
|
|
616
|
-
return null;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* Extract public key from DID document
|
|
621
|
-
*/
|
|
622
|
-
extractPublicKey(didDoc, kid) {
|
|
623
|
-
try {
|
|
624
|
-
// Find verification method
|
|
625
|
-
const verificationMethod = didDoc.verificationMethod?.find((vm) => vm.id === `#${kid}` || vm.id === `${didDoc.id}#${kid}`);
|
|
626
|
-
if (!verificationMethod) {
|
|
627
|
-
throw new Error(`Key ${kid} not found in DID document`);
|
|
628
|
-
}
|
|
629
|
-
// Convert to JWK format if needed
|
|
630
|
-
if (verificationMethod.publicKeyJwk) {
|
|
631
|
-
return verificationMethod.publicKeyJwk;
|
|
632
|
-
}
|
|
633
|
-
if (verificationMethod.publicKeyMultibase) {
|
|
634
|
-
// Convert multibase to JWK (simplified for Ed25519)
|
|
635
|
-
// This is a placeholder - real implementation would use proper multibase decoding
|
|
636
|
-
return {
|
|
637
|
-
kty: "OKP",
|
|
638
|
-
crv: "Ed25519",
|
|
639
|
-
x: verificationMethod.publicKeyMultibase.slice(1), // Remove multibase prefix
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
throw new Error("Unsupported public key format");
|
|
643
|
-
}
|
|
644
|
-
catch (error) {
|
|
645
|
-
console.warn("Failed to extract public key:", error);
|
|
646
|
-
return null;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
/**
|
|
650
|
-
* Generate trusted headers for successful verification
|
|
651
|
-
*/
|
|
652
|
-
generateHeaders(meta) {
|
|
653
|
-
const headers = {
|
|
654
|
-
[AGENT_HEADERS.DID]: meta.did,
|
|
655
|
-
[AGENT_HEADERS.KEY_ID]: meta.kid,
|
|
656
|
-
[AGENT_HEADERS.SESSION]: meta.sessionId,
|
|
657
|
-
[AGENT_HEADERS.CONFIDENCE]: "verified",
|
|
658
|
-
[AGENT_HEADERS.VERIFIED_AT]: Math.floor(Date.now() / 1000).toString(),
|
|
659
|
-
};
|
|
660
|
-
if (meta.scopeId) {
|
|
661
|
-
headers[AGENT_HEADERS.SCOPES] = meta.scopeId;
|
|
662
|
-
}
|
|
663
|
-
if (meta.delegationRef) {
|
|
664
|
-
headers[AGENT_HEADERS.DELEGATION_REF] = meta.delegationRef;
|
|
665
|
-
}
|
|
666
|
-
// Add registry URL for traceability
|
|
667
|
-
headers[AGENT_HEADERS.REGISTRY] = `${this.config.ktaBaseUrl}/agents/${encodeURIComponent(meta.did)}`;
|
|
668
|
-
return headers;
|
|
669
|
-
}
|
|
670
|
-
/**
|
|
671
|
-
* Generate agent context for MCP recipients
|
|
672
|
-
*/
|
|
673
|
-
generateAgentContext(meta) {
|
|
674
|
-
return {
|
|
675
|
-
did: meta.did,
|
|
676
|
-
kid: meta.kid,
|
|
677
|
-
subject: meta.delegationRef ? meta.did : undefined,
|
|
678
|
-
scopes: meta.scopeId ? meta.scopeId.split(",") : [],
|
|
679
|
-
session: meta.sessionId,
|
|
680
|
-
confidence: "verified",
|
|
681
|
-
delegationRef: meta.delegationRef,
|
|
682
|
-
registry: `${this.config.ktaBaseUrl}/agents/${encodeURIComponent(meta.did)}`,
|
|
683
|
-
verifiedAt: Math.floor(Date.now() / 1000),
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
/**
|
|
687
|
-
* Create error result from structured error
|
|
688
|
-
*/
|
|
689
|
-
createErrorResult(error) {
|
|
690
|
-
return {
|
|
691
|
-
success: false,
|
|
692
|
-
error: {
|
|
693
|
-
code: error.code,
|
|
694
|
-
message: error.message,
|
|
695
|
-
details: error.details,
|
|
696
|
-
httpStatus: error.httpStatus ||
|
|
697
|
-
ERROR_HTTP_STATUS[error.code] ||
|
|
698
|
-
500,
|
|
699
|
-
},
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
/**
|
|
703
|
-
* Log verification attempt for security monitoring
|
|
704
|
-
*/
|
|
705
|
-
logVerificationAttempt(context, success, reason, duration) {
|
|
706
|
-
// In production, this would integrate with your logging system
|
|
707
|
-
const logEntry = {
|
|
708
|
-
timestamp: new Date().toISOString(),
|
|
709
|
-
did: context.proof?.meta?.did || "unknown",
|
|
710
|
-
audience: context.audience,
|
|
711
|
-
success,
|
|
712
|
-
reason,
|
|
713
|
-
duration,
|
|
714
|
-
sessionId: context.proof?.meta?.sessionId,
|
|
715
|
-
};
|
|
716
|
-
// For now, just console.log - in production, use structured logging
|
|
717
|
-
console.log("XMCP-I Verification:", JSON.stringify(logEntry));
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* Clean up expired cache entries
|
|
721
|
-
*/
|
|
722
|
-
cleanupCache() {
|
|
723
|
-
const now = Date.now();
|
|
724
|
-
for (const [key, value] of this.didCache.entries()) {
|
|
725
|
-
if (value.expiresAt <= now) {
|
|
726
|
-
this.didCache.delete(key);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
for (const [key, value] of this.delegationCache.entries()) {
|
|
730
|
-
if (value.expiresAt <= now) {
|
|
731
|
-
this.delegationCache.delete(key);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
//# sourceMappingURL=core.js.map
|