-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathcrl-verification.test.ts
More file actions
357 lines (321 loc) · 10.3 KB
/
crl-verification.test.ts
File metadata and controls
357 lines (321 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
/**
* CRL Certificate Verification Integration Tests
*
* Tests Harper's CRL (Certificate Revocation List) certificate verification
* using the new integration test infrastructure. Each test runs in isolation with its
* own Harper instance, certificates, and CRL server.
*/
import { suite, test, before, after } from 'node:test';
import { ok } from 'node:assert/strict';
import { mkdtemp, rm } from 'node:fs/promises';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import * as https from 'node:https';
import { setupHarperWithFixture, teardownHarper, type ContextWithHarper } from '@harperfast/integration-testing';
import {
generateCrlCertificates,
setupCrlServerWithCerts,
stopCrlServer,
type CrlServerContext,
type CrlCertificates,
} from '../utils/securityServices.ts';
const HTTPS_PORT = 9927;
const FIXTURE_PATH = join(import.meta.dirname, 'fixture');
// The last test stops the CRL server to verify caching, so tests must run sequentially.
// Tests CANNOT run concurrently because the caching test stops the server that earlier tests need.
suite(
'CRL Certificate Verification',
{ concurrency: 1 }, // Explicit: tests must run in order
(ctx: ContextWithHarper) => {
let crlServer: CrlServerContext | null = null;
let certsPath: string; // Track separately for cleanup even if server is stopped
before(async () => {
// 1. Create temp directory for certificates
certsPath = await mkdtemp(join(tmpdir(), 'harper-crl-test-'));
// 2. Setup CRL server (picks port, generates certs, starts server with retry)
crlServer = await setupCrlServerWithCerts(certsPath);
// 3. Setup Harper with fixture pre-installed and CA certificate configuration
await setupHarperWithFixture(ctx, FIXTURE_PATH, {
config: {
http: {
mtls: {
user: 'admin',
certificateVerification: {
failureMode: 'fail-closed',
crl: {
enabled: true,
timeout: 30000,
cacheTtl: 604800000, // 7 days
},
ocsp: {
enabled: false,
},
},
},
},
tls: {
certificateAuthority: crlServer.certs.ca,
},
},
});
});
after(async () => {
// 1. Stop CRL server (if not already stopped by caching test)
if (crlServer) {
await stopCrlServer(crlServer);
}
// 2. Teardown Harper (before removing certs in case Harper is using them)
await teardownHarper(ctx);
// 3. Cleanup certificates (always cleanup, even if server was stopped early)
await rm(certsPath, { recursive: true, force: true, maxRetries: 3 });
});
test('should accept valid certificate with CRL check', async () => {
return new Promise<void>((resolve, reject) => {
const req = https.request(
`https://${ctx.harper.hostname}:${HTTPS_PORT}/`,
{
cert: readFileSync(crlServer!.certs.valid.cert),
key: readFileSync(crlServer!.certs.valid.key),
ca: readFileSync(crlServer!.certs.ca),
rejectUnauthorized: false, // Harper uses self-signed server cert
},
(res) => {
res.resume();
ok(res.statusCode !== 401, `Expected non-401 for valid cert (cert accepted). Got: ${res.statusCode}`);
res.on('end', () => resolve());
}
);
req.on('error', (err) => reject(new Error(`Request failed: ${err.message}`)));
req.end();
});
});
test('should reject revoked certificate with CRL check', async () => {
return new Promise<void>((resolve, reject) => {
const req = https.request(
`https://${ctx.harper.hostname}:${HTTPS_PORT}/`,
{
cert: readFileSync(crlServer!.certs.revoked.cert),
key: readFileSync(crlServer!.certs.revoked.key),
ca: readFileSync(crlServer!.certs.ca),
rejectUnauthorized: false,
},
(res) => {
res.resume();
ok(res.statusCode === 401, `Expected 401 for revoked cert, got: ${res.statusCode}`);
res.on('end', () => resolve());
}
);
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout - connection hung'));
});
req.on('error', (err: any) => {
reject(new Error(`Expected application-level rejection (401), got TLS error: ${err.code || err.message}`));
});
req.end();
});
});
test('should cache CRL and serve valid certificate after CRL server stops', async () => {
const validCert = readFileSync(crlServer!.certs.valid.cert);
const validKey = readFileSync(crlServer!.certs.valid.key);
const ca = readFileSync(crlServer!.certs.ca);
const makeRequest = () =>
new Promise<number>((resolve, reject) => {
const req = https.request(
`https://${ctx.harper.hostname}:${HTTPS_PORT}/`,
{ cert: validCert, key: validKey, ca, rejectUnauthorized: false },
(res) => {
res.resume();
res.on('end', () => resolve(res.statusCode!));
}
);
req.on('error', reject);
req.end();
});
// First request - populates cache
const status1 = await makeRequest();
ok(status1 !== 401, `First request should be accepted (not 401), got ${status1}`);
// Stop CRL server to prove caching works.
// Set to null so after() hook knows server is already stopped.
if (crlServer) {
await stopCrlServer(crlServer);
crlServer = null;
}
// Second request - should succeed using cached CRL
const status2 = await makeRequest();
ok(status2 !== 401, `Second request should be accepted with cached CRL (not 401), got ${status2}`);
});
}
);
suite('CRL Certificate Verification - Disabled', (ctx: ContextWithHarper) => {
let certsPath: string;
let certs: CrlCertificates;
before(async () => {
certsPath = await mkdtemp(join(tmpdir(), 'harper-crl-disabled-'));
// Use placeholder port since CRL is disabled and won't be accessed
certs = await generateCrlCertificates(certsPath, '127.0.0.1', 19999);
await setupHarperWithFixture(ctx, FIXTURE_PATH, {
config: {
http: {
mtls: {
user: 'admin',
certificateVerification: {
failureMode: 'fail-closed',
crl: {
enabled: false,
},
ocsp: {
enabled: false,
},
},
},
},
tls: {
certificateAuthority: certs.ca,
},
},
});
});
after(async () => {
await teardownHarper(ctx);
await rm(certsPath, { recursive: true, force: true, maxRetries: 3 });
});
test('should accept certificate when CRL is disabled', async () => {
return new Promise<void>((resolve, reject) => {
const req = https.request(
`https://${ctx.harper.hostname}:${HTTPS_PORT}/`,
{
cert: readFileSync(certs.valid.cert),
key: readFileSync(certs.valid.key),
ca: readFileSync(certs.ca),
rejectUnauthorized: false,
},
(res) => {
res.resume();
ok(res.statusCode !== 401, `Expected non-401 (CRL disabled, cert accepted). Got: ${res.statusCode}`);
res.on('end', () => resolve());
}
);
req.on('error', (err) => reject(new Error(`Request failed: ${err.message}`)));
req.end();
});
});
});
suite('CRL Certificate Verification - Fail-Open Mode', (ctx: ContextWithHarper) => {
let certsPath: string;
let certs: CrlCertificates;
before(async () => {
certsPath = await mkdtemp(join(tmpdir(), 'harper-crl-failopen-'));
// No CRL server started - this will cause CRL fetch to fail
certs = await generateCrlCertificates(certsPath, '127.0.0.1', 19999);
await setupHarperWithFixture(ctx, FIXTURE_PATH, {
config: {
http: {
mtls: {
user: 'admin',
certificateVerification: {
failureMode: 'fail-open',
crl: {
enabled: true,
timeout: 1000, // Very short timeout to trigger failure
cacheTtl: 604800000,
},
},
},
},
tls: {
certificateAuthority: certs.ca,
},
},
});
});
after(async () => {
await teardownHarper(ctx);
await rm(certsPath, { recursive: true, force: true, maxRetries: 3 });
});
test('should allow connection in fail-open mode when CRL unavailable', async () => {
return new Promise<void>((resolve, reject) => {
const req = https.request(
`https://${ctx.harper.hostname}:${HTTPS_PORT}/`,
{
cert: readFileSync(certs.valid.cert),
key: readFileSync(certs.valid.key),
ca: readFileSync(certs.ca),
rejectUnauthorized: false,
},
(res) => {
res.resume();
ok(res.statusCode !== 401, `Expected non-401 in fail-open mode (CRL unavailable). Got: ${res.statusCode}`);
res.on('end', () => resolve());
}
);
req.on('error', (err) => reject(new Error(`Request failed: ${err.message}`)));
req.end();
});
});
});
suite('CRL Certificate Verification - Fail-Closed with Timeout', (ctx: ContextWithHarper) => {
let certsPath: string;
let certs: CrlCertificates;
before(async () => {
certsPath = await mkdtemp(join(tmpdir(), 'harper-crl-timeout-'));
// No CRL server started - this will cause CRL fetch to fail
certs = await generateCrlCertificates(certsPath, '127.0.0.1', 19999);
await setupHarperWithFixture(ctx, FIXTURE_PATH, {
config: {
http: {
mtls: {
user: 'admin',
certificateVerification: {
failureMode: 'fail-closed',
crl: {
enabled: true,
timeout: 1000, // Very short timeout
cacheTtl: 604800000,
},
},
},
},
tls: {
certificateAuthority: certs.ca,
},
},
});
});
after(async () => {
await teardownHarper(ctx);
await rm(certsPath, { recursive: true, force: true, maxRetries: 3 });
});
test('should reject connection in fail-closed mode when CRL times out', async () => {
return new Promise<void>((resolve) => {
const req = https.request(
`https://${ctx.harper.hostname}:${HTTPS_PORT}/`,
{
cert: readFileSync(certs.valid.cert),
key: readFileSync(certs.valid.key),
ca: readFileSync(certs.ca),
rejectUnauthorized: false,
},
(res) => {
res.resume();
ok(res.statusCode === 401, `Expected 401 in fail-closed mode when CRL times out. Got: ${res.statusCode}`);
res.on('end', () => resolve());
}
);
req.on('error', (err: any) => {
// Connection-level rejection is also acceptable
ok(
err.code === 'ECONNRESET' ||
err.code === 'EPROTO' ||
err.message?.includes('socket hang up') ||
err.message?.includes('certificate') ||
err.message?.includes('closed'),
`Expected connection error in fail-closed mode, got: ${err.code || err.message}`
);
resolve();
});
req.end();
});
});
});