Path: blob/main/src/vs/platform/browserView/electron-main/browserSessionTrust.ts
13397 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { IApplicationStorageMainService } from '../../storage/electron-main/storageMainService.js';6import { StorageScope, StorageTarget } from '../../storage/common/storage.js';7import { IBrowserViewCertificateError } from '../common/browserView.js';8import type { BrowserSession } from './browserSession.js';910/** Key used to store trusted certificate data in the application storage. */11const STORAGE_KEY = 'browserView.sessionTrustData';1213/** Trust entries expire after 1 week. */14const TRUST_DURATION_MS = 7 * 24 * 60 * 60 * 1000;1516/**17* Shape of the JSON blob persisted under {@link STORAGE_KEY}.18* Top-level keys are session ids; each value holds the session's19* trusted certificates.20*/21interface PersistedTrustData {22[sessionId: string]: {23trustedCerts?: { host: string; fingerprint: string; expiresAt: number }[];24};25}2627/**28* Public subset of {@link BrowserSessionTrust} exposed to consumers29* (e.g. {@link BrowserView}) that need to trust/untrust certificates30* or query certificate errors.31*/32export interface IBrowserSessionTrust {33trustCertificate(host: string, fingerprint: string): Promise<void>;34untrustCertificate(host: string, fingerprint: string): Promise<void>;35getCertificateError(url: string): IBrowserViewCertificateError | undefined;36installCertErrorHandler(webContents: Electron.WebContents): void;37}3839/**40* Centralises all certificate and trust-related security logic for a41* browser session. Owns the trusted-certificate store, the cert-error42* cache, the `setCertificateVerifyProc` handler on the Electron session,43* and the per-`WebContents` `certificate-error` handler.44*/45export class BrowserSessionTrust implements IBrowserSessionTrust {4647/**48* Trusted certificates stored as host → (fingerprint → expiration epoch ms).49* Entries are time-limited; see {@link TRUST_DURATION_MS}.50*/51private readonly _trustedCertificates = new Map<string, Map<string, /* expiresAt */ number>>();5253/**54* Last known certificate per host (hostname → { fingerprint, error }).55* Populated by `setCertificateVerifyProc` which fires for every TLS56* handshake, not just errors. This lets us look up cert status for a57* URL even after Chromium has cached the allow decision.58*/59private readonly _certErrors = new Map<string, { certificate: Electron.Certificate; error: string }>();6061/**62* Application storage service for persisting trusted certificates63* across restarts. Set via {@link connectStorage}; `undefined` until then.64*/65private _storage: IApplicationStorageMainService | undefined;6667constructor(68private readonly _session: BrowserSession,69) {70this._installCertVerifyProc();71}7273/**74* Install the session-level certificate verification callback that records cert errors.75* This does not grant any trust by itself; it just populates the `_certErrors` cache.76*/77private _installCertVerifyProc(): void {78this._session.electronSession.setCertificateVerifyProc((request, callback) => {79const { hostname, errorCode, certificate, verificationResult } = request;8081if (errorCode !== 0) {82this._certErrors.set(hostname, { certificate, error: verificationResult });83} else {84this._certErrors.delete(hostname);85}8687return callback(-3); // Always use default handling from Chromium88});89}9091/**92* Install a `certificate-error` handler on a {@link Electron.WebContents}93* so that user-trusted certificates are accepted at the page level.94*/95installCertErrorHandler(webContents: Electron.WebContents): void {96webContents.on('certificate-error', (event, url, _error, certificate, callback) => {97event.preventDefault();9899const host = URL.parse(url)?.hostname;100if (!host) {101return callback(false);102}103104if (this.isCertificateTrusted(host, certificate.fingerprint)) {105return callback(true);106}107108return callback(false);109});110}111112/**113* Look up the certificate status for a URL by extracting the host and114* checking whether we have a last-known bad cert that was user-trusted.115* Returns the cert error info if the host has a bad cert that was trusted,116* or `undefined` if the cert is valid or unknown.117*/118getCertificateError(url: string): IBrowserViewCertificateError | undefined {119const parsed = URL.parse(url);120if (!parsed || parsed.protocol !== 'https:') {121return undefined;122}123124const host = parsed.hostname;125if (!host) {126return undefined;127}128129const known = this._certErrors.get(host);130if (!known) {131return undefined;132}133134const cert = known.certificate;135return {136host,137fingerprint: cert.fingerprint,138error: known.error,139url,140hasTrustedException: this.isCertificateTrusted(host, cert.fingerprint),141issuerName: cert.issuerName,142subjectName: cert.subjectName,143validStart: cert.validStart,144validExpiry: cert.validExpiry,145};146}147148/**149* Trust a certificate identified by host and SHA-256 fingerprint.150*/151async trustCertificate(host: string, fingerprint: string): Promise<void> {152let entries = this._trustedCertificates.get(host);153if (!entries) {154entries = new Map();155this._trustedCertificates.set(host, entries);156}157entries.set(fingerprint, Date.now() + TRUST_DURATION_MS);158this.writeStorage();159}160161/**162* Revoke trust for a certificate identified by host and fingerprint.163*/164async untrustCertificate(host: string, fingerprint: string): Promise<void> {165const entries = this._trustedCertificates.get(host);166if (entries && entries.delete(fingerprint)) {167if (entries.size === 0) {168this._trustedCertificates.delete(host);169}170} else {171throw new Error(`Certificate not found: host=${host} fingerprint=${fingerprint}`);172}173this.writeStorage();174// Important: close all connections since they may be using the now-untrusted cert.175await this._session.electronSession.closeAllConnections();176}177178/**179* Check whether a certificate is trusted for a given host.180*/181isCertificateTrusted(host: string, fingerprint: string): boolean {182const expiresAt = this._trustedCertificates.get(host)?.get(fingerprint);183if (expiresAt === undefined) {184return false;185}186if (Date.now() > expiresAt) {187return false;188}189return true;190}191192/**193* Connect application storage so that trusted certificates are194* persisted across restarts. Restores any previously-saved data on195* first call; subsequent calls are no-ops.196*/197connectStorage(storage: IApplicationStorageMainService): void {198if (this._storage) {199return; // already connected200}201this._storage = storage;202this.readStorage();203}204205/**206* Clear all trust state: in-memory certs, cert-error cache, persisted207* data, and close open connections that may be using now-untrusted certs.208*/209async clear(): Promise<void> {210this._trustedCertificates.clear();211this._certErrors.clear();212this.writeStorage();213// Important: close all connections since they may be using now-untrusted certs.214await this._session.electronSession.closeAllConnections();215}216217// #region Persistence helpers218219/**220* Restore trusted certificates from application storage.221*/222private readStorage(): void {223const storage = this._storage;224if (!storage) {225return;226}227228const raw = storage.get(STORAGE_KEY, StorageScope.APPLICATION);229if (!raw) {230return;231}232233const now = Date.now();234let pruned = false;235try {236const all: PersistedTrustData = JSON.parse(raw);237const certs = all[this._session.id]?.trustedCerts;238if (certs) {239for (const { host, fingerprint, expiresAt } of certs) {240if (expiresAt > now) {241let entries = this._trustedCertificates.get(host);242if (!entries) {243entries = new Map();244this._trustedCertificates.set(host, entries);245}246entries.set(fingerprint, expiresAt);247} else {248pruned = true;249}250}251}252} catch {253// Corrupt data — ignore254}255256// Flush expired entries from storage257if (pruned) {258this.writeStorage();259}260}261262/**263* Write trusted certificates to application storage.264* The single storage key holds **all** sessions' data so that we can265* clean up stale entries atomically.266*/267private writeStorage(): void {268const storage = this._storage;269if (!storage) {270return;271}272273// Read existing blob (other sessions may have data too)274let all: PersistedTrustData = {};275try {276const raw = storage.get(STORAGE_KEY, StorageScope.APPLICATION);277if (raw) {278all = JSON.parse(raw);279}280} catch {281// Overwrite corrupt data282}283284// Ensure this session's entry exists285if (!all[this._session.id]) {286all[this._session.id] = {};287}288289// Update the trusted certs slice290if (this._trustedCertificates.size === 0) {291delete all[this._session.id].trustedCerts;292} else {293const certs: { host: string; fingerprint: string; expiresAt: number }[] = [];294for (const [host, entries] of this._trustedCertificates) {295for (const [fingerprint, expiresAt] of entries) {296certs.push({ host, fingerprint, expiresAt });297}298}299all[this._session.id].trustedCerts = certs;300}301302// Remove empty session entries303if (Object.keys(all[this._session.id]).length === 0) {304delete all[this._session.id];305}306307// Write back (or remove if empty)308if (Object.keys(all).length === 0) {309storage.remove(STORAGE_KEY, StorageScope.APPLICATION);310} else {311storage.store(STORAGE_KEY, JSON.stringify(all), StorageScope.APPLICATION, StorageTarget.MACHINE);312}313}314315// #endregion316}317318319