Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/browserView/test/electron-main/browserSessionTrust.test.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import assert from 'assert';
7
import * as sinon from 'sinon';
8
import { EventEmitter } from 'events';
9
import { runWithFakedTimers } from '../../../../base/test/common/timeTravelScheduler.js';
10
import { StorageScope, StorageTarget } from '../../../storage/common/storage.js';
11
import { IApplicationStorageMainService } from '../../../storage/electron-main/storageMainService.js';
12
import { BrowserSessionTrust } from '../../electron-main/browserSessionTrust.js';
13
import type { BrowserSession } from '../../electron-main/browserSession.js';
14
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
15
16
const STORAGE_KEY = 'browserView.sessionTrustData';
17
const TRUST_DURATION_MS = 7 * 24 * 60 * 60 * 1000;
18
19
type CertificateVerifyProc = Parameters<Electron.Session['setCertificateVerifyProc']>[0];
20
type CertificateVerifyRequest = Parameters<NonNullable<CertificateVerifyProc>>[0];
21
22
class TestElectronSession {
23
readonly closeAllConnections = sinon.stub().resolves();
24
certificateVerifyProc: CertificateVerifyProc | undefined;
25
26
setCertificateVerifyProc(callback: CertificateVerifyProc): void {
27
this.certificateVerifyProc = callback;
28
}
29
30
asSession(): Electron.Session {
31
return this as unknown as Electron.Session;
32
}
33
}
34
35
class TestBrowserSession {
36
constructor(
37
readonly id: string,
38
readonly electronSession: Electron.Session,
39
) { }
40
41
asBrowserSession(): BrowserSession {
42
return this as unknown as BrowserSession;
43
}
44
}
45
46
class TestApplicationStorageMainService {
47
private readonly data = new Map<string, string>();
48
readonly store = sinon.stub<[string, string | number | boolean | object | null | undefined, StorageScope, StorageTarget], void>().callsFake((key, value) => {
49
this.data.set(key, String(value));
50
});
51
readonly remove = sinon.stub<[string, StorageScope], void>().callsFake(key => {
52
this.data.delete(key);
53
});
54
55
get(key: string, _scope: StorageScope, fallbackValue?: string): string | undefined {
56
return this.data.get(key) ?? fallbackValue;
57
}
58
59
seed(key: string, value: string): void {
60
this.data.set(key, value);
61
}
62
63
read(key: string): string | undefined {
64
return this.data.get(key);
65
}
66
67
asService(): IApplicationStorageMainService {
68
return this as unknown as IApplicationStorageMainService;
69
}
70
}
71
72
class TestWebContents extends EventEmitter {
73
asWebContents(): Electron.WebContents {
74
return this as unknown as Electron.WebContents;
75
}
76
}
77
78
function createTrust(sessionId = 'test-session'): {
79
trust: BrowserSessionTrust;
80
electronSession: TestElectronSession;
81
storage: TestApplicationStorageMainService;
82
} {
83
const electronSession = new TestElectronSession();
84
const browserSession = new TestBrowserSession(sessionId, electronSession.asSession());
85
const trust = new BrowserSessionTrust(browserSession.asBrowserSession());
86
const storage = new TestApplicationStorageMainService();
87
88
return { trust, electronSession, storage };
89
}
90
91
function createCertificate(fingerprint: string, extra?: Partial<Electron.Certificate>): Electron.Certificate {
92
return { fingerprint, issuerName: 'Test CA', subjectName: 'test.example.com', validStart: 0, validExpiry: 0, ...extra } as Electron.Certificate;
93
}
94
95
function invokeVerifyProc(
96
electronSession: TestElectronSession,
97
request: Partial<CertificateVerifyRequest> & { hostname: string; certificate: Electron.Certificate }
98
): number {
99
assert.ok(electronSession.certificateVerifyProc);
100
101
let result: number | undefined;
102
electronSession.certificateVerifyProc!({
103
errorCode: 0,
104
verificationResult: 'OK',
105
...request
106
} as CertificateVerifyRequest, value => {
107
result = value;
108
});
109
110
assert.notStrictEqual(result, undefined);
111
return result!;
112
}
113
114
suite('BrowserSessionTrust', () => {
115
teardown(() => {
116
sinon.restore();
117
});
118
119
test('installs certificate verify proc and tracks certificate errors', () => {
120
const { trust, electronSession } = createTrust();
121
122
const verificationResult = invokeVerifyProc(electronSession, {
123
hostname: 'example.com',
124
errorCode: -202,
125
verificationResult: 'net::ERR_CERT_AUTHORITY_INVALID',
126
certificate: createCertificate('abc123')
127
});
128
129
assert.strictEqual(verificationResult, -3);
130
assert.deepStrictEqual(trust.getCertificateError('https://example.com/path'), {
131
host: 'example.com',
132
fingerprint: 'abc123',
133
error: 'net::ERR_CERT_AUTHORITY_INVALID',
134
url: 'https://example.com/path',
135
hasTrustedException: false,
136
issuerName: 'Test CA',
137
subjectName: 'test.example.com',
138
validStart: 0,
139
validExpiry: 0,
140
});
141
142
invokeVerifyProc(electronSession, {
143
hostname: 'example.com',
144
certificate: createCertificate('abc123')
145
});
146
147
assert.strictEqual(trust.getCertificateError('https://example.com/path'), undefined);
148
});
149
150
test('trustCertificate persists data under the trust storage key', async () => {
151
const { trust, storage } = createTrust();
152
trust.connectStorage(storage.asService());
153
154
await trust.trustCertificate('example.com', 'abc123');
155
156
assert.strictEqual(storage.store.calledOnce, true);
157
assert.deepStrictEqual(storage.store.firstCall.args.slice(0, 4), [STORAGE_KEY, storage.read(STORAGE_KEY), StorageScope.APPLICATION, StorageTarget.MACHINE]);
158
159
const persisted = JSON.parse(storage.read(STORAGE_KEY)!);
160
assert.deepStrictEqual(persisted['test-session'].trustedCerts.map((entry: { host: string; fingerprint: string }) => ({ host: entry.host, fingerprint: entry.fingerprint })), [{ host: 'example.com', fingerprint: 'abc123' }]);
161
});
162
163
test('trustCertificate stores expiresAt relative to current time', async () => {
164
const clock = sinon.useFakeTimers({ now: Date.parse('2026-03-01T00:00:00.000Z') });
165
const { trust, storage } = createTrust();
166
trust.connectStorage(storage.asService());
167
168
await trust.trustCertificate('example.com', 'abc123');
169
170
const persisted = JSON.parse(storage.read(STORAGE_KEY)!);
171
const [entry] = persisted['test-session'].trustedCerts as { host: string; fingerprint: string; expiresAt: number }[];
172
assert.strictEqual(entry.host, 'example.com');
173
assert.strictEqual(entry.fingerprint, 'abc123');
174
assert.strictEqual(entry.expiresAt, Date.now() + TRUST_DURATION_MS);
175
176
clock.restore();
177
});
178
179
test('trust is valid at expiration and invalid after expiration', async () => {
180
const clock = sinon.useFakeTimers({ now: Date.parse('2026-03-01T00:00:00.000Z') });
181
const { trust, electronSession, storage } = createTrust();
182
const webContents = new TestWebContents();
183
trust.installCertErrorHandler(webContents.asWebContents());
184
trust.connectStorage(storage.asService());
185
await trust.trustCertificate('example.com', 'abc123');
186
electronSession.closeAllConnections.resetHistory();
187
188
// Prior to the expiration boundary, trust should still be valid
189
clock.tick(TRUST_DURATION_MS - 10);
190
let callbackResult: boolean | undefined;
191
const firstEvent = { preventDefault: sinon.spy() };
192
webContents.emit('certificate-error', firstEvent, 'https://example.com', 'ERR_CERT', createCertificate('abc123'), (value: boolean) => {
193
callbackResult = value;
194
});
195
assert.strictEqual(callbackResult, true);
196
197
// After expiration, trust should be revoked
198
clock.tick(20);
199
const secondEvent = { preventDefault: sinon.spy() };
200
webContents.emit('certificate-error', secondEvent, 'https://example.com', 'ERR_CERT', createCertificate('abc123'), (value: boolean) => {
201
callbackResult = value;
202
});
203
assert.strictEqual(callbackResult, false);
204
205
clock.restore();
206
});
207
208
test('connectStorage restores valid trust entries and prunes expired ones', () => runWithFakedTimers({ useFakeTimers: true }, async () => {
209
const { trust, storage } = createTrust();
210
const webContents = new TestWebContents();
211
trust.installCertErrorHandler(webContents.asWebContents());
212
storage.seed(STORAGE_KEY, JSON.stringify({
213
'test-session': {
214
trustedCerts: [
215
{ host: 'valid.example.com', fingerprint: 'valid', expiresAt: Date.now() + 1000 },
216
{ host: 'expired.example.com', fingerprint: 'expired', expiresAt: Date.now() - 1000 }
217
]
218
}
219
}));
220
221
trust.connectStorage(storage.asService());
222
223
let callbackResult: boolean | undefined;
224
const validEvent = { preventDefault: sinon.spy() };
225
webContents.emit('certificate-error', validEvent, 'https://valid.example.com', 'ERR_CERT', createCertificate('valid'), (value: boolean) => {
226
callbackResult = value;
227
});
228
assert.strictEqual(callbackResult, true);
229
230
const expiredEvent = { preventDefault: sinon.spy() };
231
webContents.emit('certificate-error', expiredEvent, 'https://expired.example.com', 'ERR_CERT', createCertificate('expired'), (value: boolean) => {
232
callbackResult = value;
233
});
234
assert.strictEqual(callbackResult, false);
235
236
const persisted = JSON.parse(storage.read(STORAGE_KEY)!);
237
assert.deepStrictEqual(persisted['test-session'].trustedCerts.map((entry: { host: string; fingerprint: string }) => ({ host: entry.host, fingerprint: entry.fingerprint })), [{ host: 'valid.example.com', fingerprint: 'valid' }]);
238
}));
239
240
test('stored and reloaded trust expires and is pruned', async () => {
241
const clock = sinon.useFakeTimers({ now: Date.parse('2026-03-01T00:00:00.000Z') });
242
243
const storage = new TestApplicationStorageMainService();
244
const firstSession = new TestElectronSession();
245
const firstBrowserSession = new TestBrowserSession('test-session', firstSession.asSession());
246
const firstTrust = new BrowserSessionTrust(firstBrowserSession.asBrowserSession());
247
firstTrust.connectStorage(storage.asService());
248
await firstTrust.trustCertificate('reload.example.com', 'reload-fingerprint');
249
250
clock.tick(TRUST_DURATION_MS + 1);
251
252
const secondSession = new TestElectronSession();
253
const secondBrowserSession = new TestBrowserSession('test-session', secondSession.asSession());
254
const secondTrust = new BrowserSessionTrust(secondBrowserSession.asBrowserSession());
255
const webContents = new TestWebContents();
256
secondTrust.installCertErrorHandler(webContents.asWebContents());
257
secondTrust.connectStorage(storage.asService());
258
259
let callbackResult: boolean | undefined;
260
const event = { preventDefault: sinon.spy() };
261
webContents.emit('certificate-error', event, 'https://reload.example.com', 'ERR_CERT', createCertificate('reload-fingerprint'), (value: boolean) => {
262
callbackResult = value;
263
});
264
assert.strictEqual(callbackResult, false);
265
assert.strictEqual(storage.read(STORAGE_KEY), undefined);
266
267
clock.restore();
268
});
269
270
test('untrustCertificate removes persisted trust and closes connections', async () => {
271
const { trust, electronSession, storage } = createTrust();
272
trust.connectStorage(storage.asService());
273
await trust.trustCertificate('example.com', 'abc123');
274
electronSession.closeAllConnections.resetHistory();
275
storage.store.resetHistory();
276
277
await trust.untrustCertificate('example.com', 'abc123');
278
279
assert.strictEqual(electronSession.closeAllConnections.calledOnce, true);
280
assert.strictEqual(storage.remove.calledOnceWithExactly(STORAGE_KEY, StorageScope.APPLICATION), true);
281
assert.strictEqual(storage.read(STORAGE_KEY), undefined);
282
});
283
284
test('untrustCertificate throws when certificate is not found', async () => {
285
const { trust, electronSession, storage } = createTrust();
286
trust.connectStorage(storage.asService());
287
288
await assert.rejects(
289
() => trust.untrustCertificate('missing.example.com', 'missing-fingerprint'),
290
error => {
291
assert.ok(error instanceof Error);
292
assert.strictEqual(error.message, 'Certificate not found: host=missing.example.com fingerprint=missing-fingerprint');
293
return true;
294
}
295
);
296
assert.strictEqual(electronSession.closeAllConnections.called, false);
297
});
298
299
test('clear removes trust, clears cert errors, and closes connections', async () => {
300
const { trust, electronSession, storage } = createTrust();
301
trust.connectStorage(storage.asService());
302
await trust.trustCertificate('example.com', 'abc123');
303
invokeVerifyProc(electronSession, {
304
hostname: 'example.com',
305
errorCode: -202,
306
verificationResult: 'net::ERR_CERT_COMMON_NAME_INVALID',
307
certificate: createCertificate('abc123')
308
});
309
310
await trust.clear();
311
312
assert.strictEqual(electronSession.closeAllConnections.calledOnce, true);
313
assert.strictEqual(trust.getCertificateError('https://example.com'), undefined);
314
assert.strictEqual(storage.read(STORAGE_KEY), undefined);
315
});
316
317
test('installCertErrorHandler only allows trusted certificates', async () => {
318
const { trust } = createTrust();
319
const webContents = new TestWebContents();
320
trust.installCertErrorHandler(webContents.asWebContents());
321
322
let callbackResult: boolean | undefined;
323
const firstEvent = { preventDefault: sinon.spy() };
324
webContents.emit('certificate-error', firstEvent, 'https://example.com', 'ERR_CERT', createCertificate('abc123'), (value: boolean) => {
325
callbackResult = value;
326
});
327
assert.strictEqual(callbackResult, false);
328
assert.strictEqual(firstEvent.preventDefault.calledOnce, true);
329
330
await trust.trustCertificate('example.com', 'abc123');
331
const secondEvent = { preventDefault: sinon.spy() };
332
webContents.emit('certificate-error', secondEvent, 'https://example.com', 'ERR_CERT', createCertificate('abc123'), (value: boolean) => {
333
callbackResult = value;
334
});
335
assert.strictEqual(callbackResult, true);
336
assert.strictEqual(secondEvent.preventDefault.calledOnce, true);
337
});
338
339
ensureNoDisposablesAreLeakedInTestSuite();
340
});
341
342