@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
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* delegation context to downstream services.
|
|
6
6
|
*
|
|
7
7
|
* Headers (MCP-I §7):
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
8
|
+
* - KYA-Agent-DID: the original agent's DID
|
|
9
|
+
* - KYA-Delegation-Chain: the delegation chain ID (vcId of the root delegation)
|
|
10
|
+
* - KYA-Session-Id: the current session ID
|
|
11
|
+
* - KYA-Delegation-Proof: a signed JWT proving the delegation is being forwarded
|
|
12
12
|
*
|
|
13
13
|
* Related Spec: MCP-I §7 — Outbound Delegation Propagation
|
|
14
14
|
*/
|
|
@@ -23,10 +23,10 @@ import { logger } from '../logging/index.js';
|
|
|
23
23
|
* Header names for outbound delegation propagation
|
|
24
24
|
*/
|
|
25
25
|
export const OUTBOUND_HEADER_NAMES = {
|
|
26
|
-
AGENT_DID: '
|
|
27
|
-
DELEGATION_CHAIN: '
|
|
28
|
-
SESSION_ID: '
|
|
29
|
-
DELEGATION_PROOF: '
|
|
26
|
+
AGENT_DID: 'KYA-Agent-DID',
|
|
27
|
+
DELEGATION_CHAIN: 'KYA-Delegation-Chain',
|
|
28
|
+
SESSION_ID: 'KYA-Session-Id',
|
|
29
|
+
DELEGATION_PROOF: 'KYA-Delegation-Proof',
|
|
30
30
|
} as const;
|
|
31
31
|
|
|
32
32
|
/**
|
|
@@ -51,10 +51,10 @@ export interface OutboundDelegationContext {
|
|
|
51
51
|
* Outbound delegation headers to attach to downstream requests
|
|
52
52
|
*/
|
|
53
53
|
export interface OutboundDelegationHeaders {
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
57
|
-
'
|
|
54
|
+
'KYA-Agent-DID': string;
|
|
55
|
+
'KYA-Delegation-Chain': string;
|
|
56
|
+
'KYA-Session-Id': string;
|
|
57
|
+
'KYA-Delegation-Proof': string;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
@@ -182,9 +182,9 @@ export async function buildOutboundDelegationHeaders(
|
|
|
182
182
|
});
|
|
183
183
|
|
|
184
184
|
return {
|
|
185
|
-
'
|
|
186
|
-
'
|
|
187
|
-
'
|
|
188
|
-
'
|
|
185
|
+
'KYA-Agent-DID': session.agentDid,
|
|
186
|
+
'KYA-Delegation-Chain': delegation.vcId,
|
|
187
|
+
'KYA-Session-Id': session.sessionId,
|
|
188
|
+
'KYA-Delegation-Proof': jwt,
|
|
189
189
|
};
|
|
190
190
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Enables downstream services to independently verify the delegation chain.
|
|
6
6
|
*
|
|
7
7
|
* Wire format: signed compact EdDSA JWT (60s TTL, per-call jti)
|
|
8
|
-
* Header injection:
|
|
8
|
+
* Header injection: KYA-Delegation-Id, KYA-Delegation-Chain, KYA-Delegation-Proof, KYA-Granted-Scopes
|
|
9
9
|
*
|
|
10
10
|
* Related Spec: MCP-I §2 — Outbound Delegation Propagation
|
|
11
11
|
*/
|
|
@@ -28,6 +28,8 @@ export interface StatusListIdentityProvider {
|
|
|
28
28
|
export class StatusList2021Manager {
|
|
29
29
|
private statusListBaseUrl: string;
|
|
30
30
|
private defaultListSize: number;
|
|
31
|
+
/** Per-status-list mutex to serialize updateStatus calls and prevent race conditions. */
|
|
32
|
+
private updateLocks = new Map<string, Promise<void>>();
|
|
31
33
|
|
|
32
34
|
constructor(
|
|
33
35
|
private storage: StatusListStorageProvider,
|
|
@@ -63,6 +65,21 @@ export class StatusList2021Manager {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
async updateStatus(credentialStatus: CredentialStatus, revoked: boolean): Promise<void> {
|
|
68
|
+
const { statusListCredential } = credentialStatus;
|
|
69
|
+
|
|
70
|
+
// Serialize updates per status list to prevent concurrent read-modify-write races.
|
|
71
|
+
// Each call chains on the previous operation for the same list.
|
|
72
|
+
const previous = this.updateLocks.get(statusListCredential) ?? Promise.resolve();
|
|
73
|
+
const operation = previous.then(() => this.doUpdateStatus(credentialStatus, revoked));
|
|
74
|
+
|
|
75
|
+
// Store a non-rejecting version so the chain continues even if one update fails
|
|
76
|
+
this.updateLocks.set(statusListCredential, operation.catch(() => {}));
|
|
77
|
+
|
|
78
|
+
// Propagate the actual error to the caller
|
|
79
|
+
await operation;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async doUpdateStatus(credentialStatus: CredentialStatus, revoked: boolean): Promise<void> {
|
|
66
83
|
const { statusListCredential, statusListIndex } = credentialStatus;
|
|
67
84
|
|
|
68
85
|
const statusList = await this.storage.getStatusList(statusListCredential);
|
|
@@ -71,11 +71,7 @@ export async function generateIdentity(
|
|
|
71
71
|
*/
|
|
72
72
|
interface McpServerLike {
|
|
73
73
|
connect(transport: Transport): Promise<unknown>;
|
|
74
|
-
registerTool(
|
|
75
|
-
name: string,
|
|
76
|
-
config: Record<string, unknown>,
|
|
77
|
-
handler: (args: unknown) => Promise<unknown>,
|
|
78
|
-
): void;
|
|
74
|
+
registerTool(...args: unknown[]): void;
|
|
79
75
|
}
|
|
80
76
|
|
|
81
77
|
/**
|
|
@@ -85,6 +85,20 @@ export interface MCPIDelegationConfig {
|
|
|
85
85
|
resolveDelegationChain?: (
|
|
86
86
|
leafCredential: DelegationCredential,
|
|
87
87
|
) => Promise<DelegationCredential[]>;
|
|
88
|
+
/**
|
|
89
|
+
* When true, re-delegations (credentials with a `parentId`) MUST include
|
|
90
|
+
* an `audience` constraint binding them to the verifying server's DID.
|
|
91
|
+
*
|
|
92
|
+
* This prevents confused-deputy attacks where a delegated credential is
|
|
93
|
+
* forwarded to an unintended server. Without audience binding, any server
|
|
94
|
+
* that receives the credential will accept it.
|
|
95
|
+
*
|
|
96
|
+
* Recommended for production. See: Alan Karp's transitive access analysis
|
|
97
|
+
* and MCP-I §11.6 (Confused Deputy Attacks).
|
|
98
|
+
*
|
|
99
|
+
* Default is false for backward compatibility.
|
|
100
|
+
*/
|
|
101
|
+
requireAudienceOnRedelegation?: boolean;
|
|
88
102
|
/**
|
|
89
103
|
* Compatibility mode for legacy integrations that cannot yet provide
|
|
90
104
|
* full delegation-chain and status-list resolvers.
|
|
@@ -789,6 +803,21 @@ export function createMCPIMiddleware(
|
|
|
789
803
|
};
|
|
790
804
|
}
|
|
791
805
|
|
|
806
|
+
// When requireAudienceOnRedelegation is enabled, every non-root
|
|
807
|
+
// credential in the chain MUST have an audience constraint.
|
|
808
|
+
// This prevents confused-deputy attacks where re-delegated
|
|
809
|
+
// credentials are forwarded to unintended servers.
|
|
810
|
+
if (
|
|
811
|
+
delegationConfig?.requireAudienceOnRedelegation &&
|
|
812
|
+
delegation.parentId &&
|
|
813
|
+
!delegation.constraints.audience
|
|
814
|
+
) {
|
|
815
|
+
return {
|
|
816
|
+
valid: false,
|
|
817
|
+
reason: `Delegation ${delegation.id} is a re-delegation (parentId: ${delegation.parentId}) but has no audience constraint. Re-delegations must include an audience when requireAudienceOnRedelegation is enabled`,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
792
821
|
if (!previousDelegation || !previousCredential) {
|
|
793
822
|
if (delegation.parentId) {
|
|
794
823
|
return {
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProofVerifier Integration Tests (Real Crypto)
|
|
3
|
+
*
|
|
4
|
+
* Companion to verifier.test.ts — these tests use real Ed25519 signing,
|
|
5
|
+
* real nonce caching, and real clock providers instead of mocking.
|
|
6
|
+
*
|
|
7
|
+
* The mocked unit tests verify pipeline logic and error code propagation.
|
|
8
|
+
* These integration tests verify that real proofs are correctly verified
|
|
9
|
+
* and that security properties hold with actual cryptographic operations.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
|
13
|
+
import { ProofGenerator } from '../generator.js';
|
|
14
|
+
import { ProofVerifier } from '../verifier.js';
|
|
15
|
+
import type { AgentIdentity } from '../../providers/base.js';
|
|
16
|
+
import { extractPublicKeyFromDidKey, publicKeyToJwk } from '../../delegation/did-key-resolver.js';
|
|
17
|
+
import type { Ed25519JWK } from '../../utils/crypto-service.js';
|
|
18
|
+
import {
|
|
19
|
+
createRealCryptoProvider,
|
|
20
|
+
createRealIdentity,
|
|
21
|
+
RealClockProvider,
|
|
22
|
+
RealFetchProvider,
|
|
23
|
+
MemoryNonceCacheProvider,
|
|
24
|
+
} from '../../__tests__/audit/helpers/crypto-helpers.js';
|
|
25
|
+
import { NodeCryptoProvider } from '../../__tests__/utils/node-crypto-provider.js';
|
|
26
|
+
|
|
27
|
+
describe('ProofVerifier (real crypto)', () => {
|
|
28
|
+
let crypto: NodeCryptoProvider;
|
|
29
|
+
let agent: AgentIdentity;
|
|
30
|
+
let otherAgent: AgentIdentity;
|
|
31
|
+
|
|
32
|
+
beforeAll(async () => {
|
|
33
|
+
crypto = createRealCryptoProvider();
|
|
34
|
+
agent = await createRealIdentity(crypto);
|
|
35
|
+
otherAgent = await createRealIdentity(crypto);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function makeVerifier(): { verifier: ProofVerifier; nonceCache: MemoryNonceCacheProvider } {
|
|
39
|
+
const nonceCache = new MemoryNonceCacheProvider();
|
|
40
|
+
const verifier = new ProofVerifier({
|
|
41
|
+
cryptoProvider: crypto,
|
|
42
|
+
clockProvider: new RealClockProvider(),
|
|
43
|
+
nonceCacheProvider: nonceCache,
|
|
44
|
+
fetchProvider: new RealFetchProvider(),
|
|
45
|
+
timestampSkewSeconds: 300,
|
|
46
|
+
});
|
|
47
|
+
return { verifier, nonceCache };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getJwk(identity: AgentIdentity): Ed25519JWK {
|
|
51
|
+
const raw = extractPublicKeyFromDidKey(identity.did);
|
|
52
|
+
const jwk = publicKeyToJwk(raw!);
|
|
53
|
+
jwk.kid = identity.kid;
|
|
54
|
+
return jwk as Ed25519JWK;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function generateProof(identity: AgentIdentity) {
|
|
58
|
+
const gen = new ProofGenerator(
|
|
59
|
+
{ did: identity.did, kid: identity.kid, privateKey: identity.privateKey, publicKey: identity.publicKey },
|
|
60
|
+
crypto
|
|
61
|
+
);
|
|
62
|
+
return gen.generateProof(
|
|
63
|
+
{ method: 'tools/call', params: { name: 'test-tool' } },
|
|
64
|
+
{ data: { output: 'result' } },
|
|
65
|
+
{
|
|
66
|
+
sessionId: 'sess_integration',
|
|
67
|
+
audience: 'did:web:server.example.com',
|
|
68
|
+
nonce: `nonce-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
69
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
70
|
+
createdAt: Math.floor(Date.now() / 1000),
|
|
71
|
+
lastActivity: Math.floor(Date.now() / 1000),
|
|
72
|
+
ttlMinutes: 30,
|
|
73
|
+
identityState: 'anonymous',
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Core Verification ─────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
it('should verify a valid proof with real Ed25519 signature', async () => {
|
|
81
|
+
const { verifier } = makeVerifier();
|
|
82
|
+
const proof = await generateProof(agent);
|
|
83
|
+
const jwk = getJwk(agent);
|
|
84
|
+
|
|
85
|
+
const result = await verifier.verifyProof(proof, jwk);
|
|
86
|
+
|
|
87
|
+
expect(result.valid).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should reject proof signed by a different key', async () => {
|
|
91
|
+
const { verifier } = makeVerifier();
|
|
92
|
+
const proof = await generateProof(agent);
|
|
93
|
+
const wrongJwk = getJwk(otherAgent);
|
|
94
|
+
|
|
95
|
+
const result = await verifier.verifyProof(proof, wrongJwk);
|
|
96
|
+
|
|
97
|
+
expect(result.valid).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ── Nonce Replay (real cache) ─────────────────────────────────
|
|
101
|
+
|
|
102
|
+
it('should prevent nonce replay with real MemoryNonceCacheProvider', async () => {
|
|
103
|
+
const { verifier } = makeVerifier();
|
|
104
|
+
const proof = await generateProof(agent);
|
|
105
|
+
const jwk = getJwk(agent);
|
|
106
|
+
|
|
107
|
+
const first = await verifier.verifyProof(proof, jwk);
|
|
108
|
+
expect(first.valid).toBe(true);
|
|
109
|
+
|
|
110
|
+
const replay = await verifier.verifyProof(proof, jwk);
|
|
111
|
+
expect(replay.valid).toBe(false);
|
|
112
|
+
expect(replay.reason).toContain('replay');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should scope nonces per agent DID', async () => {
|
|
116
|
+
const { verifier } = makeVerifier();
|
|
117
|
+
|
|
118
|
+
const proofA = await generateProof(agent);
|
|
119
|
+
const proofB = await generateProof(otherAgent);
|
|
120
|
+
const jwkA = getJwk(agent);
|
|
121
|
+
const jwkB = getJwk(otherAgent);
|
|
122
|
+
|
|
123
|
+
const resultA = await verifier.verifyProof(proofA, jwkA);
|
|
124
|
+
const resultB = await verifier.verifyProof(proofB, jwkB);
|
|
125
|
+
|
|
126
|
+
expect(resultA.valid).toBe(true);
|
|
127
|
+
expect(resultB.valid).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ── Detached Verification ─────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
it('should verify proof via verifyProofDetached with string payload', async () => {
|
|
133
|
+
const { verifier } = makeVerifier();
|
|
134
|
+
const proof = await generateProof(agent);
|
|
135
|
+
const jwk = getJwk(agent);
|
|
136
|
+
const canonical = verifier.buildCanonicalPayload(proof.meta);
|
|
137
|
+
|
|
138
|
+
const result = await verifier.verifyProofDetached(proof, canonical, jwk);
|
|
139
|
+
|
|
140
|
+
expect(result.valid).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should verify proof via verifyProofDetached with Uint8Array payload', async () => {
|
|
144
|
+
const { verifier } = makeVerifier();
|
|
145
|
+
const proof = await generateProof(agent);
|
|
146
|
+
const jwk = getJwk(agent);
|
|
147
|
+
const canonical = new TextEncoder().encode(verifier.buildCanonicalPayload(proof.meta));
|
|
148
|
+
|
|
149
|
+
const result = await verifier.verifyProofDetached(proof, canonical, jwk);
|
|
150
|
+
|
|
151
|
+
expect(result.valid).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ── Proof Structure Rejection ─────────────────────────────────
|
|
155
|
+
|
|
156
|
+
it('should reject malformed proof structure', async () => {
|
|
157
|
+
const { verifier } = makeVerifier();
|
|
158
|
+
const jwk = getJwk(agent);
|
|
159
|
+
|
|
160
|
+
const result = await verifier.verifyProof(
|
|
161
|
+
{ jws: 'not-valid', meta: { did: 'x' } } as any,
|
|
162
|
+
jwk
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(result.valid).toBe(false);
|
|
166
|
+
expect(result.reason).toContain('Invalid proof structure');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ── fetchPublicKeyFromDID (real DID resolution) ───────────────
|
|
170
|
+
|
|
171
|
+
it('should resolve a real did:key to a valid Ed25519 JWK', async () => {
|
|
172
|
+
const { verifier } = makeVerifier();
|
|
173
|
+
|
|
174
|
+
const jwk = await verifier.fetchPublicKeyFromDID(agent.did);
|
|
175
|
+
|
|
176
|
+
expect(jwk).toBeDefined();
|
|
177
|
+
expect(jwk!.kty).toBe('OKP');
|
|
178
|
+
expect(jwk!.crv).toBe('Ed25519');
|
|
179
|
+
expect(jwk!.x).toBeTruthy();
|
|
180
|
+
});
|
|
181
|
+
});
|