@gjsify/tls 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/README.md +27 -0
- package/lib/esm/index.js +377 -0
- package/lib/types/index.d.ts +121 -0
- package/package.json +44 -0
- package/src/index.spec.ts +674 -0
- package/src/index.ts +510 -0
- package/src/test.mts +6 -0
- package/tsconfig.json +31 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
// Ported from refs/node-test/parallel/test-tls-check-server-identity.js,
|
|
2
|
+
// test-tls-basic-validations.js, refs/bun/test/js/node/tls/
|
|
3
|
+
// Original: MIT license, Node.js contributors
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from '@gjsify/unit';
|
|
6
|
+
import tls, {
|
|
7
|
+
TLSSocket,
|
|
8
|
+
connect,
|
|
9
|
+
createServer,
|
|
10
|
+
createSecureContext,
|
|
11
|
+
checkServerIdentity,
|
|
12
|
+
getCiphers,
|
|
13
|
+
rootCertificates,
|
|
14
|
+
DEFAULT_MIN_VERSION,
|
|
15
|
+
DEFAULT_MAX_VERSION,
|
|
16
|
+
DEFAULT_CIPHERS,
|
|
17
|
+
} from 'node:tls';
|
|
18
|
+
|
|
19
|
+
// Our implementation exports TLSServer, Node.js exports Server
|
|
20
|
+
const TLSServer = (tls as any).TLSServer || (tls as any).Server;
|
|
21
|
+
|
|
22
|
+
export default async () => {
|
|
23
|
+
await describe('tls', async () => {
|
|
24
|
+
|
|
25
|
+
// ===================== Constants =====================
|
|
26
|
+
await describe('constants', async () => {
|
|
27
|
+
await it('should export DEFAULT_MIN_VERSION as TLSv1.2', async () => {
|
|
28
|
+
expect(DEFAULT_MIN_VERSION).toBe('TLSv1.2');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await it('should export DEFAULT_MAX_VERSION as TLSv1.3', async () => {
|
|
32
|
+
expect(DEFAULT_MAX_VERSION).toBe('TLSv1.3');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await it('DEFAULT_CIPHERS should be a non-empty string', async () => {
|
|
36
|
+
expect(typeof DEFAULT_CIPHERS).toBe('string');
|
|
37
|
+
expect(DEFAULT_CIPHERS.length).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await it('DEFAULT_CIPHERS should contain AES', async () => {
|
|
41
|
+
expect(DEFAULT_CIPHERS).toContain('AES');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await it('DEFAULT_CIPHERS should be colon-separated', async () => {
|
|
45
|
+
expect(DEFAULT_CIPHERS).toContain(':');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ===================== Module exports =====================
|
|
50
|
+
await describe('exports', async () => {
|
|
51
|
+
await it('should export TLSSocket as a function', async () => {
|
|
52
|
+
expect(typeof TLSSocket).toBe('function');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await it('should export connect as a function', async () => {
|
|
56
|
+
expect(typeof connect).toBe('function');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await it('should export createSecureContext as a function', async () => {
|
|
60
|
+
expect(typeof createSecureContext).toBe('function');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await it('should export rootCertificates as an array', async () => {
|
|
64
|
+
expect(Array.isArray(rootCertificates)).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await it('should export checkServerIdentity as a function', async () => {
|
|
68
|
+
expect(typeof checkServerIdentity).toBe('function');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await it('should export getCiphers as a function', async () => {
|
|
72
|
+
expect(typeof getCiphers).toBe('function');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await it('should export createServer as a function', async () => {
|
|
76
|
+
expect(typeof createServer).toBe('function');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await it('should have all exports on default export', async () => {
|
|
80
|
+
expect(typeof tls.TLSSocket).toBe('function');
|
|
81
|
+
expect(typeof tls.connect).toBe('function');
|
|
82
|
+
expect(typeof tls.createSecureContext).toBe('function');
|
|
83
|
+
expect(typeof tls.checkServerIdentity).toBe('function');
|
|
84
|
+
expect(typeof tls.getCiphers).toBe('function');
|
|
85
|
+
expect(typeof tls.createServer).toBe('function');
|
|
86
|
+
expect(Array.isArray(tls.rootCertificates)).toBe(true);
|
|
87
|
+
expect(tls.DEFAULT_MIN_VERSION).toBe('TLSv1.2');
|
|
88
|
+
expect(tls.DEFAULT_MAX_VERSION).toBe('TLSv1.3');
|
|
89
|
+
expect(typeof tls.DEFAULT_CIPHERS).toBe('string');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ===================== TLSSocket =====================
|
|
94
|
+
await describe('TLSSocket', async () => {
|
|
95
|
+
await it('should be constructable', async () => {
|
|
96
|
+
const socket = new TLSSocket(undefined as any);
|
|
97
|
+
expect(socket).toBeDefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await it('should have encrypted property set to true', async () => {
|
|
101
|
+
const socket = new TLSSocket(undefined as any);
|
|
102
|
+
expect(socket.encrypted).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await it('should have authorized property as boolean', async () => {
|
|
106
|
+
const socket = new TLSSocket(undefined as any);
|
|
107
|
+
expect(typeof socket.authorized).toBe('boolean');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await it('authorized should default to false', async () => {
|
|
111
|
+
const socket = new TLSSocket(undefined as any);
|
|
112
|
+
expect(socket.authorized).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await it('should have getPeerCertificate method', async () => {
|
|
116
|
+
const socket = new TLSSocket(undefined as any);
|
|
117
|
+
expect(typeof socket.getPeerCertificate).toBe('function');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await it('should have getProtocol method', async () => {
|
|
121
|
+
const socket = new TLSSocket(undefined as any);
|
|
122
|
+
expect(typeof socket.getProtocol).toBe('function');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await it('should have getCipher method', async () => {
|
|
126
|
+
const socket = new TLSSocket(undefined as any);
|
|
127
|
+
expect(typeof socket.getCipher).toBe('function');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await it('should have alpnProtocol property', async () => {
|
|
131
|
+
const socket = new TLSSocket(undefined as any);
|
|
132
|
+
// Node.js: alpnProtocol is a property (null or string), our impl: false or string
|
|
133
|
+
const val = (socket as any).alpnProtocol;
|
|
134
|
+
expect(val === false || val === null || typeof val === 'string').toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await it('getPeerCertificate should return object when not connected', async () => {
|
|
138
|
+
const socket = new TLSSocket(undefined as any);
|
|
139
|
+
const cert = socket.getPeerCertificate();
|
|
140
|
+
expect(typeof cert).toBe('object');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await it('getProtocol should return null when not connected', async () => {
|
|
144
|
+
const socket = new TLSSocket(undefined as any);
|
|
145
|
+
const proto = socket.getProtocol();
|
|
146
|
+
expect(proto === null || typeof proto === 'string').toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await it('getCipher should return null when not connected', async () => {
|
|
150
|
+
const socket = new TLSSocket(undefined as any);
|
|
151
|
+
const cipher = socket.getCipher();
|
|
152
|
+
expect(cipher === null || cipher === undefined || typeof cipher === 'object').toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await it('alpnProtocol should default to false', async () => {
|
|
156
|
+
const socket = new TLSSocket(undefined as any);
|
|
157
|
+
expect((socket as any).alpnProtocol === false || (socket as any).alpnProtocol === null).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await it('authorizationError should be undefined initially', async () => {
|
|
161
|
+
const socket = new TLSSocket(undefined as any);
|
|
162
|
+
expect(socket.authorizationError === undefined || socket.authorizationError === null).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await it('should extend Socket (EventEmitter)', async () => {
|
|
166
|
+
const socket = new TLSSocket(undefined as any);
|
|
167
|
+
expect(typeof socket.on).toBe('function');
|
|
168
|
+
expect(typeof socket.emit).toBe('function');
|
|
169
|
+
expect(typeof socket.once).toBe('function');
|
|
170
|
+
expect(typeof socket.removeListener).toBe('function');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await it('should have destroy method', async () => {
|
|
174
|
+
const socket = new TLSSocket(undefined as any);
|
|
175
|
+
expect(typeof socket.destroy).toBe('function');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await it('should have write method', async () => {
|
|
179
|
+
const socket = new TLSSocket(undefined as any);
|
|
180
|
+
expect(typeof socket.write).toBe('function');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await it('should have end method', async () => {
|
|
184
|
+
const socket = new TLSSocket(undefined as any);
|
|
185
|
+
expect(typeof socket.end).toBe('function');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ===================== TLSServer / createServer =====================
|
|
190
|
+
await describe('TLSServer', async () => {
|
|
191
|
+
await it('should export TLSServer as a function', async () => {
|
|
192
|
+
expect(typeof TLSServer).toBe('function');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await it('should export createServer as a function', async () => {
|
|
196
|
+
expect(typeof createServer).toBe('function');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await it('should be constructable', async () => {
|
|
200
|
+
const server = new TLSServer();
|
|
201
|
+
expect(server).toBeDefined();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await it('createServer should return a TLSServer', async () => {
|
|
205
|
+
const server = createServer();
|
|
206
|
+
expect(server).toBeDefined();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await it('createServer should accept a listener', async () => {
|
|
210
|
+
const server = createServer(() => {});
|
|
211
|
+
expect(server).toBeDefined();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await it('createServer should accept options and listener', async () => {
|
|
215
|
+
const server = createServer({ cert: '', key: '' }, () => {});
|
|
216
|
+
expect(server).toBeDefined();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await it('should have addContext method', async () => {
|
|
220
|
+
const server = new TLSServer();
|
|
221
|
+
expect(typeof server.addContext).toBe('function');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
await it('should have listen method', async () => {
|
|
225
|
+
const server = new TLSServer();
|
|
226
|
+
expect(typeof server.listen).toBe('function');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
await it('should have close method', async () => {
|
|
230
|
+
const server = new TLSServer();
|
|
231
|
+
expect(typeof server.close).toBe('function');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await it('should have address method', async () => {
|
|
235
|
+
const server = new TLSServer();
|
|
236
|
+
expect(typeof server.address).toBe('function');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await it('should be an EventEmitter', async () => {
|
|
240
|
+
const server = new TLSServer();
|
|
241
|
+
expect(typeof server.on).toBe('function');
|
|
242
|
+
expect(typeof server.emit).toBe('function');
|
|
243
|
+
expect(typeof server.once).toBe('function');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ===================== createSecureContext =====================
|
|
248
|
+
await describe('createSecureContext', async () => {
|
|
249
|
+
await it('should return a context object', async () => {
|
|
250
|
+
const ctx = createSecureContext();
|
|
251
|
+
expect(ctx).toBeDefined();
|
|
252
|
+
expect(typeof ctx).toBe('object');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await it('should accept empty options', async () => {
|
|
256
|
+
const ctx = createSecureContext({});
|
|
257
|
+
expect(ctx).toBeDefined();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await it('should have context property', async () => {
|
|
261
|
+
const ctx = createSecureContext();
|
|
262
|
+
expect(ctx.context !== undefined).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await it('should return same type for repeated calls', async () => {
|
|
266
|
+
const ctx1 = createSecureContext();
|
|
267
|
+
const ctx2 = createSecureContext();
|
|
268
|
+
expect(typeof ctx1).toBe(typeof ctx2);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// ===================== getCiphers =====================
|
|
273
|
+
await describe('getCiphers', async () => {
|
|
274
|
+
await it('should return an array of strings', async () => {
|
|
275
|
+
const ciphers = getCiphers();
|
|
276
|
+
expect(Array.isArray(ciphers)).toBe(true);
|
|
277
|
+
expect(ciphers.length).toBeGreaterThan(0);
|
|
278
|
+
for (const c of ciphers) {
|
|
279
|
+
expect(typeof c).toBe('string');
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await it('should be on the default export', async () => {
|
|
284
|
+
expect(typeof tls.getCiphers).toBe('function');
|
|
285
|
+
const ciphers = tls.getCiphers();
|
|
286
|
+
expect(Array.isArray(ciphers)).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await it('should contain lowercase cipher names', async () => {
|
|
290
|
+
const ciphers = getCiphers();
|
|
291
|
+
for (const c of ciphers) {
|
|
292
|
+
expect(c).toBe(c.toLowerCase());
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await it('should contain aes ciphers', async () => {
|
|
297
|
+
const ciphers = getCiphers();
|
|
298
|
+
const hasAes = ciphers.some(c => c.includes('aes'));
|
|
299
|
+
expect(hasAes).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await it('should not contain duplicates', async () => {
|
|
303
|
+
const ciphers = getCiphers();
|
|
304
|
+
const unique = new Set(ciphers);
|
|
305
|
+
expect(unique.size).toBe(ciphers.length);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ===================== connect =====================
|
|
310
|
+
await describe('tls.connect', async () => {
|
|
311
|
+
await it('should be a function', async () => {
|
|
312
|
+
expect(typeof connect).toBe('function');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await it('should be on the default export', async () => {
|
|
316
|
+
expect(typeof tls.connect).toBe('function');
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ===================== checkServerIdentity =====================
|
|
321
|
+
// Comprehensive tests ported from refs/node-test/parallel/test-tls-check-server-identity.js
|
|
322
|
+
await describe('checkServerIdentity', async () => {
|
|
323
|
+
|
|
324
|
+
// --- Basic function checks ---
|
|
325
|
+
await it('should be a function', async () => {
|
|
326
|
+
expect(typeof checkServerIdentity).toBe('function');
|
|
327
|
+
expect(typeof tls.checkServerIdentity).toBe('function');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// --- CN matching ---
|
|
331
|
+
await describe('CN matching', async () => {
|
|
332
|
+
await it('should return undefined for exact CN match', async () => {
|
|
333
|
+
const result = checkServerIdentity('a.com', { subject: { CN: 'a.com' } } as any);
|
|
334
|
+
expect(result).toBeUndefined();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
await it('should match CN case-insensitively', async () => {
|
|
338
|
+
const result = checkServerIdentity('a.com', { subject: { CN: 'A.COM' } } as any);
|
|
339
|
+
expect(result).toBeUndefined();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await it('should return error for non-matching CN', async () => {
|
|
343
|
+
const err = checkServerIdentity('a.com', { subject: { CN: 'b.com' } } as any);
|
|
344
|
+
expect(err instanceof Error).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await it('should handle trailing-dot FQDN in hostname', async () => {
|
|
348
|
+
const result = checkServerIdentity('a.com.', { subject: { CN: 'a.com' } } as any);
|
|
349
|
+
expect(result).toBeUndefined();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
await it('should handle trailing-dot FQDN in CN', async () => {
|
|
353
|
+
const result = checkServerIdentity('a.com', { subject: { CN: 'a.com.' } } as any);
|
|
354
|
+
expect(result).toBeUndefined();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
await it('should handle trailing dots on both sides', async () => {
|
|
358
|
+
const result = checkServerIdentity('a.com.', { subject: { CN: 'a.com.' } } as any);
|
|
359
|
+
expect(result).toBeUndefined();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
await it('should match array CN values', async () => {
|
|
363
|
+
const result = checkServerIdentity('b.com', {
|
|
364
|
+
subject: { CN: ['a.com', 'b.com'] },
|
|
365
|
+
} as any);
|
|
366
|
+
expect(result).toBeUndefined();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
await it('should fail when hostname not in CN array', async () => {
|
|
370
|
+
const err = checkServerIdentity('c.com', {
|
|
371
|
+
subject: { CN: ['a.com', 'b.com'] },
|
|
372
|
+
} as any);
|
|
373
|
+
expect(err instanceof Error).toBe(true);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
await it('should handle single-label hostname with single-label CN', async () => {
|
|
377
|
+
const result = checkServerIdentity('localhost', {
|
|
378
|
+
subject: { CN: 'localhost' },
|
|
379
|
+
} as any);
|
|
380
|
+
expect(result).toBeUndefined();
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// --- Wildcard matching ---
|
|
385
|
+
await describe('wildcard matching', async () => {
|
|
386
|
+
await it('should match *.example.com against sub.example.com', async () => {
|
|
387
|
+
const result = checkServerIdentity('sub.example.com', {
|
|
388
|
+
subject: {},
|
|
389
|
+
subjectaltname: 'DNS:*.example.com',
|
|
390
|
+
} as any);
|
|
391
|
+
expect(result).toBeUndefined();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
await it('should NOT match *.example.com against nested.sub.example.com', async () => {
|
|
395
|
+
const err = checkServerIdentity('nested.sub.example.com', {
|
|
396
|
+
subject: {},
|
|
397
|
+
subjectaltname: 'DNS:*.example.com',
|
|
398
|
+
} as any);
|
|
399
|
+
expect(err instanceof Error).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
await it('should NOT match *.example.com against example.com itself', async () => {
|
|
403
|
+
const err = checkServerIdentity('example.com', {
|
|
404
|
+
subject: {},
|
|
405
|
+
subjectaltname: 'DNS:*.example.com',
|
|
406
|
+
} as any);
|
|
407
|
+
expect(err instanceof Error).toBe(true);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
await it('should match wildcard CN when no SANs present', async () => {
|
|
411
|
+
const result = checkServerIdentity('foo.example.com', {
|
|
412
|
+
subject: { CN: '*.example.com' },
|
|
413
|
+
} as any);
|
|
414
|
+
expect(result).toBeUndefined();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
await it('wildcard CN should NOT match two-level deep', async () => {
|
|
418
|
+
const err = checkServerIdentity('a.b.example.com', {
|
|
419
|
+
subject: { CN: '*.example.com' },
|
|
420
|
+
} as any);
|
|
421
|
+
expect(err instanceof Error).toBe(true);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
await it('should match wildcard with different subdomains', async () => {
|
|
425
|
+
const result1 = checkServerIdentity('foo.example.com', {
|
|
426
|
+
subject: {},
|
|
427
|
+
subjectaltname: 'DNS:*.example.com',
|
|
428
|
+
} as any);
|
|
429
|
+
expect(result1).toBeUndefined();
|
|
430
|
+
|
|
431
|
+
const result2 = checkServerIdentity('bar.example.com', {
|
|
432
|
+
subject: {},
|
|
433
|
+
subjectaltname: 'DNS:*.example.com',
|
|
434
|
+
} as any);
|
|
435
|
+
expect(result2).toBeUndefined();
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// --- DNS SAN matching ---
|
|
440
|
+
await describe('DNS SAN matching', async () => {
|
|
441
|
+
await it('should match hostname in DNS SAN', async () => {
|
|
442
|
+
const result = checkServerIdentity('foo.example.com', {
|
|
443
|
+
subject: { CN: 'wrong.com' },
|
|
444
|
+
subjectaltname: 'DNS:foo.example.com',
|
|
445
|
+
} as any);
|
|
446
|
+
expect(result).toBeUndefined();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
await it('SAN takes precedence over CN', async () => {
|
|
450
|
+
// When SANs are present, CN should be ignored
|
|
451
|
+
const err = checkServerIdentity('wrong.com', {
|
|
452
|
+
subject: { CN: 'wrong.com' },
|
|
453
|
+
subjectaltname: 'DNS:right.com',
|
|
454
|
+
} as any);
|
|
455
|
+
expect(err instanceof Error).toBe(true);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
await it('should handle multiple DNS SANs', async () => {
|
|
459
|
+
const result = checkServerIdentity('b.com', {
|
|
460
|
+
subject: { CN: 'a.com' },
|
|
461
|
+
subjectaltname: 'DNS:a.com, DNS:b.com',
|
|
462
|
+
} as any);
|
|
463
|
+
expect(result).toBeUndefined();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
await it('should fail when hostname not in any DNS SAN', async () => {
|
|
467
|
+
const err = checkServerIdentity('c.com', {
|
|
468
|
+
subject: { CN: 'c.com' },
|
|
469
|
+
subjectaltname: 'DNS:a.com, DNS:b.com',
|
|
470
|
+
} as any);
|
|
471
|
+
expect(err instanceof Error).toBe(true);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
await it('should match wildcard DNS SAN', async () => {
|
|
475
|
+
const result = checkServerIdentity('bar.example.com', {
|
|
476
|
+
subject: { CN: 'wrong.com' },
|
|
477
|
+
subjectaltname: 'DNS:*.example.com',
|
|
478
|
+
} as any);
|
|
479
|
+
expect(result).toBeUndefined();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
await it('should handle mixed DNS and IP SANs', async () => {
|
|
483
|
+
const result = checkServerIdentity('example.com', {
|
|
484
|
+
subject: {},
|
|
485
|
+
subjectaltname: 'DNS:example.com, IP Address:1.2.3.4',
|
|
486
|
+
} as any);
|
|
487
|
+
expect(result).toBeUndefined();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
await it('should handle DNS SAN with trailing whitespace', async () => {
|
|
491
|
+
const result = checkServerIdentity('example.com', {
|
|
492
|
+
subject: {},
|
|
493
|
+
subjectaltname: 'DNS:example.com ',
|
|
494
|
+
} as any);
|
|
495
|
+
// May or may not match depending on trimming
|
|
496
|
+
expect(result === undefined || result instanceof Error).toBe(true);
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// --- IP address matching ---
|
|
501
|
+
await describe('IP address matching', async () => {
|
|
502
|
+
await it('should match IPv4 in IP Address SAN', async () => {
|
|
503
|
+
const result = checkServerIdentity('8.8.8.8', {
|
|
504
|
+
subject: { CN: '8.8.8.8' },
|
|
505
|
+
subjectaltname: 'IP Address:8.8.8.8',
|
|
506
|
+
} as any);
|
|
507
|
+
expect(result).toBeUndefined();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
await it('should fail IPv4 not in IP Address SAN', async () => {
|
|
511
|
+
const err = checkServerIdentity('8.8.4.4', {
|
|
512
|
+
subject: { CN: '8.8.4.4' },
|
|
513
|
+
subjectaltname: 'IP Address:8.8.8.8',
|
|
514
|
+
} as any);
|
|
515
|
+
expect(err instanceof Error).toBe(true);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
await it('should fail IPv4 with only CN (no SAN IP entry)', async () => {
|
|
519
|
+
const err = checkServerIdentity('8.8.8.8', {
|
|
520
|
+
subject: { CN: '8.8.8.8' },
|
|
521
|
+
} as any);
|
|
522
|
+
expect(err instanceof Error).toBe(true);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
await it('should match multiple IP addresses in SAN', async () => {
|
|
526
|
+
const result = checkServerIdentity('1.2.3.4', {
|
|
527
|
+
subject: {},
|
|
528
|
+
subjectaltname: 'IP Address:1.2.3.4, IP Address:5.6.7.8',
|
|
529
|
+
} as any);
|
|
530
|
+
expect(result).toBeUndefined();
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
await it('should match second IP in SAN', async () => {
|
|
534
|
+
const result = checkServerIdentity('5.6.7.8', {
|
|
535
|
+
subject: {},
|
|
536
|
+
subjectaltname: 'IP Address:1.2.3.4, IP Address:5.6.7.8',
|
|
537
|
+
} as any);
|
|
538
|
+
expect(result).toBeUndefined();
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
await it('should handle IPv6 in IP Address SAN', async () => {
|
|
542
|
+
const result = checkServerIdentity('::1', {
|
|
543
|
+
subject: {},
|
|
544
|
+
subjectaltname: 'IP Address:::1',
|
|
545
|
+
} as any);
|
|
546
|
+
expect(result).toBeUndefined();
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
await it('should fail IPv4 in DNS SAN (IP should use IP Address SAN)', async () => {
|
|
550
|
+
const err = checkServerIdentity('1.2.3.4', {
|
|
551
|
+
subject: {},
|
|
552
|
+
subjectaltname: 'DNS:1.2.3.4',
|
|
553
|
+
} as any);
|
|
554
|
+
expect(err instanceof Error).toBe(true);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// --- Error object properties ---
|
|
559
|
+
await describe('error properties', async () => {
|
|
560
|
+
await it('should have reason property on error', async () => {
|
|
561
|
+
const err = checkServerIdentity('a.com', { subject: { CN: 'b.com' } } as any) as any;
|
|
562
|
+
expect(err instanceof Error).toBe(true);
|
|
563
|
+
expect(typeof err.reason).toBe('string');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
await it('should have host property on error', async () => {
|
|
567
|
+
const err = checkServerIdentity('a.com', { subject: { CN: 'b.com' } } as any) as any;
|
|
568
|
+
expect(err.host).toBe('a.com');
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
await it('should have cert property on error', async () => {
|
|
572
|
+
const cert = { subject: { CN: 'b.com' } } as any;
|
|
573
|
+
const err = checkServerIdentity('a.com', cert) as any;
|
|
574
|
+
expect(err.cert).toBe(cert);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
await it('error message should contain hostname for CN mismatch', async () => {
|
|
578
|
+
const err = checkServerIdentity('a.com', { subject: { CN: 'b.com' } } as any);
|
|
579
|
+
expect(err instanceof Error).toBe(true);
|
|
580
|
+
expect((err as Error).message).toContain('a.com');
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
await it('error message should contain IP for IP mismatch', async () => {
|
|
584
|
+
const err = checkServerIdentity('8.8.8.8', {
|
|
585
|
+
subject: {},
|
|
586
|
+
subjectaltname: 'IP Address:1.2.3.4',
|
|
587
|
+
} as any);
|
|
588
|
+
expect(err instanceof Error).toBe(true);
|
|
589
|
+
expect((err as Error).message).toContain('8.8.8.8');
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// --- Edge cases ---
|
|
594
|
+
await describe('edge cases', async () => {
|
|
595
|
+
await it('should return error when cert has no subject and no altnames', async () => {
|
|
596
|
+
const err = checkServerIdentity('a.com', {} as any);
|
|
597
|
+
expect(err instanceof Error).toBe(true);
|
|
598
|
+
expect((err as Error).message).toContain('DNS');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
await it('should handle false-y host values', async () => {
|
|
602
|
+
const err = checkServerIdentity(false as unknown as string, {
|
|
603
|
+
subject: { CN: 'a.com' },
|
|
604
|
+
} as any);
|
|
605
|
+
expect(err instanceof Error).toBe(true);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
await it('should handle empty string hostname', async () => {
|
|
609
|
+
const err = checkServerIdentity('', { subject: { CN: 'a.com' } } as any);
|
|
610
|
+
expect(err instanceof Error).toBe(true);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
await it('should handle empty CN', async () => {
|
|
614
|
+
const err = checkServerIdentity('a.com', { subject: { CN: '' } } as any);
|
|
615
|
+
expect(err instanceof Error).toBe(true);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
await it('should handle undefined CN', async () => {
|
|
619
|
+
const err = checkServerIdentity('a.com', { subject: {} } as any);
|
|
620
|
+
expect(err instanceof Error).toBe(true);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
await it('should handle empty subjectaltname string', async () => {
|
|
624
|
+
const err = checkServerIdentity('a.com', {
|
|
625
|
+
subject: { CN: 'a.com' },
|
|
626
|
+
subjectaltname: '',
|
|
627
|
+
} as any);
|
|
628
|
+
// Empty altname means CN fallback
|
|
629
|
+
expect(err === undefined || err instanceof Error).toBe(true);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
await it('should handle numeric hostname as string', async () => {
|
|
633
|
+
const result = checkServerIdentity('127.0.0.1', {
|
|
634
|
+
subject: {},
|
|
635
|
+
subjectaltname: 'IP Address:127.0.0.1',
|
|
636
|
+
} as any);
|
|
637
|
+
expect(result).toBeUndefined();
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
await it('should handle cert with only URI SAN (no DNS, no IP)', async () => {
|
|
641
|
+
const err = checkServerIdentity('a.com', {
|
|
642
|
+
subject: {},
|
|
643
|
+
subjectaltname: 'URI:https://a.com',
|
|
644
|
+
} as any);
|
|
645
|
+
// URI SANs don't count for hostname verification
|
|
646
|
+
expect(err instanceof Error).toBe(true);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
await it('should handle cert with email SAN only', async () => {
|
|
650
|
+
const err = checkServerIdentity('a.com', {
|
|
651
|
+
subject: {},
|
|
652
|
+
subjectaltname: 'email:admin@a.com',
|
|
653
|
+
} as any);
|
|
654
|
+
expect(err instanceof Error).toBe(true);
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// ===================== rootCertificates =====================
|
|
660
|
+
await describe('rootCertificates', async () => {
|
|
661
|
+
await it('should be an array', async () => {
|
|
662
|
+
expect(Array.isArray(rootCertificates)).toBe(true);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
await it('should be on default export', async () => {
|
|
666
|
+
expect(Array.isArray(tls.rootCertificates)).toBe(true);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
await it('should be the same reference on default and named export', async () => {
|
|
670
|
+
expect(tls.rootCertificates).toBe(rootCertificates);
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
};
|