@mcp-i/core 1.1.3 → 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/auth/handshake.d.ts +19 -4
- package/dist/auth/handshake.d.ts.map +1 -1
- package/dist/auth/handshake.js +52 -15
- package/dist/auth/handshake.js.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js.map +1 -1
- 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/did-key-resolver.d.ts.map +1 -1
- package/dist/delegation/did-key-resolver.js +9 -6
- package/dist/delegation/did-key-resolver.js.map +1 -1
- package/dist/delegation/outbound-headers.d.ts +14 -16
- package/dist/delegation/outbound-headers.d.ts.map +1 -1
- package/dist/delegation/outbound-headers.js +14 -15
- 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 +14 -1
- package/dist/delegation/statuslist-manager.js.map +1 -1
- package/dist/delegation/vc-verifier.d.ts.map +1 -1
- package/dist/delegation/vc-verifier.js +2 -2
- package/dist/delegation/vc-verifier.js.map +1 -1
- package/dist/errors.d.ts +42 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +45 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +1 -0
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/mcpi-transport.d.ts +39 -0
- package/dist/middleware/mcpi-transport.d.ts.map +1 -0
- package/dist/middleware/mcpi-transport.js +121 -0
- package/dist/middleware/mcpi-transport.js.map +1 -0
- package/dist/middleware/with-mcpi-server.d.ts +25 -9
- package/dist/middleware/with-mcpi-server.d.ts.map +1 -1
- package/dist/middleware/with-mcpi-server.js +62 -47
- package/dist/middleware/with-mcpi-server.js.map +1 -1
- package/dist/middleware/with-mcpi.d.ts +40 -5
- package/dist/middleware/with-mcpi.d.ts.map +1 -1
- package/dist/middleware/with-mcpi.js +120 -10
- package/dist/middleware/with-mcpi.js.map +1 -1
- package/dist/providers/memory.js +2 -2
- package/dist/providers/memory.js.map +1 -1
- package/dist/session/manager.d.ts +7 -1
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +20 -4
- package/dist/session/manager.js.map +1 -1
- package/dist/utils/crypto-service.d.ts.map +1 -1
- package/dist/utils/crypto-service.js +11 -10
- package/dist/utils/crypto-service.js.map +1 -1
- package/dist/utils/did-helpers.d.ts +12 -0
- package/dist/utils/did-helpers.d.ts.map +1 -1
- package/dist/utils/did-helpers.js +18 -0
- package/dist/utils/did-helpers.js.map +1 -1
- package/package.json +2 -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.ts +29 -0
- package/src/proof/__tests__/verifier.integration.test.ts +181 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proof Boundary Audit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the proof generation → verification pipeline at exact boundary
|
|
5
|
+
* conditions: timestamp skew thresholds, nonce scoping, malformed proofs.
|
|
6
|
+
* Uses ControllableClockProvider for deterministic timestamp testing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
|
10
|
+
import { ProofGenerator } from '../../proof/generator.js';
|
|
11
|
+
import { ProofVerifier } from '../../proof/verifier.js';
|
|
12
|
+
import type { AgentIdentity } from '../../providers/base.js';
|
|
13
|
+
import type { DetachedProof } from '../../types/protocol.js';
|
|
14
|
+
import { extractPublicKeyFromDidKey, publicKeyToJwk } from '../../delegation/did-key-resolver.js';
|
|
15
|
+
import {
|
|
16
|
+
createRealCryptoProvider,
|
|
17
|
+
createRealIdentity,
|
|
18
|
+
ControllableClockProvider,
|
|
19
|
+
RealFetchProvider,
|
|
20
|
+
MemoryNonceCacheProvider,
|
|
21
|
+
} from './helpers/crypto-helpers.js';
|
|
22
|
+
import { NodeCryptoProvider } from '../utils/node-crypto-provider.js';
|
|
23
|
+
import type { Ed25519JWK } from '../../utils/crypto-service.js';
|
|
24
|
+
|
|
25
|
+
describe('Proof Boundary Audit', () => {
|
|
26
|
+
let crypto: NodeCryptoProvider;
|
|
27
|
+
let agentA: AgentIdentity;
|
|
28
|
+
let agentB: AgentIdentity;
|
|
29
|
+
const SKEW_SECONDS = 300; // default 5 minutes
|
|
30
|
+
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
crypto = createRealCryptoProvider();
|
|
33
|
+
agentA = await createRealIdentity(crypto);
|
|
34
|
+
agentB = await createRealIdentity(crypto);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function makeGenerator(identity: AgentIdentity): ProofGenerator {
|
|
38
|
+
return new ProofGenerator(
|
|
39
|
+
{ did: identity.did, kid: identity.kid, privateKey: identity.privateKey, publicKey: identity.publicKey },
|
|
40
|
+
crypto
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeVerifier(clock: ControllableClockProvider): {
|
|
45
|
+
verifier: ProofVerifier;
|
|
46
|
+
nonceCache: MemoryNonceCacheProvider;
|
|
47
|
+
} {
|
|
48
|
+
const nonceCache = new MemoryNonceCacheProvider();
|
|
49
|
+
const verifier = new ProofVerifier({
|
|
50
|
+
cryptoProvider: crypto,
|
|
51
|
+
clockProvider: clock,
|
|
52
|
+
nonceCacheProvider: nonceCache,
|
|
53
|
+
fetchProvider: new RealFetchProvider(),
|
|
54
|
+
timestampSkewSeconds: SKEW_SECONDS,
|
|
55
|
+
});
|
|
56
|
+
return { verifier, nonceCache };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getPublicKeyJwk(identity: AgentIdentity): Ed25519JWK {
|
|
60
|
+
const rawPublicKey = extractPublicKeyFromDidKey(identity.did);
|
|
61
|
+
const jwk = publicKeyToJwk(rawPublicKey!);
|
|
62
|
+
jwk.kid = identity.kid;
|
|
63
|
+
return jwk as Ed25519JWK;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function generateProof(identity: AgentIdentity): Promise<DetachedProof> {
|
|
67
|
+
const gen = makeGenerator(identity);
|
|
68
|
+
return gen.generateProof(
|
|
69
|
+
{ method: 'tools/call', params: { name: 'test-tool', arguments: { input: 'hello' } } },
|
|
70
|
+
{ data: { output: 'world' } },
|
|
71
|
+
{
|
|
72
|
+
sessionId: 'sess_test_boundary',
|
|
73
|
+
audience: 'did:web:test-server.example.com',
|
|
74
|
+
nonce: `nonce-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
75
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
76
|
+
createdAt: Math.floor(Date.now() / 1000),
|
|
77
|
+
lastActivity: Math.floor(Date.now() / 1000),
|
|
78
|
+
ttlMinutes: 30,
|
|
79
|
+
identityState: 'anonymous',
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Timestamp Skew Boundary Tests ─────────────────────────────
|
|
85
|
+
|
|
86
|
+
describe('timestamp skew boundaries', () => {
|
|
87
|
+
it('should accept proof at exactly the skew boundary (300s)', async () => {
|
|
88
|
+
// Generate proof first, then use its actual meta.ts to set the clock.
|
|
89
|
+
// This avoids a race where a second boundary is crossed between
|
|
90
|
+
// capturing the timestamp and generating the proof (which would
|
|
91
|
+
// cause a JWS signature mismatch).
|
|
92
|
+
const proof = await generateProof(agentA);
|
|
93
|
+
const proofTimestampMs = proof.meta.ts * 1000;
|
|
94
|
+
|
|
95
|
+
// Clock is exactly SKEW_SECONDS * 1000 ms ahead of the proof timestamp
|
|
96
|
+
const clock = new ControllableClockProvider(proofTimestampMs + SKEW_SECONDS * 1000);
|
|
97
|
+
const { verifier } = makeVerifier(clock);
|
|
98
|
+
|
|
99
|
+
const jwk = getPublicKeyJwk(agentA);
|
|
100
|
+
const result = await verifier.verifyProof(proof, jwk);
|
|
101
|
+
|
|
102
|
+
expect(result.valid).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should reject proof 1ms beyond the skew boundary', async () => {
|
|
106
|
+
const proof = await generateProof(agentA);
|
|
107
|
+
const proofTimestampMs = proof.meta.ts * 1000;
|
|
108
|
+
|
|
109
|
+
// Clock is 1ms beyond the skew window
|
|
110
|
+
const clock = new ControllableClockProvider(proofTimestampMs + SKEW_SECONDS * 1000 + 1);
|
|
111
|
+
const { verifier } = makeVerifier(clock);
|
|
112
|
+
|
|
113
|
+
const jwk = getPublicKeyJwk(agentA);
|
|
114
|
+
const result = await verifier.verifyProof(proof, jwk);
|
|
115
|
+
|
|
116
|
+
expect(result.valid).toBe(false);
|
|
117
|
+
expect(result.reason).toContain('skew');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should accept proof when verifier clock is behind by exactly the skew boundary', async () => {
|
|
121
|
+
// Generate proof with real timestamp (meta.ts ≈ now)
|
|
122
|
+
const proof = await generateProof(agentA);
|
|
123
|
+
const proofTimestampMs = proof.meta.ts * 1000;
|
|
124
|
+
|
|
125
|
+
// Set verifier clock to be exactly SKEW_SECONDS behind the proof
|
|
126
|
+
// This simulates verifying a proof from a clock-ahead agent
|
|
127
|
+
const clock = new ControllableClockProvider(proofTimestampMs - SKEW_SECONDS * 1000);
|
|
128
|
+
const { verifier } = makeVerifier(clock);
|
|
129
|
+
|
|
130
|
+
const jwk = getPublicKeyJwk(agentA);
|
|
131
|
+
const result = await verifier.verifyProof(proof, jwk);
|
|
132
|
+
|
|
133
|
+
expect(result.valid).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should reject proof when verifier clock is behind beyond the skew boundary', async () => {
|
|
137
|
+
const proof = await generateProof(agentA);
|
|
138
|
+
const proofTimestampMs = proof.meta.ts * 1000;
|
|
139
|
+
|
|
140
|
+
// Set verifier clock 1ms further than the boundary
|
|
141
|
+
const clock = new ControllableClockProvider(proofTimestampMs - SKEW_SECONDS * 1000 - 1);
|
|
142
|
+
const { verifier } = makeVerifier(clock);
|
|
143
|
+
|
|
144
|
+
const jwk = getPublicKeyJwk(agentA);
|
|
145
|
+
const result = await verifier.verifyProof(proof, jwk);
|
|
146
|
+
|
|
147
|
+
expect(result.valid).toBe(false);
|
|
148
|
+
expect(result.reason).toContain('skew');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ── Nonce Scoping Tests ───────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
describe('nonce scoping', () => {
|
|
155
|
+
it('should allow same nonce value from different agents (no cross-agent collision)', async () => {
|
|
156
|
+
const sharedNonce = 'shared-nonce-value-12345';
|
|
157
|
+
const clock = new ControllableClockProvider(Date.now());
|
|
158
|
+
const { verifier } = makeVerifier(clock);
|
|
159
|
+
|
|
160
|
+
// Generate proofs for both agents with the same nonce
|
|
161
|
+
const genA = makeGenerator(agentA);
|
|
162
|
+
const genB = makeGenerator(agentB);
|
|
163
|
+
|
|
164
|
+
const session = {
|
|
165
|
+
sessionId: 'sess_nonce_test',
|
|
166
|
+
audience: 'did:web:server.example.com',
|
|
167
|
+
nonce: sharedNonce,
|
|
168
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
169
|
+
createdAt: Math.floor(Date.now() / 1000),
|
|
170
|
+
lastActivity: Math.floor(Date.now() / 1000),
|
|
171
|
+
ttlMinutes: 30,
|
|
172
|
+
identityState: 'anonymous' as const,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const request = { method: 'tools/call', params: { name: 'test' } };
|
|
176
|
+
const response = { data: { result: 'ok' } };
|
|
177
|
+
|
|
178
|
+
const proofA = await genA.generateProof(request, response, session);
|
|
179
|
+
const proofB = await genB.generateProof(request, response, session);
|
|
180
|
+
|
|
181
|
+
const jwkA = getPublicKeyJwk(agentA);
|
|
182
|
+
const jwkB = getPublicKeyJwk(agentB);
|
|
183
|
+
|
|
184
|
+
const resultA = await verifier.verifyProof(proofA, jwkA);
|
|
185
|
+
expect(resultA.valid).toBe(true);
|
|
186
|
+
|
|
187
|
+
// Same nonce, different agent — should also succeed
|
|
188
|
+
const resultB = await verifier.verifyProof(proofB, jwkB);
|
|
189
|
+
expect(resultB.valid).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should reject replay of same nonce from same agent', async () => {
|
|
193
|
+
const clock = new ControllableClockProvider(Date.now());
|
|
194
|
+
const { verifier } = makeVerifier(clock);
|
|
195
|
+
|
|
196
|
+
const proof = await generateProof(agentA);
|
|
197
|
+
const jwk = getPublicKeyJwk(agentA);
|
|
198
|
+
|
|
199
|
+
const result1 = await verifier.verifyProof(proof, jwk);
|
|
200
|
+
expect(result1.valid).toBe(true);
|
|
201
|
+
|
|
202
|
+
// Same exact proof (same nonce, same agent) — should be rejected as replay
|
|
203
|
+
const result2 = await verifier.verifyProof(proof, jwk);
|
|
204
|
+
expect(result2.valid).toBe(false);
|
|
205
|
+
expect(result2.reason).toContain('replay');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ── Wrong Key Tests ───────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
describe('signature verification with wrong key', () => {
|
|
212
|
+
it('should reject proof verified with wrong public key', async () => {
|
|
213
|
+
const clock = new ControllableClockProvider(Date.now());
|
|
214
|
+
const { verifier } = makeVerifier(clock);
|
|
215
|
+
|
|
216
|
+
// Generate proof with agent A
|
|
217
|
+
const proof = await generateProof(agentA);
|
|
218
|
+
|
|
219
|
+
// Try to verify with agent B's public key
|
|
220
|
+
const wrongJwk = getPublicKeyJwk(agentB);
|
|
221
|
+
|
|
222
|
+
const result = await verifier.verifyProof(proof, wrongJwk);
|
|
223
|
+
expect(result.valid).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ── Malformed Proof Tests ─────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
describe('malformed proof rejection', () => {
|
|
230
|
+
it('should reject proof with empty nonce', async () => {
|
|
231
|
+
const clock = new ControllableClockProvider(Date.now());
|
|
232
|
+
const { verifier } = makeVerifier(clock);
|
|
233
|
+
|
|
234
|
+
const proof = await generateProof(agentA);
|
|
235
|
+
proof.meta.nonce = '';
|
|
236
|
+
|
|
237
|
+
const jwk = getPublicKeyJwk(agentA);
|
|
238
|
+
const result = await verifier.verifyProof(proof, jwk);
|
|
239
|
+
|
|
240
|
+
expect(result.valid).toBe(false);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should reject proof with malformed requestHash', async () => {
|
|
244
|
+
const clock = new ControllableClockProvider(Date.now());
|
|
245
|
+
const { verifier } = makeVerifier(clock);
|
|
246
|
+
|
|
247
|
+
const proof = await generateProof(agentA);
|
|
248
|
+
proof.meta.requestHash = 'md5:abc123';
|
|
249
|
+
|
|
250
|
+
const jwk = getPublicKeyJwk(agentA);
|
|
251
|
+
const result = await verifier.verifyProof(proof, jwk);
|
|
252
|
+
|
|
253
|
+
expect(result.valid).toBe(false);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should reject proof with ts=0', async () => {
|
|
257
|
+
const clock = new ControllableClockProvider(Date.now());
|
|
258
|
+
const { verifier } = makeVerifier(clock);
|
|
259
|
+
|
|
260
|
+
const proof = await generateProof(agentA);
|
|
261
|
+
proof.meta.ts = 0;
|
|
262
|
+
|
|
263
|
+
const jwk = getPublicKeyJwk(agentA);
|
|
264
|
+
const result = await verifier.verifyProof(proof, jwk);
|
|
265
|
+
|
|
266
|
+
expect(result.valid).toBe(false);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusList2021 + Bitstring Round-Trip Audit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the full lifecycle: allocate status entry → check (not revoked) →
|
|
5
|
+
* revoke → check (revoked), using real gzip compression and bitstring encoding.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, beforeAll } from 'vitest';
|
|
9
|
+
import type { AgentIdentity } from '../../providers/base.js';
|
|
10
|
+
import {
|
|
11
|
+
createRealCryptoProvider,
|
|
12
|
+
createRealIdentity,
|
|
13
|
+
createRealStatusListManager,
|
|
14
|
+
type RealStatusListSetup,
|
|
15
|
+
} from './helpers/crypto-helpers.js';
|
|
16
|
+
import { NodeCryptoProvider } from '../utils/node-crypto-provider.js';
|
|
17
|
+
|
|
18
|
+
describe('StatusList Bitstring Round-Trip Audit', () => {
|
|
19
|
+
let crypto: NodeCryptoProvider;
|
|
20
|
+
let identity: AgentIdentity;
|
|
21
|
+
let setup: RealStatusListSetup;
|
|
22
|
+
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
crypto = createRealCryptoProvider();
|
|
25
|
+
identity = await createRealIdentity(crypto);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
setup = createRealStatusListManager(crypto, identity);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should allocate, check not revoked, revoke, then check revoked', async () => {
|
|
33
|
+
const statusEntry = await setup.manager.allocateStatusEntry('revocation');
|
|
34
|
+
|
|
35
|
+
// Initially not revoked
|
|
36
|
+
const beforeRevoke = await setup.manager.checkStatus(statusEntry);
|
|
37
|
+
expect(beforeRevoke).toBe(false);
|
|
38
|
+
|
|
39
|
+
// Revoke
|
|
40
|
+
await setup.manager.updateStatus(statusEntry, true);
|
|
41
|
+
|
|
42
|
+
// Now revoked
|
|
43
|
+
const afterRevoke = await setup.manager.checkStatus(statusEntry);
|
|
44
|
+
expect(afterRevoke).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should selectively revoke entries in the same status list', async () => {
|
|
48
|
+
// Allocate 5 entries
|
|
49
|
+
const entries = [];
|
|
50
|
+
for (let i = 0; i < 5; i++) {
|
|
51
|
+
entries.push(await setup.manager.allocateStatusEntry('revocation'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Revoke entries at index 1 and 3
|
|
55
|
+
await setup.manager.updateStatus(entries[1]!, true);
|
|
56
|
+
await setup.manager.updateStatus(entries[3]!, true);
|
|
57
|
+
|
|
58
|
+
// Check all 5
|
|
59
|
+
const statuses = await Promise.all(
|
|
60
|
+
entries.map((e) => setup.manager.checkStatus(e))
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(statuses[0]).toBe(false);
|
|
64
|
+
expect(statuses[1]).toBe(true);
|
|
65
|
+
expect(statuses[2]).toBe(false);
|
|
66
|
+
expect(statuses[3]).toBe(true);
|
|
67
|
+
expect(statuses[4]).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should report revoked indices correctly via getRevokedIndices', async () => {
|
|
71
|
+
const entries = [];
|
|
72
|
+
for (let i = 0; i < 6; i++) {
|
|
73
|
+
entries.push(await setup.manager.allocateStatusEntry('revocation'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Revoke indices 0, 2, 5
|
|
77
|
+
await setup.manager.updateStatus(entries[0]!, true);
|
|
78
|
+
await setup.manager.updateStatus(entries[2]!, true);
|
|
79
|
+
await setup.manager.updateStatus(entries[5]!, true);
|
|
80
|
+
|
|
81
|
+
const statusListId = `${setup.manager.getStatusListBaseUrl()}/revocation/v1`;
|
|
82
|
+
const revoked = await setup.manager.getRevokedIndices(statusListId);
|
|
83
|
+
|
|
84
|
+
expect(revoked).toContain(0);
|
|
85
|
+
expect(revoked).toContain(2);
|
|
86
|
+
expect(revoked).toContain(5);
|
|
87
|
+
expect(revoked).not.toContain(1);
|
|
88
|
+
expect(revoked).not.toContain(3);
|
|
89
|
+
expect(revoked).not.toContain(4);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should re-sign the status list credential after update', async () => {
|
|
93
|
+
const entry = await setup.manager.allocateStatusEntry('revocation');
|
|
94
|
+
|
|
95
|
+
const statusListId = `${setup.manager.getStatusListBaseUrl()}/revocation/v1`;
|
|
96
|
+
const credBefore = await setup.storage.getStatusList(statusListId);
|
|
97
|
+
const proofBefore = credBefore?.proof?.proofValue;
|
|
98
|
+
|
|
99
|
+
// Revoke — triggers re-signing
|
|
100
|
+
await setup.manager.updateStatus(entry, true);
|
|
101
|
+
|
|
102
|
+
const credAfter = await setup.storage.getStatusList(statusListId);
|
|
103
|
+
const proofAfter = credAfter?.proof?.proofValue;
|
|
104
|
+
|
|
105
|
+
expect(proofBefore).toBeDefined();
|
|
106
|
+
expect(proofAfter).toBeDefined();
|
|
107
|
+
expect(proofAfter).not.toBe(proofBefore);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle bits at byte boundaries correctly (index 7 and 8)', async () => {
|
|
111
|
+
// Allocate enough entries to reach index 7 and 8
|
|
112
|
+
const entries = [];
|
|
113
|
+
for (let i = 0; i < 9; i++) {
|
|
114
|
+
entries.push(await setup.manager.allocateStatusEntry('revocation'));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Index 7 = last bit of byte 0, Index 8 = first bit of byte 1
|
|
118
|
+
await setup.manager.updateStatus(entries[7]!, true);
|
|
119
|
+
await setup.manager.updateStatus(entries[8]!, true);
|
|
120
|
+
|
|
121
|
+
expect(await setup.manager.checkStatus(entries[6]!)).toBe(false);
|
|
122
|
+
expect(await setup.manager.checkStatus(entries[7]!)).toBe(true);
|
|
123
|
+
expect(await setup.manager.checkStatus(entries[8]!)).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should un-revoke (restore) an entry by setting status to false', async () => {
|
|
127
|
+
const entry = await setup.manager.allocateStatusEntry('revocation');
|
|
128
|
+
|
|
129
|
+
await setup.manager.updateStatus(entry, true);
|
|
130
|
+
expect(await setup.manager.checkStatus(entry)).toBe(true);
|
|
131
|
+
|
|
132
|
+
await setup.manager.updateStatus(entry, false);
|
|
133
|
+
expect(await setup.manager.checkStatus(entry)).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -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
|
+
});
|