|
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 [] |