Skip to content

Commit cdd66b5

Browse files
committed
fix(certificate): approved certificate is rejected until restart
Signed-off-by: Grigorii K. Shartsev <me@shgk.me>
1 parent 444820c commit cdd66b5

File tree

4 files changed

+63
-1
lines changed

4 files changed

+63
-1
lines changed

src/app/certificate.service.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import type { BrowserWindow, Certificate } from 'electron'
77

8+
import { session } from 'electron'
89
import { showCertificateTrustDialog } from '../certificate/certificate.window.ts'
910
import { getAppConfig, setAppConfig } from './AppConfig.ts'
1011

@@ -56,3 +57,50 @@ export async function promptCertificateTrust(window: BrowserWindow, details: Unt
5657

5758
return isAccepted
5859
}
60+
61+
/**
62+
* Verify certificate on a URL.
63+
* Note: this function only exists due to Electron limitations.
64+
* If a user accepts the certificate later than the request is rejected by timeout,
65+
* Electron considers it rejected for 30 minutes or until the app restart.
66+
* Issue: https://github.com/electron/electron/issues/47267
67+
* And there is no way to reset the cache.
68+
* Issue: https://github.com/electron/electron/issues/41448
69+
* Thus the verification on the defaultSession cannot be used (at least for login).
70+
* This function makes a single request in a new random session, to avoid verification caching.
71+
* The actual result is stored in the application config.
72+
*
73+
* @param window - Parent browser window
74+
* @param url - URL
75+
*/
76+
export async function verifyCertificate(window: BrowserWindow, url: string): Promise<boolean> {
77+
const certificateVerifySession = session.fromPartition(`certificate:verify:${Math.random().toString(36).slice(2, 9)}`)
78+
79+
let verificationResolvers: PromiseWithResolvers<boolean> | undefined
80+
81+
certificateVerifySession.setCertificateVerifyProc(async (request, callback) => {
82+
verificationResolvers = Promise.withResolvers()
83+
// Use original result, failing the request
84+
callback(-3)
85+
86+
const isAccepted = await promptCertificateTrust(window, {
87+
host: request.hostname,
88+
certificate: request.certificate,
89+
error: request.verificationResult,
90+
})
91+
verificationResolvers.resolve(isAccepted)
92+
})
93+
94+
try {
95+
await certificateVerifySession.fetch(url, { bypassCustomProtocolHandlers: true })
96+
// Successful request - no SSL errors
97+
return true
98+
} catch {
99+
// SSL Error - handled by user prompt
100+
if (verificationResolvers) {
101+
return verificationResolvers.promise
102+
}
103+
// Some unexpected network error
104+
return false
105+
}
106+
}

src/authentication/renderer/AuthenticationApp.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ async function login() {
8484
return setError(t('talk_desktop', 'Invalid server address'))
8585
}
8686
87+
// Check the certificate before actually sending a request
88+
if (!await window.TALK_DESKTOP.verifyCertificate(serverUrl.value)) {
89+
return setError(t('talk_desktop', 'Untrusted certificate'))
90+
}
91+
8792
// Prepare to request the server
8893
window.TALK_DESKTOP.disableWebRequestInterceptor()
8994
appData.reset()

src/main.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const { setupMenu } = require('./app/app.menu.js')
1111
const { loadAppConfig, getAppConfig, setAppConfig } = require('./app/AppConfig.ts')
1212
const { appData } = require('./app/AppData.js')
1313
const { registerAppProtocolHandler } = require('./app/appProtocol.ts')
14-
const { promptCertificateTrust } = require('./app/certificate.service.ts')
14+
const { verifyCertificate, promptCertificateTrust } = require('./app/certificate.service.ts')
1515
const { openChromeWebRtcInternals } = require('./app/dev.utils.ts')
1616
const { triggerDownloadUrl } = require('./app/downloads.ts')
1717
const { setupReleaseNotificationScheduler } = require('./app/githubReleaseNotification.service.js')
@@ -352,6 +352,8 @@ app.whenReady().then(async () => {
352352

353353
ipcMain.on('app:downloadURL', (event, url, filename) => triggerDownloadUrl(mainWindow, url, filename))
354354

355+
ipcMain.handle('certificate:verify', (event, url) => verifyCertificate(mainWindow, url))
356+
355357
// Click on the dock icon on macOS
356358
app.on('activate', () => {
357359
if (mainWindow && !mainWindow.isDestroyed()) {

src/preload.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,13 @@ const TALK_DESKTOP = {
191191
* @param {boolean} isAccepted - Is the certificate accepted as trusted
192192
*/
193193
acceptCertificate: (isAccepted) => ipcRenderer.send('certificate:accept', isAccepted),
194+
/**
195+
* Verify certificate on a URL
196+
*
197+
* @param {string} url - URL
198+
* @return {Promise<boolean>}
199+
*/
200+
verifyCertificate: (url) => ipcRenderer.invoke('certificate:verify', url),
194201
}
195202

196203
// Set global window.TALK_DESKTOP

0 commit comments

Comments
 (0)