src/eric7/EricNetwork/EricSslCertificatesDialog.py

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

eric ide

mercurial