Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/browserView/electron-main/browserSessionTrust.ts
13397 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 { IApplicationStorageMainService } from '../../storage/electron-main/storageMainService.js';
7
import { StorageScope, StorageTarget } from '../../storage/common/storage.js';
8
import { IBrowserViewCertificateError } from '../common/browserView.js';
9
import type { BrowserSession } from './browserSession.js';
10
11
/** Key used to store trusted certificate data in the application storage. */
12
const STORAGE_KEY = 'browserView.sessionTrustData';
13
14
/** Trust entries expire after 1 week. */
15
const TRUST_DURATION_MS = 7 * 24 * 60 * 60 * 1000;
16
17
/**
18
* Shape of the JSON blob persisted under {@link STORAGE_KEY}.
19
* Top-level keys are session ids; each value holds the session's
20
* trusted certificates.
21
*/
22
interface PersistedTrustData {
23
[sessionId: string]: {
24
trustedCerts?: { host: string; fingerprint: string; expiresAt: number }[];
25
};
26
}
27
28
/**
29
* Public subset of {@link BrowserSessionTrust} exposed to consumers
30
* (e.g. {@link BrowserView}) that need to trust/untrust certificates
31
* or query certificate errors.
32
*/
33
export interface IBrowserSessionTrust {
34
trustCertificate(host: string, fingerprint: string): Promise<void>;
35
untrustCertificate(host: string, fingerprint: string): Promise<void>;
36
getCertificateError(url: string): IBrowserViewCertificateError | undefined;
37
installCertErrorHandler(webContents: Electron.WebContents): void;
38
}
39
40
/**
41
* Centralises all certificate and trust-related security logic for a
42
* browser session. Owns the trusted-certificate store, the cert-error
43
* cache, the `setCertificateVerifyProc` handler on the Electron session,
44
* and the per-`WebContents` `certificate-error` handler.
45
*/
46
export class BrowserSessionTrust implements IBrowserSessionTrust {
47
48
/**
49
* Trusted certificates stored as host → (fingerprint → expiration epoch ms).
50
* Entries are time-limited; see {@link TRUST_DURATION_MS}.
51
*/
52
private readonly _trustedCertificates = new Map<string, Map<string, /* expiresAt */ number>>();
53
54
/**
55
* Last known certificate per host (hostname → { fingerprint, error }).
56
* Populated by `setCertificateVerifyProc` which fires for every TLS
57
* handshake, not just errors. This lets us look up cert status for a
58
* URL even after Chromium has cached the allow decision.
59
*/
60
private readonly _certErrors = new Map<string, { certificate: Electron.Certificate; error: string }>();
61
62
/**
63
* Application storage service for persisting trusted certificates
64
* across restarts. Set via {@link connectStorage}; `undefined` until then.
65
*/
66
private _storage: IApplicationStorageMainService | undefined;
67
68
constructor(
69
private readonly _session: BrowserSession,
70
) {
71
this._installCertVerifyProc();
72
}
73
74
/**
75
* Install the session-level certificate verification callback that records cert errors.
76
* This does not grant any trust by itself; it just populates the `_certErrors` cache.
77
*/
78
private _installCertVerifyProc(): void {
79
this._session.electronSession.setCertificateVerifyProc((request, callback) => {
80
const { hostname, errorCode, certificate, verificationResult } = request;
81
82
if (errorCode !== 0) {
83
this._certErrors.set(hostname, { certificate, error: verificationResult });
84
} else {
85
this._certErrors.delete(hostname);
86
}
87
88
return callback(-3); // Always use default handling from Chromium
89
});
90
}
91
92
/**
93
* Install a `certificate-error` handler on a {@link Electron.WebContents}
94
* so that user-trusted certificates are accepted at the page level.
95
*/
96
installCertErrorHandler(webContents: Electron.WebContents): void {
97
webContents.on('certificate-error', (event, url, _error, certificate, callback) => {
98
event.preventDefault();
99
100
const host = URL.parse(url)?.hostname;
101
if (!host) {
102
return callback(false);
103
}
104
105
if (this.isCertificateTrusted(host, certificate.fingerprint)) {
106
return callback(true);
107
}
108
109
return callback(false);
110
});
111
}
112
113
/**
114
* Look up the certificate status for a URL by extracting the host and
115
* checking whether we have a last-known bad cert that was user-trusted.
116
* Returns the cert error info if the host has a bad cert that was trusted,
117
* or `undefined` if the cert is valid or unknown.
118
*/
119
getCertificateError(url: string): IBrowserViewCertificateError | undefined {
120
const parsed = URL.parse(url);
121
if (!parsed || parsed.protocol !== 'https:') {
122
return undefined;
123
}
124
125
const host = parsed.hostname;
126
if (!host) {
127
return undefined;
128
}
129
130
const known = this._certErrors.get(host);
131
if (!known) {
132
return undefined;
133
}
134
135
const cert = known.certificate;
136
return {
137
host,
138
fingerprint: cert.fingerprint,
139
error: known.error,
140
url,
141
hasTrustedException: this.isCertificateTrusted(host, cert.fingerprint),
142
issuerName: cert.issuerName,
143
subjectName: cert.subjectName,
144
validStart: cert.validStart,
145
validExpiry: cert.validExpiry,
146
};
147
}
148
149
/**
150
* Trust a certificate identified by host and SHA-256 fingerprint.
151
*/
152
async trustCertificate(host: string, fingerprint: string): Promise<void> {
153
let entries = this._trustedCertificates.get(host);
154
if (!entries) {
155
entries = new Map();
156
this._trustedCertificates.set(host, entries);
157
}
158
entries.set(fingerprint, Date.now() + TRUST_DURATION_MS);
159
this.writeStorage();
160
}
161
162
/**
163
* Revoke trust for a certificate identified by host and fingerprint.
164
*/
165
async untrustCertificate(host: string, fingerprint: string): Promise<void> {
166
const entries = this._trustedCertificates.get(host);
167
if (entries && entries.delete(fingerprint)) {
168
if (entries.size === 0) {
169
this._trustedCertificates.delete(host);
170
}
171
} else {
172
throw new Error(`Certificate not found: host=${host} fingerprint=${fingerprint}`);
173
}
174
this.writeStorage();
175
// Important: close all connections since they may be using the now-untrusted cert.
176
await this._session.electronSession.closeAllConnections();
177
}
178
179
/**
180
* Check whether a certificate is trusted for a given host.
181
*/
182
isCertificateTrusted(host: string, fingerprint: string): boolean {
183
const expiresAt = this._trustedCertificates.get(host)?.get(fingerprint);
184
if (expiresAt === undefined) {
185
return false;
186
}
187
if (Date.now() > expiresAt) {
188
return false;
189
}
190
return true;
191
}
192
193
/**
194
* Connect application storage so that trusted certificates are
195
* persisted across restarts. Restores any previously-saved data on
196
* first call; subsequent calls are no-ops.
197
*/
198
connectStorage(storage: IApplicationStorageMainService): void {
199
if (this._storage) {
200
return; // already connected
201
}
202
this._storage = storage;
203
this.readStorage();
204
}
205
206
/**
207
* Clear all trust state: in-memory certs, cert-error cache, persisted
208
* data, and close open connections that may be using now-untrusted certs.
209
*/
210
async clear(): Promise<void> {
211
this._trustedCertificates.clear();
212
this._certErrors.clear();
213
this.writeStorage();
214
// Important: close all connections since they may be using now-untrusted certs.
215
await this._session.electronSession.closeAllConnections();
216
}
217
218
// #region Persistence helpers
219
220
/**
221
* Restore trusted certificates from application storage.
222
*/
223
private readStorage(): void {
224
const storage = this._storage;
225
if (!storage) {
226
return;
227
}
228
229
const raw = storage.get(STORAGE_KEY, StorageScope.APPLICATION);
230
if (!raw) {
231
return;
232
}
233
234
const now = Date.now();
235
let pruned = false;
236
try {
237
const all: PersistedTrustData = JSON.parse(raw);
238
const certs = all[this._session.id]?.trustedCerts;
239
if (certs) {
240
for (const { host, fingerprint, expiresAt } of certs) {
241
if (expiresAt > now) {
242
let entries = this._trustedCertificates.get(host);
243
if (!entries) {
244
entries = new Map();
245
this._trustedCertificates.set(host, entries);
246
}
247
entries.set(fingerprint, expiresAt);
248
} else {
249
pruned = true;
250
}
251
}
252
}
253
} catch {
254
// Corrupt data — ignore
255
}
256
257
// Flush expired entries from storage
258
if (pruned) {
259
this.writeStorage();
260
}
261
}
262
263
/**
264
* Write trusted certificates to application storage.
265
* The single storage key holds **all** sessions' data so that we can
266
* clean up stale entries atomically.
267
*/
268
private writeStorage(): void {
269
const storage = this._storage;
270
if (!storage) {
271
return;
272
}
273
274
// Read existing blob (other sessions may have data too)
275
let all: PersistedTrustData = {};
276
try {
277
const raw = storage.get(STORAGE_KEY, StorageScope.APPLICATION);
278
if (raw) {
279
all = JSON.parse(raw);
280
}
281
} catch {
282
// Overwrite corrupt data
283
}
284
285
// Ensure this session's entry exists
286
if (!all[this._session.id]) {
287
all[this._session.id] = {};
288
}
289
290
// Update the trusted certs slice
291
if (this._trustedCertificates.size === 0) {
292
delete all[this._session.id].trustedCerts;
293
} else {
294
const certs: { host: string; fingerprint: string; expiresAt: number }[] = [];
295
for (const [host, entries] of this._trustedCertificates) {
296
for (const [fingerprint, expiresAt] of entries) {
297
certs.push({ host, fingerprint, expiresAt });
298
}
299
}
300
all[this._session.id].trustedCerts = certs;
301
}
302
303
// Remove empty session entries
304
if (Object.keys(all[this._session.id]).length === 0) {
305
delete all[this._session.id];
306
}
307
308
// Write back (or remove if empty)
309
if (Object.keys(all).length === 0) {
310
storage.remove(STORAGE_KEY, StorageScope.APPLICATION);
311
} else {
312
storage.store(STORAGE_KEY, JSON.stringify(all), StorageScope.APPLICATION, StorageTarget.MACHINE);
313
}
314
}
315
316
// #endregion
317
}
318
319