eric7/E5Network/E5SslCertificatesDialog.py

branch
eric7
changeset 8312
800c432b34c8
parent 8243
cc717c2ae956
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
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 []

eric ide

mercurial