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