eric6/E5Network/E5SslCertificatesDialog.py

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

eric ide

mercurial