@mcp-i/core 0.1.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/LICENSE +21 -0
- package/README.md +390 -0
- package/dist/auth/handshake.d.ts +104 -0
- package/dist/auth/handshake.d.ts.map +1 -0
- package/dist/auth/handshake.js +230 -0
- package/dist/auth/handshake.js.map +1 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/types.d.ts +31 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +7 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/delegation/audience-validator.d.ts +9 -0
- package/dist/delegation/audience-validator.d.ts.map +1 -0
- package/dist/delegation/audience-validator.js +17 -0
- package/dist/delegation/audience-validator.js.map +1 -0
- package/dist/delegation/bitstring.d.ts +37 -0
- package/dist/delegation/bitstring.d.ts.map +1 -0
- package/dist/delegation/bitstring.js +117 -0
- package/dist/delegation/bitstring.js.map +1 -0
- package/dist/delegation/cascading-revocation.d.ts +45 -0
- package/dist/delegation/cascading-revocation.d.ts.map +1 -0
- package/dist/delegation/cascading-revocation.js +148 -0
- package/dist/delegation/cascading-revocation.js.map +1 -0
- package/dist/delegation/delegation-graph.d.ts +49 -0
- package/dist/delegation/delegation-graph.d.ts.map +1 -0
- package/dist/delegation/delegation-graph.js +99 -0
- package/dist/delegation/delegation-graph.js.map +1 -0
- package/dist/delegation/did-key-resolver.d.ts +64 -0
- package/dist/delegation/did-key-resolver.d.ts.map +1 -0
- package/dist/delegation/did-key-resolver.js +154 -0
- package/dist/delegation/did-key-resolver.js.map +1 -0
- package/dist/delegation/did-web-resolver.d.ts +83 -0
- package/dist/delegation/did-web-resolver.d.ts.map +1 -0
- package/dist/delegation/did-web-resolver.js +218 -0
- package/dist/delegation/did-web-resolver.js.map +1 -0
- package/dist/delegation/index.d.ts +21 -0
- package/dist/delegation/index.d.ts.map +1 -0
- package/dist/delegation/index.js +21 -0
- package/dist/delegation/index.js.map +1 -0
- package/dist/delegation/outbound-headers.d.ts +81 -0
- package/dist/delegation/outbound-headers.d.ts.map +1 -0
- package/dist/delegation/outbound-headers.js +139 -0
- package/dist/delegation/outbound-headers.js.map +1 -0
- package/dist/delegation/outbound-proof.d.ts +43 -0
- package/dist/delegation/outbound-proof.d.ts.map +1 -0
- package/dist/delegation/outbound-proof.js +52 -0
- package/dist/delegation/outbound-proof.js.map +1 -0
- package/dist/delegation/statuslist-manager.d.ts +44 -0
- package/dist/delegation/statuslist-manager.d.ts.map +1 -0
- package/dist/delegation/statuslist-manager.js +126 -0
- package/dist/delegation/statuslist-manager.js.map +1 -0
- package/dist/delegation/storage/memory-graph-storage.d.ts +70 -0
- package/dist/delegation/storage/memory-graph-storage.d.ts.map +1 -0
- package/dist/delegation/storage/memory-graph-storage.js +145 -0
- package/dist/delegation/storage/memory-graph-storage.js.map +1 -0
- package/dist/delegation/storage/memory-statuslist-storage.d.ts +19 -0
- package/dist/delegation/storage/memory-statuslist-storage.d.ts.map +1 -0
- package/dist/delegation/storage/memory-statuslist-storage.js +33 -0
- package/dist/delegation/storage/memory-statuslist-storage.js.map +1 -0
- package/dist/delegation/utils.d.ts +49 -0
- package/dist/delegation/utils.d.ts.map +1 -0
- package/dist/delegation/utils.js +131 -0
- package/dist/delegation/utils.js.map +1 -0
- package/dist/delegation/vc-issuer.d.ts +56 -0
- package/dist/delegation/vc-issuer.d.ts.map +1 -0
- package/dist/delegation/vc-issuer.js +80 -0
- package/dist/delegation/vc-issuer.js.map +1 -0
- package/dist/delegation/vc-verifier.d.ts +112 -0
- package/dist/delegation/vc-verifier.d.ts.map +1 -0
- package/dist/delegation/vc-verifier.js +280 -0
- package/dist/delegation/vc-verifier.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/index.d.ts +2 -0
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/logging/index.js +2 -0
- package/dist/logging/index.js.map +1 -0
- package/dist/logging/logger.d.ts +23 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +82 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/middleware/index.d.ts +7 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +7 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/with-mcpi.d.ts +152 -0
- package/dist/middleware/with-mcpi.d.ts.map +1 -0
- package/dist/middleware/with-mcpi.js +472 -0
- package/dist/middleware/with-mcpi.js.map +1 -0
- package/dist/proof/errors.d.ts +49 -0
- package/dist/proof/errors.d.ts.map +1 -0
- package/dist/proof/errors.js +61 -0
- package/dist/proof/errors.js.map +1 -0
- package/dist/proof/generator.d.ts +65 -0
- package/dist/proof/generator.d.ts.map +1 -0
- package/dist/proof/generator.js +163 -0
- package/dist/proof/generator.js.map +1 -0
- package/dist/proof/index.d.ts +4 -0
- package/dist/proof/index.d.ts.map +1 -0
- package/dist/proof/index.js +4 -0
- package/dist/proof/index.js.map +1 -0
- package/dist/proof/verifier.d.ts +108 -0
- package/dist/proof/verifier.d.ts.map +1 -0
- package/dist/proof/verifier.js +299 -0
- package/dist/proof/verifier.js.map +1 -0
- package/dist/providers/base.d.ts +64 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +19 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/index.d.ts +3 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +3 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/memory.d.ts +33 -0
- package/dist/providers/memory.d.ts.map +1 -0
- package/dist/providers/memory.js +102 -0
- package/dist/providers/memory.js.map +1 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +2 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/manager.d.ts +77 -0
- package/dist/session/manager.d.ts.map +1 -0
- package/dist/session/manager.js +251 -0
- package/dist/session/manager.js.map +1 -0
- package/dist/types/protocol.d.ts +320 -0
- package/dist/types/protocol.d.ts.map +1 -0
- package/dist/types/protocol.js +229 -0
- package/dist/types/protocol.js.map +1 -0
- package/dist/utils/base58.d.ts +31 -0
- package/dist/utils/base58.d.ts.map +1 -0
- package/dist/utils/base58.js +104 -0
- package/dist/utils/base58.js.map +1 -0
- package/dist/utils/base64.d.ts +13 -0
- package/dist/utils/base64.d.ts.map +1 -0
- package/dist/utils/base64.js +99 -0
- package/dist/utils/base64.js.map +1 -0
- package/dist/utils/crypto-service.d.ts +37 -0
- package/dist/utils/crypto-service.d.ts.map +1 -0
- package/dist/utils/crypto-service.js +153 -0
- package/dist/utils/crypto-service.js.map +1 -0
- package/dist/utils/did-helpers.d.ts +156 -0
- package/dist/utils/did-helpers.d.ts.map +1 -0
- package/dist/utils/did-helpers.js +193 -0
- package/dist/utils/did-helpers.js.map +1 -0
- package/dist/utils/ed25519-constants.d.ts +18 -0
- package/dist/utils/ed25519-constants.d.ts.map +1 -0
- package/dist/utils/ed25519-constants.js +21 -0
- package/dist/utils/ed25519-constants.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +105 -0
- package/src/__tests__/integration/full-flow.test.ts +362 -0
- package/src/__tests__/providers/base.test.ts +173 -0
- package/src/__tests__/providers/memory.test.ts +332 -0
- package/src/__tests__/utils/mock-providers.ts +319 -0
- package/src/__tests__/utils/node-crypto-provider.ts +93 -0
- package/src/auth/handshake.ts +411 -0
- package/src/auth/index.ts +11 -0
- package/src/auth/types.ts +40 -0
- package/src/delegation/__tests__/audience-validator.test.ts +110 -0
- package/src/delegation/__tests__/bitstring.test.ts +346 -0
- package/src/delegation/__tests__/cascading-revocation.test.ts +624 -0
- package/src/delegation/__tests__/delegation-graph.test.ts +623 -0
- package/src/delegation/__tests__/did-key-resolver.test.ts +265 -0
- package/src/delegation/__tests__/did-web-resolver.test.ts +467 -0
- package/src/delegation/__tests__/outbound-headers.test.ts +230 -0
- package/src/delegation/__tests__/outbound-proof.test.ts +179 -0
- package/src/delegation/__tests__/statuslist-manager.test.ts +515 -0
- package/src/delegation/__tests__/utils.test.ts +185 -0
- package/src/delegation/__tests__/vc-issuer.test.ts +487 -0
- package/src/delegation/__tests__/vc-verifier.test.ts +1029 -0
- package/src/delegation/audience-validator.ts +24 -0
- package/src/delegation/bitstring.ts +160 -0
- package/src/delegation/cascading-revocation.ts +224 -0
- package/src/delegation/delegation-graph.ts +143 -0
- package/src/delegation/did-key-resolver.ts +181 -0
- package/src/delegation/did-web-resolver.ts +270 -0
- package/src/delegation/index.ts +33 -0
- package/src/delegation/outbound-headers.ts +193 -0
- package/src/delegation/outbound-proof.ts +90 -0
- package/src/delegation/statuslist-manager.ts +219 -0
- package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +366 -0
- package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +228 -0
- package/src/delegation/storage/memory-graph-storage.ts +178 -0
- package/src/delegation/storage/memory-statuslist-storage.ts +42 -0
- package/src/delegation/utils.ts +189 -0
- package/src/delegation/vc-issuer.ts +137 -0
- package/src/delegation/vc-verifier.ts +440 -0
- package/src/index.ts +264 -0
- package/src/logging/__tests__/logger.test.ts +366 -0
- package/src/logging/index.ts +6 -0
- package/src/logging/logger.ts +91 -0
- package/src/middleware/__tests__/with-mcpi.test.ts +504 -0
- package/src/middleware/index.ts +16 -0
- package/src/middleware/with-mcpi.ts +766 -0
- package/src/proof/__tests__/proof-generator.test.ts +483 -0
- package/src/proof/__tests__/verifier.test.ts +488 -0
- package/src/proof/errors.ts +75 -0
- package/src/proof/generator.ts +255 -0
- package/src/proof/index.ts +22 -0
- package/src/proof/verifier.ts +449 -0
- package/src/providers/base.ts +68 -0
- package/src/providers/index.ts +15 -0
- package/src/providers/memory.ts +130 -0
- package/src/session/__tests__/session-manager.test.ts +342 -0
- package/src/session/index.ts +7 -0
- package/src/session/manager.ts +332 -0
- package/src/types/protocol.ts +596 -0
- package/src/utils/__tests__/base58.test.ts +281 -0
- package/src/utils/__tests__/base64.test.ts +239 -0
- package/src/utils/__tests__/crypto-service.test.ts +530 -0
- package/src/utils/__tests__/did-helpers.test.ts +156 -0
- package/src/utils/base58.ts +115 -0
- package/src/utils/base64.ts +116 -0
- package/src/utils/crypto-service.ts +209 -0
- package/src/utils/did-helpers.ts +210 -0
- package/src/utils/ed25519-constants.ts +23 -0
- package/src/utils/index.ts +9 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ProofVerifier
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive security test coverage for proof verification service.
|
|
5
|
+
* Tests nonce replay protection, timestamp skew validation, canonical payload reconstruction,
|
|
6
|
+
* and various security attack scenarios.
|
|
7
|
+
*
|
|
8
|
+
* Test Coverage Requirements: 100% - All security-critical code paths
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
12
|
+
import { ProofVerifier } from '../verifier.js';
|
|
13
|
+
import { CryptoService, type Ed25519JWK } from '../../utils/crypto-service.js';
|
|
14
|
+
import type {
|
|
15
|
+
CryptoProvider,
|
|
16
|
+
ClockProvider,
|
|
17
|
+
NonceCacheProvider,
|
|
18
|
+
FetchProvider,
|
|
19
|
+
} from '../../providers/base.js';
|
|
20
|
+
import type { DetachedProof } from '../../types/protocol.js';
|
|
21
|
+
import {
|
|
22
|
+
ProofVerificationError,
|
|
23
|
+
PROOF_VERIFICATION_ERROR_CODES,
|
|
24
|
+
} from '../errors.js';
|
|
25
|
+
|
|
26
|
+
describe('ProofVerifier Security', () => {
|
|
27
|
+
let proofVerifier: ProofVerifier;
|
|
28
|
+
let mockCryptoProvider: CryptoProvider;
|
|
29
|
+
let mockClockProvider: ClockProvider;
|
|
30
|
+
let mockNonceCache: NonceCacheProvider;
|
|
31
|
+
let mockFetchProvider: FetchProvider;
|
|
32
|
+
let cryptoService: CryptoService;
|
|
33
|
+
|
|
34
|
+
const validJwk: Ed25519JWK = {
|
|
35
|
+
kty: 'OKP',
|
|
36
|
+
crv: 'Ed25519',
|
|
37
|
+
x: 'VCpo2LMLhn6iWku8MKvSLg2ZAoC-nlOyPVQaO3FxVeQ',
|
|
38
|
+
kid: 'did:key:z123#keys-1',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const createValidProof = (): DetachedProof => {
|
|
42
|
+
const header = { alg: 'EdDSA', typ: 'JWT' };
|
|
43
|
+
// Create a proper JSON payload that matches the meta structure
|
|
44
|
+
const payload = {
|
|
45
|
+
aud: 'test-audience',
|
|
46
|
+
sub: 'did:key:z123',
|
|
47
|
+
iss: 'did:key:z123',
|
|
48
|
+
nonce: 'nonce123',
|
|
49
|
+
ts: Math.floor(Date.now() / 1000),
|
|
50
|
+
sessionId: 'session123',
|
|
51
|
+
requestHash: 'sha256:' + 'a'.repeat(64),
|
|
52
|
+
responseHash: 'sha256:' + 'b'.repeat(64),
|
|
53
|
+
};
|
|
54
|
+
// Use btoa for base64 encoding (available in test environment via polyfill)
|
|
55
|
+
const headerB64 = btoa(JSON.stringify(header))
|
|
56
|
+
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
57
|
+
const payloadB64 = btoa(JSON.stringify(payload))
|
|
58
|
+
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
59
|
+
const signatureB64 = btoa('signature')
|
|
60
|
+
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
61
|
+
const jws = `${headerB64}.${payloadB64}.${signatureB64}`;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
jws,
|
|
65
|
+
meta: {
|
|
66
|
+
did: 'did:key:z123',
|
|
67
|
+
kid: 'did:key:z123#keys-1',
|
|
68
|
+
ts: Math.floor(Date.now() / 1000),
|
|
69
|
+
nonce: 'nonce123',
|
|
70
|
+
audience: 'test-audience',
|
|
71
|
+
sessionId: 'session123',
|
|
72
|
+
requestHash: 'sha256:' + 'a'.repeat(64),
|
|
73
|
+
responseHash: 'sha256:' + 'b'.repeat(64),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
mockCryptoProvider = {
|
|
80
|
+
sign: vi.fn(),
|
|
81
|
+
verify: vi.fn().mockResolvedValue(true),
|
|
82
|
+
generateKeyPair: vi.fn(),
|
|
83
|
+
hash: vi.fn(),
|
|
84
|
+
randomBytes: vi.fn(),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
cryptoService = new CryptoService(mockCryptoProvider);
|
|
88
|
+
|
|
89
|
+
mockClockProvider = {
|
|
90
|
+
now: vi.fn().mockReturnValue(Date.now()), // Return milliseconds
|
|
91
|
+
isWithinSkew: vi.fn().mockReturnValue(true),
|
|
92
|
+
hasExpired: vi.fn(),
|
|
93
|
+
calculateExpiry: vi.fn((ttlSeconds: number) => Date.now() + (ttlSeconds * 1000)), // Return milliseconds
|
|
94
|
+
format: vi.fn(),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
mockNonceCache = {
|
|
98
|
+
has: vi.fn().mockResolvedValue(false),
|
|
99
|
+
add: vi.fn().mockResolvedValue(undefined),
|
|
100
|
+
cleanup: vi.fn().mockResolvedValue(undefined),
|
|
101
|
+
destroy: vi.fn().mockResolvedValue(undefined),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
mockFetchProvider = {
|
|
105
|
+
resolveDID: vi.fn().mockResolvedValue({
|
|
106
|
+
verificationMethod: [{
|
|
107
|
+
id: 'did:key:z123#keys-1',
|
|
108
|
+
publicKeyJwk: validJwk,
|
|
109
|
+
}],
|
|
110
|
+
}),
|
|
111
|
+
fetchStatusList: vi.fn(),
|
|
112
|
+
fetchDelegationChain: vi.fn(),
|
|
113
|
+
fetch: vi.fn(),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
proofVerifier = new ProofVerifier({
|
|
117
|
+
cryptoProvider: mockCryptoProvider,
|
|
118
|
+
clockProvider: mockClockProvider,
|
|
119
|
+
nonceCacheProvider: mockNonceCache,
|
|
120
|
+
fetchProvider: mockFetchProvider,
|
|
121
|
+
timestampSkewSeconds: 120,
|
|
122
|
+
nonceTtlSeconds: 300,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('Nonce Replay Protection', () => {
|
|
127
|
+
it('should prevent nonce replay attacks', async () => {
|
|
128
|
+
const proof = createValidProof();
|
|
129
|
+
|
|
130
|
+
// First verification should succeed
|
|
131
|
+
const result1 = await proofVerifier.verifyProof(proof, validJwk);
|
|
132
|
+
expect(result1.valid).toBe(true);
|
|
133
|
+
expect(mockNonceCache.has).toHaveBeenCalledWith('nonce123', 'did:key:z123');
|
|
134
|
+
expect(mockNonceCache.add).toHaveBeenCalled();
|
|
135
|
+
|
|
136
|
+
// Reset mock to simulate second attempt
|
|
137
|
+
mockNonceCache.has = vi.fn().mockResolvedValue(true);
|
|
138
|
+
|
|
139
|
+
// Second verification with same nonce should fail
|
|
140
|
+
const result2 = await proofVerifier.verifyProof(proof, validJwk);
|
|
141
|
+
expect(result2.valid).toBe(false);
|
|
142
|
+
expect(result2.reason).toContain('replay');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should add nonce to cache after successful verification', async () => {
|
|
146
|
+
const proof = createValidProof();
|
|
147
|
+
|
|
148
|
+
await proofVerifier.verifyProof(proof, validJwk);
|
|
149
|
+
|
|
150
|
+
expect(mockNonceCache.add).toHaveBeenCalledWith(
|
|
151
|
+
'nonce123',
|
|
152
|
+
expect.any(Number),
|
|
153
|
+
'did:key:z123'
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('Timestamp Skew Validation', () => {
|
|
159
|
+
it('should enforce timestamp skew limits', async () => {
|
|
160
|
+
const proof = createValidProof();
|
|
161
|
+
const currentTime = Date.now(); // milliseconds
|
|
162
|
+
|
|
163
|
+
// Set clock to 5 minutes in the future
|
|
164
|
+
mockClockProvider.now = vi.fn().mockReturnValue(currentTime);
|
|
165
|
+
mockClockProvider.isWithinSkew = vi.fn().mockReturnValue(false);
|
|
166
|
+
|
|
167
|
+
const result = await proofVerifier.verifyProof(proof, validJwk);
|
|
168
|
+
|
|
169
|
+
expect(result.valid).toBe(false);
|
|
170
|
+
expect(result.reason).toContain('skew');
|
|
171
|
+
// isWithinSkew is called with timestamp in milliseconds (converted from seconds)
|
|
172
|
+
expect(mockClockProvider.isWithinSkew).toHaveBeenCalledWith(
|
|
173
|
+
proof.meta.ts * 1000, // Convert seconds to milliseconds
|
|
174
|
+
120
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should accept timestamps within skew window', async () => {
|
|
179
|
+
const proof = createValidProof();
|
|
180
|
+
mockClockProvider.isWithinSkew = vi.fn().mockReturnValue(true);
|
|
181
|
+
|
|
182
|
+
const result = await proofVerifier.verifyProof(proof, validJwk);
|
|
183
|
+
|
|
184
|
+
expect(result.valid).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should use custom timestamp skew seconds', async () => {
|
|
188
|
+
const customProofVerifier = new ProofVerifier({
|
|
189
|
+
cryptoProvider: mockCryptoProvider,
|
|
190
|
+
clockProvider: mockClockProvider,
|
|
191
|
+
nonceCacheProvider: mockNonceCache,
|
|
192
|
+
fetchProvider: mockFetchProvider,
|
|
193
|
+
timestampSkewSeconds: 300, // 5 minutes
|
|
194
|
+
nonceTtlSeconds: 300,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const proof = createValidProof();
|
|
198
|
+
mockClockProvider.isWithinSkew = vi.fn().mockReturnValue(false);
|
|
199
|
+
|
|
200
|
+
await customProofVerifier.verifyProof(proof, validJwk);
|
|
201
|
+
|
|
202
|
+
// isWithinSkew is called with timestamp in milliseconds (converted from seconds)
|
|
203
|
+
expect(mockClockProvider.isWithinSkew).toHaveBeenCalledWith(
|
|
204
|
+
proof.meta.ts * 1000, // Convert seconds to milliseconds
|
|
205
|
+
300
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('Canonical Payload Reconstruction', () => {
|
|
211
|
+
it('should reconstruct canonical payload from meta', async () => {
|
|
212
|
+
const proof = createValidProof();
|
|
213
|
+
|
|
214
|
+
await proofVerifier.verifyProof(proof, validJwk);
|
|
215
|
+
|
|
216
|
+
// Verify that verifyJWS was called with detached payload
|
|
217
|
+
expect(mockCryptoProvider.verify).toHaveBeenCalled();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should validate canonical payload ordering determinism', () => {
|
|
221
|
+
const meta1 = {
|
|
222
|
+
z: 1,
|
|
223
|
+
a: 2,
|
|
224
|
+
m: 3,
|
|
225
|
+
did: 'did:test',
|
|
226
|
+
kid: 'kid',
|
|
227
|
+
ts: 123,
|
|
228
|
+
nonce: 'nonce',
|
|
229
|
+
audience: 'aud',
|
|
230
|
+
sessionId: 'session',
|
|
231
|
+
requestHash: 'sha256:' + 'a'.repeat(64),
|
|
232
|
+
responseHash: 'sha256:' + 'b'.repeat(64),
|
|
233
|
+
};
|
|
234
|
+
const meta2 = {
|
|
235
|
+
a: 2,
|
|
236
|
+
m: 3,
|
|
237
|
+
z: 1,
|
|
238
|
+
did: 'did:test',
|
|
239
|
+
kid: 'kid',
|
|
240
|
+
ts: 123,
|
|
241
|
+
nonce: 'nonce',
|
|
242
|
+
audience: 'aud',
|
|
243
|
+
sessionId: 'session',
|
|
244
|
+
requestHash: 'sha256:' + 'a'.repeat(64),
|
|
245
|
+
responseHash: 'sha256:' + 'b'.repeat(64),
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const canonical1 = proofVerifier.buildCanonicalPayload(meta1);
|
|
249
|
+
const canonical2 = proofVerifier.buildCanonicalPayload(meta2);
|
|
250
|
+
|
|
251
|
+
// Should be identical despite different key order
|
|
252
|
+
expect(canonical1).toBe(canonical2);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should handle detached JWS reconstruction', async () => {
|
|
256
|
+
const header = { alg: 'EdDSA' };
|
|
257
|
+
const headerB64 = btoa(JSON.stringify(header))
|
|
258
|
+
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
259
|
+
const signatureB64 = btoa('signature')
|
|
260
|
+
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
261
|
+
const detachedJws = `${headerB64}..${signatureB64}`;
|
|
262
|
+
|
|
263
|
+
const proof: DetachedProof = {
|
|
264
|
+
jws: detachedJws,
|
|
265
|
+
meta: createValidProof().meta,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const result = await proofVerifier.verifyProof(proof, validJwk);
|
|
269
|
+
|
|
270
|
+
// Should call verifyJWS with detached payload
|
|
271
|
+
expect(mockCryptoProvider.verify).toHaveBeenCalled();
|
|
272
|
+
expect(result.valid).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe('Proof Structure Validation', () => {
|
|
277
|
+
it('should reject invalid proof structure', async () => {
|
|
278
|
+
const invalidProof = {
|
|
279
|
+
jws: 'invalid',
|
|
280
|
+
meta: {
|
|
281
|
+
// Missing required fields
|
|
282
|
+
did: 'did:test',
|
|
283
|
+
},
|
|
284
|
+
} as any;
|
|
285
|
+
|
|
286
|
+
const result = await proofVerifier.verifyProof(invalidProof, validJwk);
|
|
287
|
+
|
|
288
|
+
expect(result.valid).toBe(false);
|
|
289
|
+
expect(result.reason).toContain('Invalid proof structure');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should reject proof with missing required meta fields', async () => {
|
|
293
|
+
const invalidProof: DetachedProof = {
|
|
294
|
+
jws: 'header.payload.signature',
|
|
295
|
+
meta: {
|
|
296
|
+
did: 'did:test',
|
|
297
|
+
kid: 'kid',
|
|
298
|
+
ts: 123,
|
|
299
|
+
nonce: 'nonce',
|
|
300
|
+
audience: 'aud',
|
|
301
|
+
sessionId: 'session',
|
|
302
|
+
// Missing requestHash and responseHash
|
|
303
|
+
requestHash: '' as any,
|
|
304
|
+
responseHash: '' as any,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const result = await proofVerifier.verifyProof(invalidProof, validJwk);
|
|
309
|
+
|
|
310
|
+
expect(result.valid).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('Signature Verification', () => {
|
|
315
|
+
it('should reject proof with invalid signature', async () => {
|
|
316
|
+
const proof = createValidProof();
|
|
317
|
+
mockCryptoProvider.verify = vi.fn().mockResolvedValue(false);
|
|
318
|
+
|
|
319
|
+
const result = await proofVerifier.verifyProof(proof, validJwk);
|
|
320
|
+
|
|
321
|
+
expect(result.valid).toBe(false);
|
|
322
|
+
expect(result.reason).toContain('Invalid JWS signature');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should handle signature verification errors gracefully', async () => {
|
|
326
|
+
const proof = createValidProof();
|
|
327
|
+
mockCryptoProvider.verify = vi.fn().mockRejectedValue(
|
|
328
|
+
new Error('Crypto error')
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const result = await proofVerifier.verifyProof(proof, validJwk);
|
|
332
|
+
|
|
333
|
+
expect(result.valid).toBe(false);
|
|
334
|
+
expect(result.reason).toBeDefined();
|
|
335
|
+
// Should not throw, should return error result
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('verifyProofDetached', () => {
|
|
340
|
+
it('should verify proof with string canonical payload', async () => {
|
|
341
|
+
const proof = createValidProof();
|
|
342
|
+
const canonicalPayload = proofVerifier.buildCanonicalPayload(proof.meta);
|
|
343
|
+
|
|
344
|
+
const result = await proofVerifier.verifyProofDetached(
|
|
345
|
+
proof,
|
|
346
|
+
canonicalPayload,
|
|
347
|
+
validJwk
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
expect(result.valid).toBe(true);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('should verify proof with Uint8Array canonical payload', async () => {
|
|
354
|
+
const proof = createValidProof();
|
|
355
|
+
const canonicalPayload = proofVerifier.buildCanonicalPayload(proof.meta);
|
|
356
|
+
const canonicalPayloadBytes = new TextEncoder().encode(canonicalPayload);
|
|
357
|
+
|
|
358
|
+
const result = await proofVerifier.verifyProofDetached(
|
|
359
|
+
proof,
|
|
360
|
+
canonicalPayloadBytes,
|
|
361
|
+
validJwk
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
expect(result.valid).toBe(true);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should prevent nonce replay in verifyProofDetached', async () => {
|
|
368
|
+
const proof = createValidProof();
|
|
369
|
+
const canonicalPayload = proofVerifier.buildCanonicalPayload(proof.meta);
|
|
370
|
+
|
|
371
|
+
// First verification
|
|
372
|
+
const result1 = await proofVerifier.verifyProofDetached(
|
|
373
|
+
proof,
|
|
374
|
+
canonicalPayload,
|
|
375
|
+
validJwk
|
|
376
|
+
);
|
|
377
|
+
expect(result1.valid).toBe(true);
|
|
378
|
+
|
|
379
|
+
// Second verification should fail
|
|
380
|
+
mockNonceCache.has = vi.fn().mockResolvedValue(true);
|
|
381
|
+
const result2 = await proofVerifier.verifyProofDetached(
|
|
382
|
+
proof,
|
|
383
|
+
canonicalPayload,
|
|
384
|
+
validJwk
|
|
385
|
+
);
|
|
386
|
+
expect(result2.valid).toBe(false);
|
|
387
|
+
expect(result2.reason).toContain('replay');
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('Error Handling', () => {
|
|
392
|
+
it('should never throw on verification errors', async () => {
|
|
393
|
+
const proof = createValidProof();
|
|
394
|
+
|
|
395
|
+
// Simulate various error conditions
|
|
396
|
+
mockNonceCache.has = vi.fn().mockRejectedValue(new Error('Cache error'));
|
|
397
|
+
|
|
398
|
+
const result = await proofVerifier.verifyProof(proof, validJwk);
|
|
399
|
+
|
|
400
|
+
// Should return error result, not throw
|
|
401
|
+
expect(result.valid).toBe(false);
|
|
402
|
+
expect(result.reason).toBeDefined();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should handle clock provider errors gracefully', async () => {
|
|
406
|
+
const proof = createValidProof();
|
|
407
|
+
mockClockProvider.isWithinSkew = vi.fn().mockImplementation(() => {
|
|
408
|
+
throw new Error('Clock error');
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const result = await proofVerifier.verifyProof(proof, validJwk);
|
|
412
|
+
|
|
413
|
+
expect(result.valid).toBe(false);
|
|
414
|
+
expect(result.reason).toBeDefined();
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
describe('fetchPublicKeyFromDID', () => {
|
|
419
|
+
it('should fetch public key from DID document', async () => {
|
|
420
|
+
const jwk = await proofVerifier.fetchPublicKeyFromDID('did:key:z123', 'keys-1');
|
|
421
|
+
|
|
422
|
+
expect(jwk).toEqual(validJwk);
|
|
423
|
+
expect(mockFetchProvider.resolveDID).toHaveBeenCalledWith('did:key:z123');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('should throw ProofVerificationError if DID document not found', async () => {
|
|
427
|
+
mockFetchProvider.resolveDID = vi.fn().mockResolvedValue(null);
|
|
428
|
+
|
|
429
|
+
await expect(
|
|
430
|
+
proofVerifier.fetchPublicKeyFromDID('did:key:z123')
|
|
431
|
+
).rejects.toThrow(ProofVerificationError);
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
await proofVerifier.fetchPublicKeyFromDID('did:key:z123');
|
|
435
|
+
} catch (error) {
|
|
436
|
+
expect(error).toBeInstanceOf(ProofVerificationError);
|
|
437
|
+
expect((error as ProofVerificationError).code).toBe(
|
|
438
|
+
PROOF_VERIFICATION_ERROR_CODES.DID_DOCUMENT_NOT_FOUND
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should throw ProofVerificationError if verification method not found', async () => {
|
|
444
|
+
mockFetchProvider.resolveDID = vi.fn().mockResolvedValue({
|
|
445
|
+
verificationMethod: [],
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
await expect(
|
|
449
|
+
proofVerifier.fetchPublicKeyFromDID('did:key:z123', 'key-1')
|
|
450
|
+
).rejects.toThrow(ProofVerificationError);
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
await proofVerifier.fetchPublicKeyFromDID('did:key:z123', 'key-1');
|
|
454
|
+
} catch (error) {
|
|
455
|
+
expect(error).toBeInstanceOf(ProofVerificationError);
|
|
456
|
+
expect((error as ProofVerificationError).code).toBe(
|
|
457
|
+
PROOF_VERIFICATION_ERROR_CODES.VERIFICATION_METHOD_NOT_FOUND
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('should throw ProofVerificationError if JWK is not Ed25519', async () => {
|
|
463
|
+
mockFetchProvider.resolveDID = vi.fn().mockResolvedValue({
|
|
464
|
+
verificationMethod: [{
|
|
465
|
+
id: 'did:key:z123#keys-1',
|
|
466
|
+
publicKeyJwk: {
|
|
467
|
+
kty: 'RSA',
|
|
468
|
+
crv: 'RS256',
|
|
469
|
+
n: 'invalid',
|
|
470
|
+
},
|
|
471
|
+
}],
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
await expect(
|
|
475
|
+
proofVerifier.fetchPublicKeyFromDID('did:key:z123')
|
|
476
|
+
).rejects.toThrow(ProofVerificationError);
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
await proofVerifier.fetchPublicKeyFromDID('did:key:z123');
|
|
480
|
+
} catch (error) {
|
|
481
|
+
expect(error).toBeInstanceOf(ProofVerificationError);
|
|
482
|
+
expect((error as ProofVerificationError).code).toBe(
|
|
483
|
+
PROOF_VERIFICATION_ERROR_CODES.INVALID_JWK_FORMAT
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proof Verification Error Codes and Types
|
|
3
|
+
*
|
|
4
|
+
* Specific error codes for proof verification failures to enable
|
|
5
|
+
* better error handling and debugging.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Error codes for proof verification
|
|
10
|
+
*/
|
|
11
|
+
export const PROOF_VERIFICATION_ERROR_CODES = {
|
|
12
|
+
// Proof structure errors
|
|
13
|
+
INVALID_PROOF_STRUCTURE: "INVALID_PROOF_STRUCTURE",
|
|
14
|
+
MISSING_REQUIRED_FIELD: "MISSING_REQUIRED_FIELD",
|
|
15
|
+
|
|
16
|
+
// Security errors
|
|
17
|
+
NONCE_REPLAY_DETECTED: "NONCE_REPLAY_DETECTED",
|
|
18
|
+
TIMESTAMP_SKEW_EXCEEDED: "TIMESTAMP_SKEW_EXCEEDED",
|
|
19
|
+
TIMESTAMP_INVALID: "TIMESTAMP_INVALID",
|
|
20
|
+
|
|
21
|
+
// Signature errors
|
|
22
|
+
INVALID_JWS_SIGNATURE: "INVALID_JWS_SIGNATURE",
|
|
23
|
+
INVALID_JWS_FORMAT: "INVALID_JWS_FORMAT",
|
|
24
|
+
INVALID_JWS_HEADER: "INVALID_JWS_HEADER",
|
|
25
|
+
INVALID_JWS_PAYLOAD: "INVALID_JWS_PAYLOAD",
|
|
26
|
+
INVALID_JWS_SIGNATURE_BASE64: "INVALID_JWS_SIGNATURE_BASE64",
|
|
27
|
+
UNSUPPORTED_ALGORITHM: "UNSUPPORTED_ALGORITHM",
|
|
28
|
+
|
|
29
|
+
// JWK errors
|
|
30
|
+
INVALID_JWK_FORMAT: "INVALID_JWK_FORMAT",
|
|
31
|
+
INVALID_JWK_KTY: "INVALID_JWK_KTY",
|
|
32
|
+
INVALID_JWK_CRV: "INVALID_JWK_CRV",
|
|
33
|
+
INVALID_JWK_X_FIELD: "INVALID_JWK_X_FIELD",
|
|
34
|
+
INVALID_JWK_KEY_LENGTH: "INVALID_JWK_KEY_LENGTH",
|
|
35
|
+
JWK_KID_MISMATCH: "JWK_KID_MISMATCH",
|
|
36
|
+
|
|
37
|
+
// DID resolution errors
|
|
38
|
+
DID_RESOLUTION_FAILED: "DID_RESOLUTION_FAILED",
|
|
39
|
+
DID_DOCUMENT_NOT_FOUND: "DID_DOCUMENT_NOT_FOUND",
|
|
40
|
+
VERIFICATION_METHOD_NOT_FOUND: "VERIFICATION_METHOD_NOT_FOUND",
|
|
41
|
+
PUBLIC_KEY_NOT_FOUND: "PUBLIC_KEY_NOT_FOUND",
|
|
42
|
+
UNSUPPORTED_DID_METHOD: "UNSUPPORTED_DID_METHOD",
|
|
43
|
+
|
|
44
|
+
// Generic errors
|
|
45
|
+
VERIFICATION_ERROR: "VERIFICATION_ERROR",
|
|
46
|
+
INTERNAL_ERROR: "INTERNAL_ERROR",
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
export type ProofVerificationErrorCode =
|
|
50
|
+
typeof PROOF_VERIFICATION_ERROR_CODES[keyof typeof PROOF_VERIFICATION_ERROR_CODES];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Proof verification error with specific error code
|
|
54
|
+
*/
|
|
55
|
+
export class ProofVerificationError extends Error {
|
|
56
|
+
constructor(
|
|
57
|
+
public readonly code: ProofVerificationErrorCode,
|
|
58
|
+
message: string,
|
|
59
|
+
public readonly details?: Record<string, unknown>
|
|
60
|
+
) {
|
|
61
|
+
super(message);
|
|
62
|
+
this.name = "ProofVerificationError";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create a proof verification error
|
|
68
|
+
*/
|
|
69
|
+
export function createProofVerificationError(
|
|
70
|
+
code: ProofVerificationErrorCode,
|
|
71
|
+
message: string,
|
|
72
|
+
details?: Record<string, unknown>
|
|
73
|
+
): ProofVerificationError {
|
|
74
|
+
return new ProofVerificationError(code, message, details);
|
|
75
|
+
}
|