@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,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VC Issuer Integration Tests (Real Crypto)
|
|
3
|
+
*
|
|
4
|
+
* Companion to vc-issuer.test.ts — these tests use real Ed25519 signing
|
|
5
|
+
* instead of mocking wrapDelegationAsVC and canonicalizeJSON.
|
|
6
|
+
*
|
|
7
|
+
* The mocked unit tests verify argument passing and error propagation.
|
|
8
|
+
* These integration tests verify that issued VCs are structurally valid
|
|
9
|
+
* and cryptographically verifiable.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
13
|
+
import {
|
|
14
|
+
DelegationCredentialIssuer,
|
|
15
|
+
createDelegationIssuer,
|
|
16
|
+
} from '../vc-issuer.js';
|
|
17
|
+
import { DelegationCredentialVerifier } from '../vc-verifier.js';
|
|
18
|
+
import { createDidKeyResolver } from '../did-key-resolver.js';
|
|
19
|
+
import { canonicalizeJSON } from '../utils.js';
|
|
20
|
+
import type { AgentIdentity } from '../../providers/base.js';
|
|
21
|
+
import type { DelegationRecord } from '../../types/protocol.js';
|
|
22
|
+
import {
|
|
23
|
+
createRealCryptoProvider,
|
|
24
|
+
createRealIdentity,
|
|
25
|
+
createRealSigningFunction,
|
|
26
|
+
createRealSignatureVerifier,
|
|
27
|
+
} from '../../__tests__/audit/helpers/crypto-helpers.js';
|
|
28
|
+
import { NodeCryptoProvider } from '../../__tests__/utils/node-crypto-provider.js';
|
|
29
|
+
|
|
30
|
+
describe('DelegationCredentialIssuer (real crypto)', () => {
|
|
31
|
+
let crypto: NodeCryptoProvider;
|
|
32
|
+
let issuerIdentity: AgentIdentity;
|
|
33
|
+
let subjectIdentity: AgentIdentity;
|
|
34
|
+
let issuer: DelegationCredentialIssuer;
|
|
35
|
+
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
crypto = createRealCryptoProvider();
|
|
38
|
+
issuerIdentity = await createRealIdentity(crypto);
|
|
39
|
+
subjectIdentity = await createRealIdentity(crypto);
|
|
40
|
+
|
|
41
|
+
issuer = createDelegationIssuer(
|
|
42
|
+
{
|
|
43
|
+
getDid: () => issuerIdentity.did,
|
|
44
|
+
getKeyId: () => issuerIdentity.kid,
|
|
45
|
+
getPrivateKey: () => issuerIdentity.privateKey,
|
|
46
|
+
},
|
|
47
|
+
createRealSigningFunction(crypto, issuerIdentity)
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function makeDelegation(overrides?: Partial<DelegationRecord>): DelegationRecord {
|
|
52
|
+
return {
|
|
53
|
+
id: 'del-integration-001',
|
|
54
|
+
issuerDid: issuerIdentity.did,
|
|
55
|
+
subjectDid: subjectIdentity.did,
|
|
56
|
+
vcId: 'urn:uuid:integration-vc-001',
|
|
57
|
+
constraints: { scopes: ['tools:read'] },
|
|
58
|
+
signature: '',
|
|
59
|
+
status: 'active',
|
|
60
|
+
createdAt: Date.now(),
|
|
61
|
+
...overrides,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
it('should produce a VC with valid W3C structure', async () => {
|
|
66
|
+
const vc = await issuer.issueDelegationCredential(makeDelegation());
|
|
67
|
+
|
|
68
|
+
expect(vc['@context']).toContain('https://www.w3.org/2018/credentials/v1');
|
|
69
|
+
expect(vc.type).toContain('VerifiableCredential');
|
|
70
|
+
expect(vc.type).toContain('DelegationCredential');
|
|
71
|
+
expect(vc.issuer).toBe(issuerIdentity.did);
|
|
72
|
+
expect(vc.credentialSubject.id).toBe(subjectIdentity.did);
|
|
73
|
+
expect(vc.proof).toBeDefined();
|
|
74
|
+
expect(vc.proof?.type).toBe('Ed25519Signature2020');
|
|
75
|
+
expect(vc.proof?.proofValue).toBeTruthy();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should produce a VC that passes real signature verification', async () => {
|
|
79
|
+
const vc = await issuer.issueDelegationCredential(makeDelegation());
|
|
80
|
+
|
|
81
|
+
const verifier = new DelegationCredentialVerifier({
|
|
82
|
+
didResolver: createDidKeyResolver(),
|
|
83
|
+
signatureVerifier: createRealSignatureVerifier(crypto),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const result = await verifier.verifyDelegationCredential(vc, {
|
|
87
|
+
skipStatus: true,
|
|
88
|
+
skipCache: true,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.valid).toBe(true);
|
|
92
|
+
expect(result.checks?.signatureValid).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should produce deterministic canonicalization for the same input', async () => {
|
|
96
|
+
const delegation = makeDelegation();
|
|
97
|
+
const vc1 = await issuer.issueDelegationCredential(delegation);
|
|
98
|
+
const vc2 = await issuer.issueDelegationCredential(delegation);
|
|
99
|
+
|
|
100
|
+
const strip = (vc: Record<string, unknown>) => {
|
|
101
|
+
const copy = { ...vc };
|
|
102
|
+
delete copy['proof'];
|
|
103
|
+
return copy;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
expect(canonicalizeJSON(strip(vc1))).toBe(canonicalizeJSON(strip(vc2)));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should produce different signatures for different delegations', async () => {
|
|
110
|
+
const vc1 = await issuer.issueDelegationCredential(makeDelegation({ id: 'del-A' }));
|
|
111
|
+
const vc2 = await issuer.issueDelegationCredential(makeDelegation({ id: 'del-B' }));
|
|
112
|
+
|
|
113
|
+
expect(vc1.proof?.proofValue).not.toBe(vc2.proof?.proofValue);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('createAndIssueDelegation should produce a verifiable VC', async () => {
|
|
117
|
+
const vc = await issuer.createAndIssueDelegation({
|
|
118
|
+
id: 'del-create-issue',
|
|
119
|
+
issuerDid: issuerIdentity.did,
|
|
120
|
+
subjectDid: subjectIdentity.did,
|
|
121
|
+
constraints: { scopes: ['tools:write'] },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const verifier = new DelegationCredentialVerifier({
|
|
125
|
+
didResolver: createDidKeyResolver(),
|
|
126
|
+
signatureVerifier: createRealSignatureVerifier(crypto),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const result = await verifier.verifyDelegationCredential(vc, {
|
|
130
|
+
skipStatus: true,
|
|
131
|
+
skipCache: true,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(result.valid).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VC-JWT Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for createUnsignedVCJWT, completeVCJWT, and parseVCJWT — the public
|
|
5
|
+
* API used by mcp-i-cloudflare's consent service to issue and verify
|
|
6
|
+
* delegation tokens as W3C VC-JWTs.
|
|
7
|
+
*
|
|
8
|
+
* These functions had 0% test coverage in mcp-i-core despite being
|
|
9
|
+
* load-bearing public API consumed by production services.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
13
|
+
import {
|
|
14
|
+
createUnsignedVCJWT,
|
|
15
|
+
completeVCJWT,
|
|
16
|
+
parseVCJWT,
|
|
17
|
+
type VCJWTHeader,
|
|
18
|
+
type VCJWTPayload,
|
|
19
|
+
} from '../utils.js';
|
|
20
|
+
import { wrapDelegationAsVC } from '../../types/protocol.js';
|
|
21
|
+
import type { DelegationRecord } from '../../types/protocol.js';
|
|
22
|
+
import type { AgentIdentity } from '../../providers/base.js';
|
|
23
|
+
import {
|
|
24
|
+
createRealCryptoProvider,
|
|
25
|
+
createRealIdentity,
|
|
26
|
+
} from '../../__tests__/audit/helpers/crypto-helpers.js';
|
|
27
|
+
import { NodeCryptoProvider } from '../../__tests__/utils/node-crypto-provider.js';
|
|
28
|
+
import { base64urlEncodeFromBytes } from '../../utils/base64.js';
|
|
29
|
+
|
|
30
|
+
describe('VC-JWT', () => {
|
|
31
|
+
let crypto: NodeCryptoProvider;
|
|
32
|
+
let issuer: AgentIdentity;
|
|
33
|
+
let subject: AgentIdentity;
|
|
34
|
+
|
|
35
|
+
beforeAll(async () => {
|
|
36
|
+
crypto = createRealCryptoProvider();
|
|
37
|
+
issuer = await createRealIdentity(crypto);
|
|
38
|
+
subject = await createRealIdentity(crypto);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function makeDelegation(): DelegationRecord {
|
|
42
|
+
return {
|
|
43
|
+
id: 'del-jwt-test',
|
|
44
|
+
issuerDid: issuer.did,
|
|
45
|
+
subjectDid: subject.did,
|
|
46
|
+
vcId: 'urn:uuid:jwt-test-001',
|
|
47
|
+
constraints: { scopes: ['tools:read', 'tools:write'] },
|
|
48
|
+
signature: '',
|
|
49
|
+
status: 'active',
|
|
50
|
+
createdAt: Date.now(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function makeVC(delegation?: DelegationRecord) {
|
|
55
|
+
return wrapDelegationAsVC(delegation ?? makeDelegation());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── createUnsignedVCJWT ───────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
describe('createUnsignedVCJWT', () => {
|
|
61
|
+
it('should produce EdDSA header with JWT type', () => {
|
|
62
|
+
const { header } = createUnsignedVCJWT(makeVC() as Record<string, unknown>);
|
|
63
|
+
|
|
64
|
+
expect(header.alg).toBe('EdDSA');
|
|
65
|
+
expect(header.typ).toBe('JWT');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should include kid in header when provided', () => {
|
|
69
|
+
const { header } = createUnsignedVCJWT(
|
|
70
|
+
makeVC() as Record<string, unknown>,
|
|
71
|
+
{ keyId: issuer.kid }
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(header.kid).toBe(issuer.kid);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should not include kid when not provided', () => {
|
|
78
|
+
const { header } = createUnsignedVCJWT(makeVC() as Record<string, unknown>);
|
|
79
|
+
|
|
80
|
+
expect(header.kid).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should extract issuer DID as iss claim', () => {
|
|
84
|
+
const { payload } = createUnsignedVCJWT(makeVC() as Record<string, unknown>);
|
|
85
|
+
|
|
86
|
+
expect(payload.iss).toBe(issuer.did);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should extract issuer from object-form issuer', () => {
|
|
90
|
+
const vc = makeVC() as Record<string, unknown>;
|
|
91
|
+
vc['issuer'] = { id: 'did:web:example.com' };
|
|
92
|
+
|
|
93
|
+
const { payload } = createUnsignedVCJWT(vc);
|
|
94
|
+
|
|
95
|
+
expect(payload.iss).toBe('did:web:example.com');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should extract subject DID as sub claim', () => {
|
|
99
|
+
const { payload } = createUnsignedVCJWT(makeVC() as Record<string, unknown>);
|
|
100
|
+
|
|
101
|
+
expect(payload.sub).toBe(subject.did);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should set jti from VC id', () => {
|
|
105
|
+
const { payload } = createUnsignedVCJWT(makeVC() as Record<string, unknown>);
|
|
106
|
+
|
|
107
|
+
expect(payload.jti).toBe('urn:uuid:jwt-test-001');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should convert expirationDate to exp (unix seconds)', () => {
|
|
111
|
+
const delegation = makeDelegation();
|
|
112
|
+
delegation.constraints.notAfter = Math.floor(Date.now() / 1000) + 3600;
|
|
113
|
+
const vc = makeVC(delegation) as Record<string, unknown>;
|
|
114
|
+
|
|
115
|
+
const { payload } = createUnsignedVCJWT(vc);
|
|
116
|
+
|
|
117
|
+
expect(payload.exp).toBeDefined();
|
|
118
|
+
expect(payload.exp).toBe(delegation.constraints.notAfter);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should convert issuanceDate to iat (unix seconds)', () => {
|
|
122
|
+
const vc = makeVC() as Record<string, unknown>;
|
|
123
|
+
const { payload } = createUnsignedVCJWT(vc);
|
|
124
|
+
|
|
125
|
+
expect(payload.iat).toBeDefined();
|
|
126
|
+
expect(typeof payload.iat).toBe('number');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should embed the VC without proof in the vc claim', () => {
|
|
130
|
+
const vc = makeVC() as Record<string, unknown>;
|
|
131
|
+
vc['proof'] = { type: 'Ed25519Signature2020', proofValue: 'should-be-stripped' };
|
|
132
|
+
|
|
133
|
+
const { payload } = createUnsignedVCJWT(vc);
|
|
134
|
+
|
|
135
|
+
expect(payload.vc).toBeDefined();
|
|
136
|
+
expect(payload.vc['proof']).toBeUndefined();
|
|
137
|
+
expect(payload.vc['@context']).toBeDefined();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should produce a valid base64url-encoded signingInput', () => {
|
|
141
|
+
const { signingInput, encodedHeader, encodedPayload } = createUnsignedVCJWT(
|
|
142
|
+
makeVC() as Record<string, unknown>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(signingInput).toBe(`${encodedHeader}.${encodedPayload}`);
|
|
146
|
+
// base64url: no +, /, or = characters
|
|
147
|
+
expect(signingInput).not.toMatch(/[+/=]/);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should produce decodable header and payload', () => {
|
|
151
|
+
const { encodedHeader, encodedPayload } = createUnsignedVCJWT(
|
|
152
|
+
makeVC() as Record<string, unknown>
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const header = JSON.parse(atob(encodedHeader.replace(/-/g, '+').replace(/_/g, '/')));
|
|
156
|
+
const payload = JSON.parse(atob(encodedPayload.replace(/-/g, '+').replace(/_/g, '/')));
|
|
157
|
+
|
|
158
|
+
expect(header.alg).toBe('EdDSA');
|
|
159
|
+
expect(payload.iss).toBe(issuer.did);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ── completeVCJWT ─────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
describe('completeVCJWT', () => {
|
|
166
|
+
it('should append signature to signingInput', () => {
|
|
167
|
+
const { signingInput } = createUnsignedVCJWT(makeVC() as Record<string, unknown>);
|
|
168
|
+
const jwt = completeVCJWT(signingInput, 'fake-signature');
|
|
169
|
+
|
|
170
|
+
expect(jwt).toBe(`${signingInput}.fake-signature`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should produce a 3-part JWT string', () => {
|
|
174
|
+
const { signingInput } = createUnsignedVCJWT(makeVC() as Record<string, unknown>);
|
|
175
|
+
const jwt = completeVCJWT(signingInput, 'sig123');
|
|
176
|
+
|
|
177
|
+
const parts = jwt.split('.');
|
|
178
|
+
expect(parts.length).toBe(3);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ── parseVCJWT ────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
describe('parseVCJWT', () => {
|
|
185
|
+
it('should parse a valid VC-JWT round-trip', () => {
|
|
186
|
+
const vc = makeVC() as Record<string, unknown>;
|
|
187
|
+
const { signingInput, header: originalHeader, payload: originalPayload } =
|
|
188
|
+
createUnsignedVCJWT(vc, { keyId: issuer.kid });
|
|
189
|
+
const jwt = completeVCJWT(signingInput, 'test-signature');
|
|
190
|
+
|
|
191
|
+
const parsed = parseVCJWT(jwt);
|
|
192
|
+
|
|
193
|
+
expect(parsed).not.toBeNull();
|
|
194
|
+
expect(parsed!.header.alg).toBe(originalHeader.alg);
|
|
195
|
+
expect(parsed!.header.kid).toBe(originalHeader.kid);
|
|
196
|
+
expect(parsed!.payload.iss).toBe(originalPayload.iss);
|
|
197
|
+
expect(parsed!.payload.sub).toBe(originalPayload.sub);
|
|
198
|
+
expect(parsed!.payload.jti).toBe(originalPayload.jti);
|
|
199
|
+
expect(parsed!.payload.vc).toBeDefined();
|
|
200
|
+
expect(parsed!.signature).toBe('test-signature');
|
|
201
|
+
expect(parsed!.signingInput).toBe(signingInput);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should return null for non-3-part string', () => {
|
|
205
|
+
expect(parseVCJWT('only-one-part')).toBeNull();
|
|
206
|
+
expect(parseVCJWT('two.parts')).toBeNull();
|
|
207
|
+
expect(parseVCJWT('four.parts.here.extra')).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should return null for invalid base64url in header', () => {
|
|
211
|
+
expect(parseVCJWT('!!!invalid.payload.sig')).toBeNull();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should return null for invalid JSON in header', () => {
|
|
215
|
+
const notJson = btoa('not json').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
216
|
+
expect(parseVCJWT(`${notJson}.${notJson}.sig`)).toBeNull();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should return null for empty string', () => {
|
|
220
|
+
expect(parseVCJWT('')).toBeNull();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should preserve signingInput for verification', () => {
|
|
224
|
+
const { signingInput } = createUnsignedVCJWT(makeVC() as Record<string, unknown>);
|
|
225
|
+
const jwt = completeVCJWT(signingInput, 'sig');
|
|
226
|
+
|
|
227
|
+
const parsed = parseVCJWT(jwt);
|
|
228
|
+
expect(parsed!.signingInput).toBe(signingInput);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ── Full Round-Trip with Real Crypto ──────────────────────────
|
|
233
|
+
|
|
234
|
+
describe('full round-trip with real Ed25519 signing', () => {
|
|
235
|
+
it('should create → sign → parse → verify a VC-JWT', async () => {
|
|
236
|
+
// Step 1: Create unsigned JWT (same as consent.service.ts)
|
|
237
|
+
const vc = makeVC() as Record<string, unknown>;
|
|
238
|
+
const { signingInput } = createUnsignedVCJWT(vc, { keyId: issuer.kid });
|
|
239
|
+
|
|
240
|
+
// Step 2: Sign with real Ed25519
|
|
241
|
+
const signingInputBytes = new TextEncoder().encode(signingInput);
|
|
242
|
+
const signatureBytes = await crypto.sign(signingInputBytes, issuer.privateKey);
|
|
243
|
+
const signature = base64urlEncodeFromBytes(new Uint8Array(signatureBytes));
|
|
244
|
+
|
|
245
|
+
// Step 3: Complete the JWT
|
|
246
|
+
const jwt = completeVCJWT(signingInput, signature);
|
|
247
|
+
|
|
248
|
+
// Step 4: Parse it back
|
|
249
|
+
const parsed = parseVCJWT(jwt);
|
|
250
|
+
expect(parsed).not.toBeNull();
|
|
251
|
+
expect(parsed!.header.alg).toBe('EdDSA');
|
|
252
|
+
expect(parsed!.payload.iss).toBe(issuer.did);
|
|
253
|
+
|
|
254
|
+
// Step 5: Verify the signature
|
|
255
|
+
const verifyInput = new TextEncoder().encode(parsed!.signingInput);
|
|
256
|
+
// Decode base64url signature back to bytes
|
|
257
|
+
const sigBase64 = parsed!.signature.replace(/-/g, '+').replace(/_/g, '/');
|
|
258
|
+
const sigPadded = sigBase64 + '='.repeat((4 - sigBase64.length % 4) % 4);
|
|
259
|
+
const sigBuf = Uint8Array.from(atob(sigPadded), c => c.charCodeAt(0));
|
|
260
|
+
|
|
261
|
+
const isValid = await crypto.verify(verifyInput, sigBuf, issuer.publicKey);
|
|
262
|
+
expect(isValid).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should reject tampered JWT via signature verification', async () => {
|
|
266
|
+
const vc = makeVC() as Record<string, unknown>;
|
|
267
|
+
const { signingInput } = createUnsignedVCJWT(vc, { keyId: issuer.kid });
|
|
268
|
+
|
|
269
|
+
const signatureBytes = await crypto.sign(
|
|
270
|
+
new TextEncoder().encode(signingInput),
|
|
271
|
+
issuer.privateKey
|
|
272
|
+
);
|
|
273
|
+
const signature = base64urlEncodeFromBytes(new Uint8Array(signatureBytes));
|
|
274
|
+
const jwt = completeVCJWT(signingInput, signature);
|
|
275
|
+
|
|
276
|
+
// Tamper: replace the payload portion
|
|
277
|
+
const parts = jwt.split('.');
|
|
278
|
+
const tamperedPayload = btoa('{"iss":"did:key:evil","vc":{}}')
|
|
279
|
+
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
280
|
+
const tamperedJwt = `${parts[0]}.${tamperedPayload}.${parts[2]}`;
|
|
281
|
+
|
|
282
|
+
const parsed = parseVCJWT(tamperedJwt);
|
|
283
|
+
expect(parsed).not.toBeNull();
|
|
284
|
+
|
|
285
|
+
// Signature should NOT verify against tampered content
|
|
286
|
+
const verifyInput = new TextEncoder().encode(parsed!.signingInput);
|
|
287
|
+
const sigBase64 = parsed!.signature.replace(/-/g, '+').replace(/_/g, '/');
|
|
288
|
+
const sigPadded = sigBase64 + '='.repeat((4 - sigBase64.length % 4) % 4);
|
|
289
|
+
const sigBuf = Uint8Array.from(atob(sigPadded), c => c.charCodeAt(0));
|
|
290
|
+
|
|
291
|
+
const isValid = await crypto.verify(verifyInput, sigBuf, issuer.publicKey);
|
|
292
|
+
expect(isValid).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should reject JWT signed by wrong key', async () => {
|
|
296
|
+
const vc = makeVC() as Record<string, unknown>;
|
|
297
|
+
const { signingInput } = createUnsignedVCJWT(vc);
|
|
298
|
+
|
|
299
|
+
// Sign with issuer's key
|
|
300
|
+
const signatureBytes = await crypto.sign(
|
|
301
|
+
new TextEncoder().encode(signingInput),
|
|
302
|
+
issuer.privateKey
|
|
303
|
+
);
|
|
304
|
+
const signature = base64urlEncodeFromBytes(new Uint8Array(signatureBytes));
|
|
305
|
+
const jwt = completeVCJWT(signingInput, signature);
|
|
306
|
+
|
|
307
|
+
// Verify with subject's key — should fail
|
|
308
|
+
const parsed = parseVCJWT(jwt);
|
|
309
|
+
const verifyInput = new TextEncoder().encode(parsed!.signingInput);
|
|
310
|
+
const sigBase64 = parsed!.signature.replace(/-/g, '+').replace(/_/g, '/');
|
|
311
|
+
const sigPadded = sigBase64 + '='.repeat((4 - sigBase64.length % 4) % 4);
|
|
312
|
+
const sigBuf = Uint8Array.from(atob(sigPadded), c => c.charCodeAt(0));
|
|
313
|
+
|
|
314
|
+
const isValid = await crypto.verify(verifyInput, sigBuf, subject.publicKey);
|
|
315
|
+
expect(isValid).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VC Verifier Integration Tests (Real Crypto)
|
|
3
|
+
*
|
|
4
|
+
* Companion to vc-verifier.test.ts — these tests use real Ed25519 signatures
|
|
5
|
+
* and real validation functions instead of mocking the verification pipeline.
|
|
6
|
+
*
|
|
7
|
+
* The mocked unit tests verify pipeline logic and error paths.
|
|
8
|
+
* These integration tests verify that real VCs are correctly accepted/rejected.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
12
|
+
import {
|
|
13
|
+
DelegationCredentialVerifier,
|
|
14
|
+
type DIDResolver,
|
|
15
|
+
} from '../vc-verifier.js';
|
|
16
|
+
import { DelegationCredentialIssuer } from '../vc-issuer.js';
|
|
17
|
+
import { createDidKeyResolver } from '../did-key-resolver.js';
|
|
18
|
+
import type { AgentIdentity } from '../../providers/base.js';
|
|
19
|
+
import type { DelegationRecord, DelegationCredential } from '../../types/protocol.js';
|
|
20
|
+
import {
|
|
21
|
+
createRealCryptoProvider,
|
|
22
|
+
createRealIdentity,
|
|
23
|
+
createRealSigningFunction,
|
|
24
|
+
createRealSignatureVerifier,
|
|
25
|
+
} from '../../__tests__/audit/helpers/crypto-helpers.js';
|
|
26
|
+
import { NodeCryptoProvider } from '../../__tests__/utils/node-crypto-provider.js';
|
|
27
|
+
|
|
28
|
+
describe('DelegationCredentialVerifier (real crypto)', () => {
|
|
29
|
+
let crypto: NodeCryptoProvider;
|
|
30
|
+
let issuerIdentity: AgentIdentity;
|
|
31
|
+
let subjectIdentity: AgentIdentity;
|
|
32
|
+
let issuer: DelegationCredentialIssuer;
|
|
33
|
+
let verifier: DelegationCredentialVerifier;
|
|
34
|
+
let didResolver: DIDResolver;
|
|
35
|
+
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
crypto = createRealCryptoProvider();
|
|
38
|
+
issuerIdentity = await createRealIdentity(crypto);
|
|
39
|
+
subjectIdentity = await createRealIdentity(crypto);
|
|
40
|
+
|
|
41
|
+
didResolver = createDidKeyResolver();
|
|
42
|
+
|
|
43
|
+
issuer = new DelegationCredentialIssuer(
|
|
44
|
+
{
|
|
45
|
+
getDid: () => issuerIdentity.did,
|
|
46
|
+
getKeyId: () => issuerIdentity.kid,
|
|
47
|
+
getPrivateKey: () => issuerIdentity.privateKey,
|
|
48
|
+
},
|
|
49
|
+
createRealSigningFunction(crypto, issuerIdentity)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
verifier = new DelegationCredentialVerifier({
|
|
53
|
+
didResolver,
|
|
54
|
+
signatureVerifier: createRealSignatureVerifier(crypto),
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
async function issueVC(overrides?: Partial<DelegationRecord>): Promise<DelegationCredential> {
|
|
59
|
+
return issuer.issueDelegationCredential({
|
|
60
|
+
id: 'del-verifier-test',
|
|
61
|
+
issuerDid: issuerIdentity.did,
|
|
62
|
+
subjectDid: subjectIdentity.did,
|
|
63
|
+
vcId: `urn:uuid:verifier-test-${Date.now()}`,
|
|
64
|
+
constraints: {
|
|
65
|
+
scopes: ['tools:read'],
|
|
66
|
+
notBefore: Math.floor(Date.now() / 1000) - 3600,
|
|
67
|
+
notAfter: Math.floor(Date.now() / 1000) + 3600,
|
|
68
|
+
},
|
|
69
|
+
signature: '',
|
|
70
|
+
status: 'active',
|
|
71
|
+
createdAt: Date.now(),
|
|
72
|
+
...overrides,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Basic Validation (real validateDelegationCredential) ──────
|
|
77
|
+
|
|
78
|
+
it('should accept a valid VC through all stages', async () => {
|
|
79
|
+
const vc = await issueVC();
|
|
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
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should reject an expired VC via real expiry check', async () => {
|
|
93
|
+
const vc = await issueVC({
|
|
94
|
+
constraints: {
|
|
95
|
+
scopes: ['tools:read'],
|
|
96
|
+
notAfter: Math.floor(Date.now() / 1000) - 60, // expired 1 minute ago
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const result = await verifier.verifyDelegationCredential(vc, {
|
|
101
|
+
skipStatus: true,
|
|
102
|
+
skipCache: true,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(result.valid).toBe(false);
|
|
106
|
+
expect(result.stage).toBe('basic');
|
|
107
|
+
expect(result.reason).toContain('expired');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should reject a VC with revoked status field', async () => {
|
|
111
|
+
const vc = await issueVC({ status: 'revoked' });
|
|
112
|
+
|
|
113
|
+
const result = await verifier.verifyDelegationCredential(vc, {
|
|
114
|
+
skipStatus: true,
|
|
115
|
+
skipCache: true,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(result.valid).toBe(false);
|
|
119
|
+
expect(result.reason).toContain('revoked');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ── Signature Verification (real Ed25519) ─────────────────────
|
|
123
|
+
|
|
124
|
+
it('should reject a tampered VC via real signature verification', async () => {
|
|
125
|
+
const vc = await issueVC();
|
|
126
|
+
const tampered = structuredClone(vc);
|
|
127
|
+
tampered.credentialSubject.delegation.scopes = ['admin:*'];
|
|
128
|
+
|
|
129
|
+
const result = await verifier.verifyDelegationCredential(tampered, {
|
|
130
|
+
skipStatus: true,
|
|
131
|
+
skipCache: true,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(result.valid).toBe(false);
|
|
135
|
+
expect(result.checks?.signatureValid).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should reject when issuer DID is swapped', async () => {
|
|
139
|
+
const vc = await issueVC();
|
|
140
|
+
const tampered = structuredClone(vc);
|
|
141
|
+
tampered.issuer = subjectIdentity.did;
|
|
142
|
+
|
|
143
|
+
const result = await verifier.verifyDelegationCredential(tampered, {
|
|
144
|
+
skipStatus: true,
|
|
145
|
+
skipCache: true,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(result.valid).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should reject when DID resolver cannot find issuer', async () => {
|
|
152
|
+
const vc = await issueVC();
|
|
153
|
+
|
|
154
|
+
const nullVerifier = new DelegationCredentialVerifier({
|
|
155
|
+
didResolver: { resolve: async () => null },
|
|
156
|
+
signatureVerifier: createRealSignatureVerifier(crypto),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const result = await nullVerifier.verifyDelegationCredential(vc, {
|
|
160
|
+
skipStatus: true,
|
|
161
|
+
skipCache: true,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(result.valid).toBe(false);
|
|
165
|
+
expect(result.reason).toContain('resolve');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ── Missing Proof ─────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
it('should reject VC without proof at basic stage', async () => {
|
|
171
|
+
const vc = await issueVC();
|
|
172
|
+
const noProof = { ...vc } as Record<string, unknown>;
|
|
173
|
+
delete noProof['proof'];
|
|
174
|
+
|
|
175
|
+
const result = await verifier.verifyDelegationCredential(
|
|
176
|
+
noProof as DelegationCredential,
|
|
177
|
+
{ skipStatus: true, skipCache: true }
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect(result.valid).toBe(false);
|
|
181
|
+
expect(result.stage).toBe('basic');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ── Metrics ───────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
it('should report timing metrics for all stages', async () => {
|
|
187
|
+
const vc = await issueVC();
|
|
188
|
+
|
|
189
|
+
const result = await verifier.verifyDelegationCredential(vc, {
|
|
190
|
+
skipStatus: true,
|
|
191
|
+
skipCache: true,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(result.metrics).toBeDefined();
|
|
195
|
+
expect(result.metrics?.totalMs).toBeGreaterThanOrEqual(0);
|
|
196
|
+
expect(result.metrics?.basicCheckMs).toBeGreaterThanOrEqual(0);
|
|
197
|
+
expect(result.metrics?.signatureCheckMs).toBeGreaterThanOrEqual(0);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -140,9 +140,11 @@ export class CascadingRevocationManager {
|
|
|
140
140
|
reason?: string;
|
|
141
141
|
revokedAncestor?: string;
|
|
142
142
|
}> {
|
|
143
|
+
// Walk root → target so ancestor revocation is detected before the
|
|
144
|
+
// target's own (cascade-set) bit. getChain() already returns root-first order.
|
|
143
145
|
const chain = await this.graph.getChain(delegationId);
|
|
144
146
|
|
|
145
|
-
for (const node of chain
|
|
147
|
+
for (const node of chain) {
|
|
146
148
|
if (node.credentialStatusId) {
|
|
147
149
|
const credentialStatus = this.parseCredentialStatus(node.credentialStatusId);
|
|
148
150
|
if (credentialStatus) {
|