|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2010 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to show and edit all certificates. |
|
8 """ |
|
9 |
|
10 import contextlib |
|
11 |
|
12 from PyQt5.QtCore import ( |
|
13 pyqtSlot, Qt, QByteArray, QFile, QFileInfo, QIODevice |
|
14 ) |
|
15 from PyQt5.QtWidgets import QDialog, QTreeWidgetItem |
|
16 with contextlib.suppress(ImportError): |
|
17 from PyQt5.QtNetwork import ( |
|
18 QSslCertificate, QSslSocket, QSslConfiguration, QSsl |
|
19 ) |
|
20 |
|
21 from E5Gui import E5MessageBox, E5FileDialog |
|
22 |
|
23 from .Ui_E5SslCertificatesDialog import Ui_E5SslCertificatesDialog |
|
24 |
|
25 import Preferences |
|
26 import Utilities |
|
27 import UI.PixmapCache |
|
28 import Globals |
|
29 |
|
30 |
|
31 class E5SslCertificatesDialog(QDialog, Ui_E5SslCertificatesDialog): |
|
32 """ |
|
33 Class implementing a dialog to show and edit all certificates. |
|
34 """ |
|
35 CertRole = Qt.ItemDataRole.UserRole + 1 |
|
36 |
|
37 def __init__(self, parent=None): |
|
38 """ |
|
39 Constructor |
|
40 |
|
41 @param parent reference to the parent widget (QWidget) |
|
42 """ |
|
43 super().__init__(parent) |
|
44 self.setupUi(self) |
|
45 |
|
46 self.serversViewButton.setIcon( |
|
47 UI.PixmapCache.getIcon("certificates")) |
|
48 self.serversDeleteButton.setIcon( |
|
49 UI.PixmapCache.getIcon("certificateDelete")) |
|
50 self.serversExportButton.setIcon( |
|
51 UI.PixmapCache.getIcon("certificateExport")) |
|
52 self.serversImportButton.setIcon( |
|
53 UI.PixmapCache.getIcon("certificateImport")) |
|
54 |
|
55 self.caViewButton.setIcon( |
|
56 UI.PixmapCache.getIcon("certificates")) |
|
57 self.caDeleteButton.setIcon( |
|
58 UI.PixmapCache.getIcon("certificateDelete")) |
|
59 self.caExportButton.setIcon( |
|
60 UI.PixmapCache.getIcon("certificateExport")) |
|
61 self.caImportButton.setIcon( |
|
62 UI.PixmapCache.getIcon("certificateImport")) |
|
63 |
|
64 self.__populateServerCertificatesTree() |
|
65 self.__populateCaCertificatesTree() |
|
66 |
|
67 def __populateServerCertificatesTree(self): |
|
68 """ |
|
69 Private slot to populate the server certificates tree. |
|
70 """ |
|
71 certificateDict = Globals.toDict( |
|
72 Preferences.Prefs.settings.value("Ssl/CaCertificatesDict")) |
|
73 for server in certificateDict: |
|
74 for cert in QSslCertificate.fromData(certificateDict[server]): |
|
75 self.__createServerCertificateEntry(server, cert) |
|
76 |
|
77 self.serversCertificatesTree.expandAll() |
|
78 for i in range(self.serversCertificatesTree.columnCount()): |
|
79 self.serversCertificatesTree.resizeColumnToContents(i) |
|
80 |
|
81 def __createServerCertificateEntry(self, server, cert): |
|
82 """ |
|
83 Private method to create a server certificate entry. |
|
84 |
|
85 @param server server name of the certificate (string) |
|
86 @param cert certificate to insert (QSslCertificate) |
|
87 """ |
|
88 # step 1: extract the info to be shown |
|
89 organisation = Utilities.decodeString( |
|
90 ", ".join(cert.subjectInfo( |
|
91 QSslCertificate.SubjectInfo.Organization))) |
|
92 commonName = Utilities.decodeString( |
|
93 ", ".join(cert.subjectInfo( |
|
94 QSslCertificate.SubjectInfo.CommonName))) |
|
95 if organisation is None or organisation == "": |
|
96 organisation = self.tr("(Unknown)") |
|
97 if commonName is None or commonName == "": |
|
98 commonName = self.tr("(Unknown common name)") |
|
99 expiryDate = cert.expiryDate().toString("yyyy-MM-dd") |
|
100 |
|
101 # step 2: create the entry |
|
102 items = self.serversCertificatesTree.findItems( |
|
103 organisation, |
|
104 Qt.MatchFlag.MatchFixedString | Qt.MatchFlag.MatchCaseSensitive) |
|
105 if len(items) == 0: |
|
106 parent = QTreeWidgetItem( |
|
107 self.serversCertificatesTree, [organisation]) |
|
108 parent.setFirstColumnSpanned(True) |
|
109 else: |
|
110 parent = items[0] |
|
111 |
|
112 itm = QTreeWidgetItem(parent, [commonName, server, expiryDate]) |
|
113 itm.setData(0, self.CertRole, cert.toPem()) |
|
114 |
|
115 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) |
|
116 def on_serversCertificatesTree_currentItemChanged(self, current, previous): |
|
117 """ |
|
118 Private slot handling a change of the current item in the |
|
119 server certificates list. |
|
120 |
|
121 @param current new current item (QTreeWidgetItem) |
|
122 @param previous previous current item (QTreeWidgetItem) |
|
123 """ |
|
124 enable = current is not None and current.parent() is not None |
|
125 self.serversViewButton.setEnabled(enable) |
|
126 self.serversDeleteButton.setEnabled(enable) |
|
127 self.serversExportButton.setEnabled(enable) |
|
128 |
|
129 @pyqtSlot() |
|
130 def on_serversViewButton_clicked(self): |
|
131 """ |
|
132 Private slot to show data of the selected server certificate. |
|
133 """ |
|
134 with contextlib.suppress(ImportError): |
|
135 from E5Network.E5SslCertificatesInfoDialog import ( |
|
136 E5SslCertificatesInfoDialog |
|
137 ) |
|
138 cert = QSslCertificate.fromData( |
|
139 self.serversCertificatesTree.currentItem().data( |
|
140 0, self.CertRole)) |
|
141 dlg = E5SslCertificatesInfoDialog(cert, self) |
|
142 dlg.exec() |
|
143 |
|
144 @pyqtSlot() |
|
145 def on_serversDeleteButton_clicked(self): |
|
146 """ |
|
147 Private slot to delete the selected server certificate. |
|
148 """ |
|
149 itm = self.serversCertificatesTree.currentItem() |
|
150 res = E5MessageBox.yesNo( |
|
151 self, |
|
152 self.tr("Delete Server Certificate"), |
|
153 self.tr("""<p>Shall the server certificate really be""" |
|
154 """ deleted?</p><p>{0}</p>""" |
|
155 """<p>If the server certificate is deleted, the""" |
|
156 """ normal security checks will be reinstantiated""" |
|
157 """ and the server has to present a valid""" |
|
158 """ certificate.</p>""") |
|
159 .format(itm.text(0))) |
|
160 if res: |
|
161 server = itm.text(1) |
|
162 cert = self.serversCertificatesTree.currentItem().data( |
|
163 0, self.CertRole) |
|
164 |
|
165 # delete the selected entry and its parent entry, |
|
166 # if it was the only one |
|
167 parent = itm.parent() |
|
168 parent.takeChild(parent.indexOfChild(itm)) |
|
169 if parent.childCount() == 0: |
|
170 self.serversCertificatesTree.takeTopLevelItem( |
|
171 self.serversCertificatesTree.indexOfTopLevelItem(parent)) |
|
172 |
|
173 # delete the certificate from the user certificate store |
|
174 certificateDict = Globals.toDict( |
|
175 Preferences.Prefs.settings.value("Ssl/CaCertificatesDict")) |
|
176 if server in certificateDict: |
|
177 certs = [c.toPem() for c in |
|
178 QSslCertificate.fromData(certificateDict[server])] |
|
179 if cert in certs: |
|
180 certs.remove(cert) |
|
181 if certs: |
|
182 pems = QByteArray() |
|
183 for cert in certs: |
|
184 pems.append(cert + b'\n') |
|
185 certificateDict[server] = pems |
|
186 else: |
|
187 del certificateDict[server] |
|
188 Preferences.Prefs.settings.setValue( |
|
189 "Ssl/CaCertificatesDict", |
|
190 certificateDict) |
|
191 |
|
192 # delete the certificate from the default certificates |
|
193 self.__updateDefaultConfiguration() |
|
194 |
|
195 @pyqtSlot() |
|
196 def on_serversImportButton_clicked(self): |
|
197 """ |
|
198 Private slot to import server certificates. |
|
199 """ |
|
200 certs = self.__importCertificate() |
|
201 if certs: |
|
202 server = "*" |
|
203 certificateDict = Globals.toDict( |
|
204 Preferences.Prefs.settings.value("Ssl/CaCertificatesDict")) |
|
205 if server in certificateDict: |
|
206 sCerts = QSslCertificate.fromData(certificateDict[server]) |
|
207 else: |
|
208 sCerts = [] |
|
209 |
|
210 pems = QByteArray() |
|
211 for cert in certs: |
|
212 if cert in sCerts: |
|
213 commonStr = ", ".join( |
|
214 cert.subjectInfo( |
|
215 QSslCertificate.SubjectInfo.CommonName)) |
|
216 E5MessageBox.warning( |
|
217 self, |
|
218 self.tr("Import Certificate"), |
|
219 self.tr( |
|
220 """<p>The certificate <b>{0}</b> already exists.""" |
|
221 """ Skipping.</p>""") |
|
222 .format(Utilities.decodeString(commonStr))) |
|
223 else: |
|
224 pems.append(cert.toPem() + '\n') |
|
225 if server not in certificateDict: |
|
226 certificateDict[server] = QByteArray() |
|
227 certificateDict[server].append(pems) |
|
228 Preferences.Prefs.settings.setValue( |
|
229 "Ssl/CaCertificatesDict", |
|
230 certificateDict) |
|
231 |
|
232 self.serversCertificatesTree.clear() |
|
233 self.__populateServerCertificatesTree() |
|
234 |
|
235 self.__updateDefaultConfiguration() |
|
236 |
|
237 @pyqtSlot() |
|
238 def on_serversExportButton_clicked(self): |
|
239 """ |
|
240 Private slot to export the selected server certificate. |
|
241 """ |
|
242 cert = self.serversCertificatesTree.currentItem().data( |
|
243 0, self.CertRole) |
|
244 fname = ( |
|
245 self.serversCertificatesTree.currentItem().text(0).replace(" ", "") |
|
246 .replace("\t", "") |
|
247 ) |
|
248 self.__exportCertificate(fname, cert) |
|
249 |
|
250 def __updateDefaultConfiguration(self): |
|
251 """ |
|
252 Private method to update the default SSL configuration. |
|
253 """ |
|
254 caList = self.__getSystemCaCertificates() |
|
255 certificateDict = Globals.toDict( |
|
256 Preferences.Prefs.settings.value("Ssl/CaCertificatesDict")) |
|
257 for server in certificateDict: |
|
258 for cert in QSslCertificate.fromData(certificateDict[server]): |
|
259 if cert not in caList: |
|
260 caList.append(cert) |
|
261 sslCfg = QSslConfiguration.defaultConfiguration() |
|
262 sslCfg.setCaCertificates(caList) |
|
263 QSslConfiguration.setDefaultConfiguration(sslCfg) |
|
264 |
|
265 def __getSystemCaCertificates(self): |
|
266 """ |
|
267 Private method to get the list of system certificates. |
|
268 |
|
269 @return list of system certificates (list of QSslCertificate) |
|
270 """ |
|
271 caList = QSslCertificate.fromData(Globals.toByteArray( |
|
272 Preferences.Prefs.settings.value("Help/SystemCertificates"))) |
|
273 if not caList: |
|
274 caList = QSslSocket.systemCaCertificates() |
|
275 return caList |
|
276 |
|
277 def __populateCaCertificatesTree(self): |
|
278 """ |
|
279 Private slot to populate the CA certificates tree. |
|
280 """ |
|
281 for cert in self.__getSystemCaCertificates(): |
|
282 self.__createCaCertificateEntry(cert) |
|
283 |
|
284 self.caCertificatesTree.expandAll() |
|
285 for i in range(self.caCertificatesTree.columnCount()): |
|
286 self.caCertificatesTree.resizeColumnToContents(i) |
|
287 self.caCertificatesTree.sortItems(0, Qt.SortOrder.AscendingOrder) |
|
288 |
|
289 def __createCaCertificateEntry(self, cert): |
|
290 """ |
|
291 Private method to create a CA certificate entry. |
|
292 |
|
293 @param cert certificate to insert (QSslCertificate) |
|
294 """ |
|
295 # step 1: extract the info to be shown |
|
296 organisation = Utilities.decodeString( |
|
297 ", ".join(cert.subjectInfo( |
|
298 QSslCertificate.SubjectInfo.Organization))) |
|
299 commonName = Utilities.decodeString( |
|
300 ", ".join(cert.subjectInfo( |
|
301 QSslCertificate.SubjectInfo.CommonName))) |
|
302 if organisation is None or organisation == "": |
|
303 organisation = self.tr("(Unknown)") |
|
304 if commonName is None or commonName == "": |
|
305 commonName = self.tr("(Unknown common name)") |
|
306 expiryDate = cert.expiryDate().toString("yyyy-MM-dd") |
|
307 |
|
308 # step 2: create the entry |
|
309 items = self.caCertificatesTree.findItems( |
|
310 organisation, |
|
311 Qt.MatchFlag.MatchFixedString | Qt.MatchFlag.MatchCaseSensitive) |
|
312 if len(items) == 0: |
|
313 parent = QTreeWidgetItem(self.caCertificatesTree, [organisation]) |
|
314 parent.setFirstColumnSpanned(True) |
|
315 else: |
|
316 parent = items[0] |
|
317 |
|
318 itm = QTreeWidgetItem(parent, [commonName, expiryDate]) |
|
319 itm.setData(0, self.CertRole, cert.toPem()) |
|
320 |
|
321 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) |
|
322 def on_caCertificatesTree_currentItemChanged(self, current, previous): |
|
323 """ |
|
324 Private slot handling a change of the current item |
|
325 in the CA certificates list. |
|
326 |
|
327 @param current new current item (QTreeWidgetItem) |
|
328 @param previous previous current item (QTreeWidgetItem) |
|
329 """ |
|
330 enable = current is not None and current.parent() is not None |
|
331 self.caViewButton.setEnabled(enable) |
|
332 self.caDeleteButton.setEnabled(enable) |
|
333 self.caExportButton.setEnabled(enable) |
|
334 |
|
335 @pyqtSlot() |
|
336 def on_caViewButton_clicked(self): |
|
337 """ |
|
338 Private slot to show data of the selected CA certificate. |
|
339 """ |
|
340 with contextlib.suppress(ImportError): |
|
341 from E5Network.E5SslCertificatesInfoDialog import ( |
|
342 E5SslCertificatesInfoDialog |
|
343 ) |
|
344 cert = QSslCertificate.fromData( |
|
345 self.caCertificatesTree.currentItem().data(0, self.CertRole)) |
|
346 dlg = E5SslCertificatesInfoDialog(cert, self) |
|
347 dlg.exec() |
|
348 |
|
349 @pyqtSlot() |
|
350 def on_caDeleteButton_clicked(self): |
|
351 """ |
|
352 Private slot to delete the selected CA certificate. |
|
353 """ |
|
354 itm = self.caCertificatesTree.currentItem() |
|
355 res = E5MessageBox.yesNo( |
|
356 self, |
|
357 self.tr("Delete CA Certificate"), |
|
358 self.tr( |
|
359 """<p>Shall the CA certificate really be deleted?</p>""" |
|
360 """<p>{0}</p>""" |
|
361 """<p>If the CA certificate is deleted, the browser""" |
|
362 """ will not trust any certificate issued by this CA.</p>""") |
|
363 .format(itm.text(0))) |
|
364 if res: |
|
365 cert = self.caCertificatesTree.currentItem().data(0, self.CertRole) |
|
366 |
|
367 # delete the selected entry and its parent entry, |
|
368 # if it was the only one |
|
369 parent = itm.parent() |
|
370 parent.takeChild(parent.indexOfChild(itm)) |
|
371 if parent.childCount() == 0: |
|
372 self.caCertificatesTree.takeTopLevelItem( |
|
373 self.caCertificatesTree.indexOfTopLevelItem(parent)) |
|
374 |
|
375 # delete the certificate from the CA certificate store |
|
376 caCerts = self.__getSystemCaCertificates() |
|
377 if cert in caCerts: |
|
378 caCerts.remove(cert) |
|
379 pems = QByteArray() |
|
380 for cert in caCerts: |
|
381 pems.append(cert.toPem() + '\n') |
|
382 Preferences.Prefs.settings.setValue( |
|
383 "Help/SystemCertificates", pems) |
|
384 |
|
385 # delete the certificate from the default certificates |
|
386 self.__updateDefaultConfiguration() |
|
387 |
|
388 @pyqtSlot() |
|
389 def on_caImportButton_clicked(self): |
|
390 """ |
|
391 Private slot to import server certificates. |
|
392 """ |
|
393 certs = self.__importCertificate() |
|
394 if certs: |
|
395 caCerts = self.__getSystemCaCertificates() |
|
396 for cert in certs: |
|
397 if cert in caCerts: |
|
398 commonStr = ", ".join( |
|
399 cert.subjectInfo( |
|
400 QSslCertificate.SubjectInfo.CommonName)) |
|
401 E5MessageBox.warning( |
|
402 self, |
|
403 self.tr("Import Certificate"), |
|
404 self.tr( |
|
405 """<p>The certificate <b>{0}</b> already exists.""" |
|
406 """ Skipping.</p>""") |
|
407 .format(Utilities.decodeString(commonStr))) |
|
408 else: |
|
409 caCerts.append(cert) |
|
410 |
|
411 pems = QByteArray() |
|
412 for cert in caCerts: |
|
413 pems.append(cert.toPem() + '\n') |
|
414 Preferences.Prefs.settings.setValue( |
|
415 "Help/SystemCertificates", pems) |
|
416 |
|
417 self.caCertificatesTree.clear() |
|
418 self.__populateCaCertificatesTree() |
|
419 |
|
420 self.__updateDefaultConfiguration() |
|
421 |
|
422 @pyqtSlot() |
|
423 def on_caExportButton_clicked(self): |
|
424 """ |
|
425 Private slot to export the selected CA certificate. |
|
426 """ |
|
427 cert = self.caCertificatesTree.currentItem().data(0, self.CertRole) |
|
428 fname = ( |
|
429 self.caCertificatesTree.currentItem().text(0).replace(" ", "") |
|
430 .replace("\t", "") |
|
431 ) |
|
432 self.__exportCertificate(fname, cert) |
|
433 |
|
434 def __exportCertificate(self, name, cert): |
|
435 """ |
|
436 Private slot to export a certificate. |
|
437 |
|
438 @param name default file name without extension (string) |
|
439 @param cert certificate to be exported (QSslCertificate) |
|
440 """ |
|
441 if cert is not None: |
|
442 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( |
|
443 self, |
|
444 self.tr("Export Certificate"), |
|
445 name, |
|
446 self.tr("Certificate File (PEM) (*.pem);;" |
|
447 "Certificate File (DER) (*.der)"), |
|
448 None, |
|
449 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) |
|
450 |
|
451 if fname: |
|
452 ext = QFileInfo(fname).suffix() |
|
453 if not ext or ext not in ["pem", "der"]: |
|
454 ex = selectedFilter.split("(*")[1].split(")")[0] |
|
455 if ex: |
|
456 fname += ex |
|
457 if QFileInfo(fname).exists(): |
|
458 res = E5MessageBox.yesNo( |
|
459 self, |
|
460 self.tr("Export Certificate"), |
|
461 self.tr("<p>The file <b>{0}</b> already exists." |
|
462 " Overwrite it?</p>").format(fname), |
|
463 icon=E5MessageBox.Warning) |
|
464 if not res: |
|
465 return |
|
466 |
|
467 f = QFile(fname) |
|
468 if not f.open(QIODevice.OpenModeFlag.WriteOnly): |
|
469 E5MessageBox.critical( |
|
470 self, |
|
471 self.tr("Export Certificate"), |
|
472 self.tr( |
|
473 """<p>The certificate could not be written""" |
|
474 """ to file <b>{0}</b></p><p>Error: {1}</p>""") |
|
475 .format(fname, f.errorString())) |
|
476 return |
|
477 |
|
478 if fname.endswith(".pem"): |
|
479 crt = cert.toPem() |
|
480 else: |
|
481 crt = cert.toDer() |
|
482 f.write(crt) |
|
483 f.close() |
|
484 |
|
485 def __importCertificate(self): |
|
486 """ |
|
487 Private method to read a certificate. |
|
488 |
|
489 @return certificates read (list of QSslCertificate) |
|
490 """ |
|
491 fname = E5FileDialog.getOpenFileName( |
|
492 self, |
|
493 self.tr("Import Certificate"), |
|
494 "", |
|
495 self.tr("Certificate Files (*.pem *.crt *.der *.cer *.ca);;" |
|
496 "All Files (*)")) |
|
497 |
|
498 if fname: |
|
499 f = QFile(fname) |
|
500 if not f.open(QIODevice.OpenModeFlag.ReadOnly): |
|
501 E5MessageBox.critical( |
|
502 self, |
|
503 self.tr("Export Certificate"), |
|
504 self.tr( |
|
505 """<p>The certificate could not be read from file""" |
|
506 """ <b>{0}</b></p><p>Error: {1}</p>""") |
|
507 .format(fname, f.errorString())) |
|
508 return [] |
|
509 |
|
510 crt = f.readAll() |
|
511 f.close() |
|
512 cert = QSslCertificate.fromData(crt, QSsl.EncodingFormat.Pem) |
|
513 if not cert: |
|
514 cert = QSslCertificate.fromData(crt, QSsl.EncodingFormat.Der) |
|
515 |
|
516 return cert |
|
517 |
|
518 return [] |