@mcp-i/core 1.1.1 → 1.2.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/dist/delegation/cascading-revocation.d.ts.map +1 -1
- package/dist/delegation/cascading-revocation.js +3 -1
- package/dist/delegation/cascading-revocation.js.map +1 -1
- package/dist/delegation/outbound-headers.d.ts +12 -12
- package/dist/delegation/outbound-headers.d.ts.map +1 -1
- package/dist/delegation/outbound-headers.js +12 -12
- package/dist/delegation/outbound-headers.js.map +1 -1
- package/dist/delegation/outbound-proof.d.ts +1 -1
- package/dist/delegation/outbound-proof.js +1 -1
- package/dist/delegation/statuslist-manager.d.ts +3 -0
- package/dist/delegation/statuslist-manager.d.ts.map +1 -1
- package/dist/delegation/statuslist-manager.js +13 -0
- package/dist/delegation/statuslist-manager.js.map +1 -1
- package/dist/middleware/with-mcpi-server.d.ts +1 -1
- package/dist/middleware/with-mcpi-server.d.ts.map +1 -1
- package/dist/middleware/with-mcpi-server.js.map +1 -1
- package/dist/middleware/with-mcpi.d.ts +14 -0
- package/dist/middleware/with-mcpi.d.ts.map +1 -1
- package/dist/middleware/with-mcpi.js +12 -0
- package/dist/middleware/with-mcpi.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/audit/canonicalization-integrity.test.ts +243 -0
- package/src/__tests__/audit/graph-revocation-roundtrip.test.ts +280 -0
- package/src/__tests__/audit/helpers/crypto-helpers.ts +245 -0
- package/src/__tests__/audit/proof-boundary.test.ts +269 -0
- package/src/__tests__/audit/statuslist-bitstring-roundtrip.test.ts +135 -0
- package/src/__tests__/audit/vc-roundtrip.test.ts +290 -0
- package/src/delegation/__tests__/outbound-headers.test.ts +16 -16
- package/src/delegation/__tests__/transitive-access.test.ts +1233 -0
- package/src/delegation/__tests__/vc-issuer.integration.test.ts +136 -0
- package/src/delegation/__tests__/vc-jwt.test.ts +318 -0
- package/src/delegation/__tests__/vc-verifier.integration.test.ts +199 -0
- package/src/delegation/cascading-revocation.ts +3 -1
- package/src/delegation/outbound-headers.ts +16 -16
- package/src/delegation/outbound-proof.ts +1 -1
- package/src/delegation/statuslist-manager.ts +17 -0
- package/src/middleware/with-mcpi-server.ts +1 -5
- package/src/middleware/with-mcpi.ts +29 -0
- package/src/proof/__tests__/verifier.integration.test.ts +181 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VC Round-Trip Audit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the full lifecycle: issue a DelegationCredential with real Ed25519
|
|
5
|
+
* signing, then verify it through the 3-stage verification pipeline.
|
|
6
|
+
* No mocks on crypto, canonicalization, or signing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
10
|
+
import { DelegationCredentialIssuer } from '../../delegation/vc-issuer.js';
|
|
11
|
+
import {
|
|
12
|
+
DelegationCredentialVerifier,
|
|
13
|
+
type DIDResolver,
|
|
14
|
+
} from '../../delegation/vc-verifier.js';
|
|
15
|
+
import { createDidKeyResolver } from '../../delegation/did-key-resolver.js';
|
|
16
|
+
import type { AgentIdentity } from '../../providers/base.js';
|
|
17
|
+
import type { DelegationRecord, DelegationCredential } from '../../types/protocol.js';
|
|
18
|
+
import {
|
|
19
|
+
createRealCryptoProvider,
|
|
20
|
+
createRealIdentity,
|
|
21
|
+
createRealSigningFunction,
|
|
22
|
+
createRealSignatureVerifier,
|
|
23
|
+
} from './helpers/crypto-helpers.js';
|
|
24
|
+
import { NodeCryptoProvider } from '../utils/node-crypto-provider.js';
|
|
25
|
+
|
|
26
|
+
describe('VC Round-Trip Audit', () => {
|
|
27
|
+
let crypto: NodeCryptoProvider;
|
|
28
|
+
let issuerIdentity: AgentIdentity;
|
|
29
|
+
let subjectIdentity: AgentIdentity;
|
|
30
|
+
let issuer: DelegationCredentialIssuer;
|
|
31
|
+
let verifier: DelegationCredentialVerifier;
|
|
32
|
+
let didResolver: DIDResolver;
|
|
33
|
+
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
crypto = createRealCryptoProvider();
|
|
36
|
+
issuerIdentity = await createRealIdentity(crypto);
|
|
37
|
+
subjectIdentity = await createRealIdentity(crypto);
|
|
38
|
+
|
|
39
|
+
const signingFunction = createRealSigningFunction(crypto, issuerIdentity);
|
|
40
|
+
const signatureVerifier = createRealSignatureVerifier(crypto);
|
|
41
|
+
didResolver = createDidKeyResolver();
|
|
42
|
+
|
|
43
|
+
const issuerIdProvider = {
|
|
44
|
+
getDid: () => issuerIdentity.did,
|
|
45
|
+
getKeyId: () => issuerIdentity.kid,
|
|
46
|
+
getPrivateKey: () => issuerIdentity.privateKey,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
issuer = new DelegationCredentialIssuer(issuerIdProvider, signingFunction);
|
|
50
|
+
|
|
51
|
+
verifier = new DelegationCredentialVerifier({
|
|
52
|
+
didResolver,
|
|
53
|
+
signatureVerifier,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function makeDelegationRecord(overrides?: Partial<DelegationRecord>): DelegationRecord {
|
|
58
|
+
return {
|
|
59
|
+
id: 'test-delegation-001',
|
|
60
|
+
issuerDid: issuerIdentity.did,
|
|
61
|
+
subjectDid: subjectIdentity.did,
|
|
62
|
+
vcId: 'urn:uuid:test-vc-001',
|
|
63
|
+
constraints: {
|
|
64
|
+
scopes: ['tools:read', 'tools:write'],
|
|
65
|
+
notBefore: Math.floor(Date.now() / 1000) - 3600,
|
|
66
|
+
notAfter: Math.floor(Date.now() / 1000) + 3600,
|
|
67
|
+
},
|
|
68
|
+
signature: '',
|
|
69
|
+
status: 'active',
|
|
70
|
+
createdAt: Date.now(),
|
|
71
|
+
...overrides,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Round-Trip Tests ──────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
it('should issue and verify a delegation credential round-trip', async () => {
|
|
78
|
+
const delegation = makeDelegationRecord();
|
|
79
|
+
const vc = await issuer.issueDelegationCredential(delegation);
|
|
80
|
+
|
|
81
|
+
const result = await verifier.verifyDelegationCredential(vc, {
|
|
82
|
+
skipStatus: true,
|
|
83
|
+
skipCache: true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(result.valid).toBe(true);
|
|
87
|
+
expect(result.stage).toBe('complete');
|
|
88
|
+
expect(result.checks?.basicValid).toBe(true);
|
|
89
|
+
expect(result.checks?.signatureValid).toBe(true);
|
|
90
|
+
expect(result.reason).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should reject VC with tampered credentialSubject', async () => {
|
|
94
|
+
const delegation = makeDelegationRecord();
|
|
95
|
+
const vc = await issuer.issueDelegationCredential(delegation);
|
|
96
|
+
|
|
97
|
+
// Tamper with the scopes after signing
|
|
98
|
+
const tampered = structuredClone(vc);
|
|
99
|
+
tampered.credentialSubject.delegation.scopes = ['admin:*'];
|
|
100
|
+
|
|
101
|
+
const result = await verifier.verifyDelegationCredential(tampered, {
|
|
102
|
+
skipStatus: true,
|
|
103
|
+
skipCache: true,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(result.valid).toBe(false);
|
|
107
|
+
expect(result.checks?.signatureValid).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should reject VC with swapped issuer DID', async () => {
|
|
111
|
+
const delegation = makeDelegationRecord();
|
|
112
|
+
const vc = await issuer.issueDelegationCredential(delegation);
|
|
113
|
+
|
|
114
|
+
// Swap issuer to subject's DID — resolver will return subject's key,
|
|
115
|
+
// which won't match the issuer's signature
|
|
116
|
+
const tampered = structuredClone(vc);
|
|
117
|
+
tampered.issuer = subjectIdentity.did;
|
|
118
|
+
|
|
119
|
+
const result = await verifier.verifyDelegationCredential(tampered, {
|
|
120
|
+
skipStatus: true,
|
|
121
|
+
skipCache: true,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(result.valid).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should reject expired VC', async () => {
|
|
128
|
+
const delegation = makeDelegationRecord({
|
|
129
|
+
constraints: {
|
|
130
|
+
scopes: ['tools:read'],
|
|
131
|
+
notAfter: Math.floor(Date.now() / 1000) - 1, // 1 second in the past
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const vc = await issuer.issueDelegationCredential(delegation);
|
|
135
|
+
|
|
136
|
+
const result = await verifier.verifyDelegationCredential(vc, {
|
|
137
|
+
skipStatus: true,
|
|
138
|
+
skipCache: true,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(result.valid).toBe(false);
|
|
142
|
+
expect(result.stage).toBe('basic');
|
|
143
|
+
expect(result.reason).toContain('expired');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should reject VC with revoked status field', async () => {
|
|
147
|
+
const delegation = makeDelegationRecord({ status: 'revoked' });
|
|
148
|
+
const vc = await issuer.issueDelegationCredential(delegation);
|
|
149
|
+
|
|
150
|
+
const result = await verifier.verifyDelegationCredential(vc, {
|
|
151
|
+
skipStatus: true,
|
|
152
|
+
skipCache: true,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(result.valid).toBe(false);
|
|
156
|
+
expect(result.stage).toBe('basic');
|
|
157
|
+
expect(result.reason).toContain('revoked');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should reject VC without proof field', async () => {
|
|
161
|
+
const delegation = makeDelegationRecord();
|
|
162
|
+
const vc = await issuer.issueDelegationCredential(delegation);
|
|
163
|
+
|
|
164
|
+
// Remove proof entirely
|
|
165
|
+
const noProof = { ...vc } as Record<string, unknown>;
|
|
166
|
+
delete noProof['proof'];
|
|
167
|
+
|
|
168
|
+
const result = await verifier.verifyDelegationCredential(
|
|
169
|
+
noProof as DelegationCredential,
|
|
170
|
+
{ skipStatus: true, skipCache: true }
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
expect(result.valid).toBe(false);
|
|
174
|
+
expect(result.stage).toBe('basic');
|
|
175
|
+
expect(result.reason).toContain('proof');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should reject VC when DID resolver returns null', async () => {
|
|
179
|
+
const delegation = makeDelegationRecord();
|
|
180
|
+
const vc = await issuer.issueDelegationCredential(delegation);
|
|
181
|
+
|
|
182
|
+
const nullResolver: DIDResolver = {
|
|
183
|
+
resolve: async () => null,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const strictVerifier = new DelegationCredentialVerifier({
|
|
187
|
+
didResolver: nullResolver,
|
|
188
|
+
signatureVerifier: createRealSignatureVerifier(crypto),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const result = await strictVerifier.verifyDelegationCredential(vc, {
|
|
192
|
+
skipStatus: true,
|
|
193
|
+
skipCache: true,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(result.valid).toBe(false);
|
|
197
|
+
expect(result.reason).toContain('resolve');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ── Caching Tests ─────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
it('should return cached result on second verification', async () => {
|
|
203
|
+
const delegation = makeDelegationRecord();
|
|
204
|
+
const vc = await issuer.issueDelegationCredential(delegation);
|
|
205
|
+
|
|
206
|
+
const cachingVerifier = new DelegationCredentialVerifier({
|
|
207
|
+
didResolver,
|
|
208
|
+
signatureVerifier: createRealSignatureVerifier(crypto),
|
|
209
|
+
cacheTtl: 60_000,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const first = await cachingVerifier.verifyDelegationCredential(vc, {
|
|
213
|
+
skipStatus: true,
|
|
214
|
+
});
|
|
215
|
+
expect(first.valid).toBe(true);
|
|
216
|
+
expect(first.cached).toBeUndefined();
|
|
217
|
+
|
|
218
|
+
const second = await cachingVerifier.verifyDelegationCredential(vc, {
|
|
219
|
+
skipStatus: true,
|
|
220
|
+
});
|
|
221
|
+
expect(second.valid).toBe(true);
|
|
222
|
+
expect(second.cached).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should evict oldest cache entry when maxCacheSize is exceeded', async () => {
|
|
226
|
+
const cachingVerifier = new DelegationCredentialVerifier({
|
|
227
|
+
didResolver,
|
|
228
|
+
signatureVerifier: createRealSignatureVerifier(crypto),
|
|
229
|
+
cacheTtl: 60_000,
|
|
230
|
+
maxCacheSize: 2,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Issue 3 distinct VCs
|
|
234
|
+
const vcs: DelegationCredential[] = [];
|
|
235
|
+
for (let i = 0; i < 3; i++) {
|
|
236
|
+
const delegation = makeDelegationRecord({
|
|
237
|
+
id: `cache-test-${i}`,
|
|
238
|
+
vcId: `urn:uuid:cache-test-${i}`,
|
|
239
|
+
});
|
|
240
|
+
vcs.push(await issuer.issueDelegationCredential(delegation));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Verify all 3 — first should be evicted
|
|
244
|
+
for (const vc of vcs) {
|
|
245
|
+
await cachingVerifier.verifyDelegationCredential(vc, { skipStatus: true });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// First VC should NOT be cached (evicted)
|
|
249
|
+
const first = await cachingVerifier.verifyDelegationCredential(vcs[0]!, {
|
|
250
|
+
skipStatus: true,
|
|
251
|
+
});
|
|
252
|
+
expect(first.cached).toBeUndefined();
|
|
253
|
+
|
|
254
|
+
// Third VC should still be cached
|
|
255
|
+
const third = await cachingVerifier.verifyDelegationCredential(vcs[2]!, {
|
|
256
|
+
skipStatus: true,
|
|
257
|
+
});
|
|
258
|
+
expect(third.cached).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ── Signature Integrity ───────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
it('should produce different signatures for different delegations', async () => {
|
|
264
|
+
const vc1 = await issuer.issueDelegationCredential(
|
|
265
|
+
makeDelegationRecord({ id: 'del-A' })
|
|
266
|
+
);
|
|
267
|
+
const vc2 = await issuer.issueDelegationCredential(
|
|
268
|
+
makeDelegationRecord({ id: 'del-B' })
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
expect(vc1.proof?.proofValue).not.toBe(vc2.proof?.proofValue);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should produce identical canonicalization for same delegation regardless of field order', async () => {
|
|
275
|
+
const delegation = makeDelegationRecord();
|
|
276
|
+
const vc1 = await issuer.issueDelegationCredential(delegation);
|
|
277
|
+
const vc2 = await issuer.issueDelegationCredential(delegation);
|
|
278
|
+
|
|
279
|
+
// Same input should produce same unsigned VC structure
|
|
280
|
+
// (proof timestamps differ, but the unsigned portion should canonicalize the same)
|
|
281
|
+
const strip = (vc: DelegationCredential) => {
|
|
282
|
+
const copy = { ...vc } as Record<string, unknown>;
|
|
283
|
+
delete copy['proof'];
|
|
284
|
+
return copy;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const { canonicalizeJSON } = await import('../../delegation/utils.js');
|
|
288
|
+
expect(canonicalizeJSON(strip(vc1))).toBe(canonicalizeJSON(strip(vc2)));
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -90,38 +90,38 @@ describe('buildOutboundDelegationHeaders', () => {
|
|
|
90
90
|
const context = createTestContext();
|
|
91
91
|
const headers = await buildOutboundDelegationHeaders(context);
|
|
92
92
|
|
|
93
|
-
expect(headers).toHaveProperty('
|
|
94
|
-
expect(headers).toHaveProperty('
|
|
95
|
-
expect(headers).toHaveProperty('
|
|
96
|
-
expect(headers).toHaveProperty('
|
|
93
|
+
expect(headers).toHaveProperty('KYA-Agent-DID');
|
|
94
|
+
expect(headers).toHaveProperty('KYA-Delegation-Chain');
|
|
95
|
+
expect(headers).toHaveProperty('KYA-Session-Id');
|
|
96
|
+
expect(headers).toHaveProperty('KYA-Delegation-Proof');
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
it('
|
|
99
|
+
it('KYA-Agent-DID matches session.agentDid', async () => {
|
|
100
100
|
const context = createTestContext();
|
|
101
101
|
const headers = await buildOutboundDelegationHeaders(context);
|
|
102
102
|
|
|
103
|
-
expect(headers['
|
|
103
|
+
expect(headers['KYA-Agent-DID']).toBe(context.session.agentDid);
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
it('
|
|
106
|
+
it('KYA-Delegation-Chain matches delegation.vcId', async () => {
|
|
107
107
|
const context = createTestContext();
|
|
108
108
|
const headers = await buildOutboundDelegationHeaders(context);
|
|
109
109
|
|
|
110
|
-
expect(headers['
|
|
110
|
+
expect(headers['KYA-Delegation-Chain']).toBe(context.delegation.vcId);
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
it('
|
|
113
|
+
it('KYA-Session-Id matches session.sessionId', async () => {
|
|
114
114
|
const context = createTestContext();
|
|
115
115
|
const headers = await buildOutboundDelegationHeaders(context);
|
|
116
116
|
|
|
117
|
-
expect(headers['
|
|
117
|
+
expect(headers['KYA-Session-Id']).toBe(context.session.sessionId);
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
it('
|
|
120
|
+
it('KYA-Delegation-Proof is a valid JWT with correct claims', async () => {
|
|
121
121
|
const context = createTestContext();
|
|
122
122
|
const headers = await buildOutboundDelegationHeaders(context);
|
|
123
123
|
|
|
124
|
-
const jwt = headers['
|
|
124
|
+
const jwt = headers['KYA-Delegation-Proof'];
|
|
125
125
|
|
|
126
126
|
// Verify it's a valid JWT format (3 parts)
|
|
127
127
|
expect(jwt.split('.')).toHaveLength(3);
|
|
@@ -148,7 +148,7 @@ describe('buildOutboundDelegationHeaders', () => {
|
|
|
148
148
|
});
|
|
149
149
|
const headers = await buildOutboundDelegationHeaders(context);
|
|
150
150
|
|
|
151
|
-
const payload = decodeJwt(headers['
|
|
151
|
+
const payload = decodeJwt(headers['KYA-Delegation-Proof']);
|
|
152
152
|
expect(payload.aud).toBe('api.service.example.com');
|
|
153
153
|
});
|
|
154
154
|
|
|
@@ -158,7 +158,7 @@ describe('buildOutboundDelegationHeaders', () => {
|
|
|
158
158
|
});
|
|
159
159
|
const headers = await buildOutboundDelegationHeaders(context);
|
|
160
160
|
|
|
161
|
-
const payload = decodeJwt(headers['
|
|
161
|
+
const payload = decodeJwt(headers['KYA-Delegation-Proof']);
|
|
162
162
|
expect(payload.aud).toBe('internal-service.local');
|
|
163
163
|
});
|
|
164
164
|
|
|
@@ -168,7 +168,7 @@ describe('buildOutboundDelegationHeaders', () => {
|
|
|
168
168
|
});
|
|
169
169
|
const headers = await buildOutboundDelegationHeaders(context);
|
|
170
170
|
|
|
171
|
-
const payload = decodeJwt(headers['
|
|
171
|
+
const payload = decodeJwt(headers['KYA-Delegation-Proof']);
|
|
172
172
|
expect(payload.aud).toBe('secure.example.org');
|
|
173
173
|
});
|
|
174
174
|
|
|
@@ -176,7 +176,7 @@ describe('buildOutboundDelegationHeaders', () => {
|
|
|
176
176
|
const context = createTestContext();
|
|
177
177
|
const headers = await buildOutboundDelegationHeaders(context);
|
|
178
178
|
|
|
179
|
-
const payload = decodeJwt(headers['
|
|
179
|
+
const payload = decodeJwt(headers['KYA-Delegation-Proof']);
|
|
180
180
|
expect((payload.exp as number) - (payload.iat as number)).toBe(60);
|
|
181
181
|
});
|
|
182
182
|
|