eric7/EricNetwork/EricSslCertificatesDialog.py

branch
eric7
changeset 8354
12ebd3934fef
parent 8322
b422b4e77d19
child 8356
68ec9c3d4de5
diff -r 799196d0b05d -r 12ebd3934fef eric7/EricNetwork/EricSslCertificatesDialog.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/EricNetwork/EricSslCertificatesDialog.py	Sat May 22 16:52:45 2021 +0200
@@ -0,0 +1,518 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2010 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to show and edit all certificates.
+"""
+
+import contextlib
+
+from PyQt6.QtCore import (
+    pyqtSlot, Qt, QByteArray, QFile, QFileInfo, QIODevice
+)
+from PyQt6.QtWidgets import QDialog, QTreeWidgetItem
+with contextlib.suppress(ImportError):
+    from PyQt6.QtNetwork import (
+        QSslCertificate, QSslSocket, QSslConfiguration, QSsl
+    )
+
+from E5Gui import E5MessageBox, E5FileDialog
+
+from .Ui_EricSslCertificatesDialog import Ui_EricSslCertificatesDialog
+
+import Preferences
+import Utilities
+import UI.PixmapCache
+import Globals
+
+
+class EricSslCertificatesDialog(QDialog, Ui_EricSslCertificatesDialog):
+    """
+    Class implementing a dialog to show and edit all certificates.
+    """
+    CertRole = Qt.ItemDataRole.UserRole + 1
+    
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent widget (QWidget)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+        
+        self.serversViewButton.setIcon(
+            UI.PixmapCache.getIcon("certificates"))
+        self.serversDeleteButton.setIcon(
+            UI.PixmapCache.getIcon("certificateDelete"))
+        self.serversExportButton.setIcon(
+            UI.PixmapCache.getIcon("certificateExport"))
+        self.serversImportButton.setIcon(
+            UI.PixmapCache.getIcon("certificateImport"))
+        
+        self.caViewButton.setIcon(
+            UI.PixmapCache.getIcon("certificates"))
+        self.caDeleteButton.setIcon(
+            UI.PixmapCache.getIcon("certificateDelete"))
+        self.caExportButton.setIcon(
+            UI.PixmapCache.getIcon("certificateExport"))
+        self.caImportButton.setIcon(
+            UI.PixmapCache.getIcon("certificateImport"))
+        
+        self.__populateServerCertificatesTree()
+        self.__populateCaCertificatesTree()
+    
+    def __populateServerCertificatesTree(self):
+        """
+        Private slot to populate the server certificates tree.
+        """
+        certificateDict = Globals.toDict(
+            Preferences.Prefs.settings.value("Ssl/CaCertificatesDict"))
+        for server in certificateDict:
+            for cert in QSslCertificate.fromData(certificateDict[server]):
+                self.__createServerCertificateEntry(server, cert)
+        
+        self.serversCertificatesTree.expandAll()
+        for i in range(self.serversCertificatesTree.columnCount()):
+            self.serversCertificatesTree.resizeColumnToContents(i)
+    
+    def __createServerCertificateEntry(self, server, cert):
+        """
+        Private method to create a server certificate entry.
+        
+        @param server server name of the certificate (string)
+        @param cert certificate to insert (QSslCertificate)
+        """
+        # step 1: extract the info to be shown
+        organisation = Utilities.decodeString(
+            ", ".join(cert.subjectInfo(
+                QSslCertificate.SubjectInfo.Organization)))
+        commonName = Utilities.decodeString(
+            ", ".join(cert.subjectInfo(
+                QSslCertificate.SubjectInfo.CommonName)))
+        if organisation is None or organisation == "":
+            organisation = self.tr("(Unknown)")
+        if commonName is None or commonName == "":
+            commonName = self.tr("(Unknown common name)")
+        expiryDate = cert.expiryDate().toString("yyyy-MM-dd")
+        
+        # step 2: create the entry
+        items = self.serversCertificatesTree.findItems(
+            organisation,
+            Qt.MatchFlag.MatchFixedString | Qt.MatchFlag.MatchCaseSensitive)
+        if len(items) == 0:
+            parent = QTreeWidgetItem(
+                self.serversCertificatesTree, [organisation])
+            parent.setFirstColumnSpanned(True)
+        else:
+            parent = items[0]
+        
+        itm = QTreeWidgetItem(parent, [commonName, server, expiryDate])
+        itm.setData(0, self.CertRole, cert.toPem())
+    
+    @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
+    def on_serversCertificatesTree_currentItemChanged(self, current, previous):
+        """
+        Private slot handling a change of the current item in the
+        server certificates list.
+        
+        @param current new current item (QTreeWidgetItem)
+        @param previous previous current item (QTreeWidgetItem)
+        """
+        enable = current is not None and current.parent() is not None
+        self.serversViewButton.setEnabled(enable)
+        self.serversDeleteButton.setEnabled(enable)
+        self.serversExportButton.setEnabled(enable)
+    
+    @pyqtSlot()
+    def on_serversViewButton_clicked(self):
+        """
+        Private slot to show data of the selected server certificate.
+        """
+        with contextlib.suppress(ImportError):
+            from EricNetwork.EricSslCertificatesInfoDialog import (
+                EricSslCertificatesInfoDialog
+            )
+            cert = QSslCertificate.fromData(
+                self.serversCertificatesTree.currentItem().data(
+                    0, self.CertRole))
+            dlg = EricSslCertificatesInfoDialog(cert, self)
+            dlg.exec()
+    
+    @pyqtSlot()
+    def on_serversDeleteButton_clicked(self):
+        """
+        Private slot to delete the selected server certificate.
+        """
+        itm = self.serversCertificatesTree.currentItem()
+        res = E5MessageBox.yesNo(
+            self,
+            self.tr("Delete Server Certificate"),
+            self.tr("""<p>Shall the server certificate really be"""
+                    """ deleted?</p><p>{0}</p>"""
+                    """<p>If the server certificate is deleted, the"""
+                    """ normal security checks will be reinstantiated"""
+                    """ and the server has to present a valid"""
+                    """ certificate.</p>""")
+            .format(itm.text(0)))
+        if res:
+            server = itm.text(1)
+            cert = self.serversCertificatesTree.currentItem().data(
+                0, self.CertRole)
+            
+            # delete the selected entry and its parent entry,
+            # if it was the only one
+            parent = itm.parent()
+            parent.takeChild(parent.indexOfChild(itm))
+            if parent.childCount() == 0:
+                self.serversCertificatesTree.takeTopLevelItem(
+                    self.serversCertificatesTree.indexOfTopLevelItem(parent))
+            
+            # delete the certificate from the user certificate store
+            certificateDict = Globals.toDict(
+                Preferences.Prefs.settings.value("Ssl/CaCertificatesDict"))
+            if server in certificateDict:
+                certs = [c.toPem() for c in
+                         QSslCertificate.fromData(certificateDict[server])]
+                if cert in certs:
+                    certs.remove(cert)
+                if certs:
+                    pems = QByteArray()
+                    for cert in certs:
+                        pems.append(cert + b'\n')
+                    certificateDict[server] = pems
+                else:
+                    del certificateDict[server]
+            Preferences.Prefs.settings.setValue(
+                "Ssl/CaCertificatesDict",
+                certificateDict)
+            
+            # delete the certificate from the default certificates
+            self.__updateDefaultConfiguration()
+    
+    @pyqtSlot()
+    def on_serversImportButton_clicked(self):
+        """
+        Private slot to import server certificates.
+        """
+        certs = self.__importCertificate()
+        if certs:
+            server = "*"
+            certificateDict = Globals.toDict(
+                Preferences.Prefs.settings.value("Ssl/CaCertificatesDict"))
+            if server in certificateDict:
+                sCerts = QSslCertificate.fromData(certificateDict[server])
+            else:
+                sCerts = []
+            
+            pems = QByteArray()
+            for cert in certs:
+                if cert in sCerts:
+                    commonStr = ", ".join(
+                        cert.subjectInfo(
+                            QSslCertificate.SubjectInfo.CommonName))
+                    E5MessageBox.warning(
+                        self,
+                        self.tr("Import Certificate"),
+                        self.tr(
+                            """<p>The certificate <b>{0}</b> already exists."""
+                            """ Skipping.</p>""")
+                        .format(Utilities.decodeString(commonStr)))
+                else:
+                    pems.append(cert.toPem() + '\n')
+            if server not in certificateDict:
+                certificateDict[server] = QByteArray()
+            certificateDict[server].append(pems)
+            Preferences.Prefs.settings.setValue(
+                "Ssl/CaCertificatesDict",
+                certificateDict)
+            
+            self.serversCertificatesTree.clear()
+            self.__populateServerCertificatesTree()
+            
+            self.__updateDefaultConfiguration()
+    
+    @pyqtSlot()
+    def on_serversExportButton_clicked(self):
+        """
+        Private slot to export the selected server certificate.
+        """
+        cert = self.serversCertificatesTree.currentItem().data(
+            0, self.CertRole)
+        fname = (
+            self.serversCertificatesTree.currentItem().text(0).replace(" ", "")
+            .replace("\t", "")
+        )
+        self.__exportCertificate(fname, cert)
+    
+    def __updateDefaultConfiguration(self):
+        """
+        Private method to update the default SSL configuration.
+        """
+        caList = self.__getSystemCaCertificates()
+        certificateDict = Globals.toDict(
+            Preferences.Prefs.settings.value("Ssl/CaCertificatesDict"))
+        for server in certificateDict:
+            for cert in QSslCertificate.fromData(certificateDict[server]):
+                if cert not in caList:
+                    caList.append(cert)
+        sslCfg = QSslConfiguration.defaultConfiguration()
+        sslCfg.setCaCertificates(caList)
+        QSslConfiguration.setDefaultConfiguration(sslCfg)
+    
+    def __getSystemCaCertificates(self):
+        """
+        Private method to get the list of system certificates.
+        
+        @return list of system certificates (list of QSslCertificate)
+        """
+        caList = QSslCertificate.fromData(Globals.toByteArray(
+            Preferences.Prefs.settings.value("Help/SystemCertificates")))
+        if not caList:
+            caList = QSslSocket.systemCaCertificates()
+        return caList
+    
+    def __populateCaCertificatesTree(self):
+        """
+        Private slot to populate the CA certificates tree.
+        """
+        for cert in self.__getSystemCaCertificates():
+            self.__createCaCertificateEntry(cert)
+        
+        self.caCertificatesTree.expandAll()
+        for i in range(self.caCertificatesTree.columnCount()):
+            self.caCertificatesTree.resizeColumnToContents(i)
+        self.caCertificatesTree.sortItems(0, Qt.SortOrder.AscendingOrder)
+    
+    def __createCaCertificateEntry(self, cert):
+        """
+        Private method to create a CA certificate entry.
+        
+        @param cert certificate to insert (QSslCertificate)
+        """
+        # step 1: extract the info to be shown
+        organisation = Utilities.decodeString(
+            ", ".join(cert.subjectInfo(
+                QSslCertificate.SubjectInfo.Organization)))
+        commonName = Utilities.decodeString(
+            ", ".join(cert.subjectInfo(
+                QSslCertificate.SubjectInfo.CommonName)))
+        if organisation is None or organisation == "":
+            organisation = self.tr("(Unknown)")
+        if commonName is None or commonName == "":
+            commonName = self.tr("(Unknown common name)")
+        expiryDate = cert.expiryDate().toString("yyyy-MM-dd")
+        
+        # step 2: create the entry
+        items = self.caCertificatesTree.findItems(
+            organisation,
+            Qt.MatchFlag.MatchFixedString | Qt.MatchFlag.MatchCaseSensitive)
+        if len(items) == 0:
+            parent = QTreeWidgetItem(self.caCertificatesTree, [organisation])
+            parent.setFirstColumnSpanned(True)
+        else:
+            parent = items[0]
+        
+        itm = QTreeWidgetItem(parent, [commonName, expiryDate])
+        itm.setData(0, self.CertRole, cert.toPem())
+    
+    @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
+    def on_caCertificatesTree_currentItemChanged(self, current, previous):
+        """
+        Private slot handling a change of the current item
+        in the CA certificates list.
+        
+        @param current new current item (QTreeWidgetItem)
+        @param previous previous current item (QTreeWidgetItem)
+        """
+        enable = current is not None and current.parent() is not None
+        self.caViewButton.setEnabled(enable)
+        self.caDeleteButton.setEnabled(enable)
+        self.caExportButton.setEnabled(enable)
+    
+    @pyqtSlot()
+    def on_caViewButton_clicked(self):
+        """
+        Private slot to show data of the selected CA certificate.
+        """
+        with contextlib.suppress(ImportError):
+            from EricNetwork.EricSslCertificatesInfoDialog import (
+                EricSslCertificatesInfoDialog
+            )
+            cert = QSslCertificate.fromData(
+                self.caCertificatesTree.currentItem().data(0, self.CertRole))
+            dlg = EricSslCertificatesInfoDialog(cert, self)
+            dlg.exec()
+    
+    @pyqtSlot()
+    def on_caDeleteButton_clicked(self):
+        """
+        Private slot to delete the selected CA certificate.
+        """
+        itm = self.caCertificatesTree.currentItem()
+        res = E5MessageBox.yesNo(
+            self,
+            self.tr("Delete CA Certificate"),
+            self.tr(
+                """<p>Shall the CA certificate really be deleted?</p>"""
+                """<p>{0}</p>"""
+                """<p>If the CA certificate is deleted, the browser"""
+                """ will not trust any certificate issued by this CA.</p>""")
+            .format(itm.text(0)))
+        if res:
+            cert = self.caCertificatesTree.currentItem().data(0, self.CertRole)
+            
+            # delete the selected entry and its parent entry,
+            # if it was the only one
+            parent = itm.parent()
+            parent.takeChild(parent.indexOfChild(itm))
+            if parent.childCount() == 0:
+                self.caCertificatesTree.takeTopLevelItem(
+                    self.caCertificatesTree.indexOfTopLevelItem(parent))
+            
+            # delete the certificate from the CA certificate store
+            caCerts = self.__getSystemCaCertificates()
+            if cert in caCerts:
+                caCerts.remove(cert)
+            pems = QByteArray()
+            for cert in caCerts:
+                pems.append(cert.toPem() + '\n')
+            Preferences.Prefs.settings.setValue(
+                "Help/SystemCertificates", pems)
+            
+            # delete the certificate from the default certificates
+            self.__updateDefaultConfiguration()
+    
+    @pyqtSlot()
+    def on_caImportButton_clicked(self):
+        """
+        Private slot to import server certificates.
+        """
+        certs = self.__importCertificate()
+        if certs:
+            caCerts = self.__getSystemCaCertificates()
+            for cert in certs:
+                if cert in caCerts:
+                    commonStr = ", ".join(
+                        cert.subjectInfo(
+                            QSslCertificate.SubjectInfo.CommonName))
+                    E5MessageBox.warning(
+                        self,
+                        self.tr("Import Certificate"),
+                        self.tr(
+                            """<p>The certificate <b>{0}</b> already exists."""
+                            """ Skipping.</p>""")
+                        .format(Utilities.decodeString(commonStr)))
+                else:
+                    caCerts.append(cert)
+            
+            pems = QByteArray()
+            for cert in caCerts:
+                pems.append(cert.toPem() + '\n')
+            Preferences.Prefs.settings.setValue(
+                "Help/SystemCertificates", pems)
+            
+            self.caCertificatesTree.clear()
+            self.__populateCaCertificatesTree()
+            
+            self.__updateDefaultConfiguration()
+    
+    @pyqtSlot()
+    def on_caExportButton_clicked(self):
+        """
+        Private slot to export the selected CA certificate.
+        """
+        cert = self.caCertificatesTree.currentItem().data(0, self.CertRole)
+        fname = (
+            self.caCertificatesTree.currentItem().text(0).replace(" ", "")
+            .replace("\t", "")
+        )
+        self.__exportCertificate(fname, cert)
+    
+    def __exportCertificate(self, name, cert):
+        """
+        Private slot to export a certificate.
+        
+        @param name default file name without extension (string)
+        @param cert certificate to be exported (QSslCertificate)
+        """
+        if cert is not None:
+            fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
+                self,
+                self.tr("Export Certificate"),
+                name,
+                self.tr("Certificate File (PEM) (*.pem);;"
+                        "Certificate File (DER) (*.der)"),
+                None,
+                E5FileDialog.DontConfirmOverwrite)
+            
+            if fname:
+                ext = QFileInfo(fname).suffix()
+                if not ext or ext not in ["pem", "der"]:
+                    ex = selectedFilter.split("(*")[1].split(")")[0]
+                    if ex:
+                        fname += ex
+                if QFileInfo(fname).exists():
+                    res = E5MessageBox.yesNo(
+                        self,
+                        self.tr("Export Certificate"),
+                        self.tr("<p>The file <b>{0}</b> already exists."
+                                " Overwrite it?</p>").format(fname),
+                        icon=E5MessageBox.Warning)
+                    if not res:
+                        return
+                
+                f = QFile(fname)
+                if not f.open(QIODevice.OpenModeFlag.WriteOnly):
+                    E5MessageBox.critical(
+                        self,
+                        self.tr("Export Certificate"),
+                        self.tr(
+                            """<p>The certificate could not be written"""
+                            """ to file <b>{0}</b></p><p>Error: {1}</p>""")
+                        .format(fname, f.errorString()))
+                    return
+                
+                if fname.endswith(".pem"):
+                    crt = cert.toPem()
+                else:
+                    crt = cert.toDer()
+                f.write(crt)
+                f.close()
+    
+    def __importCertificate(self):
+        """
+        Private method to read a certificate.
+        
+        @return certificates read (list of QSslCertificate)
+        """
+        fname = E5FileDialog.getOpenFileName(
+            self,
+            self.tr("Import Certificate"),
+            "",
+            self.tr("Certificate Files (*.pem *.crt *.der *.cer *.ca);;"
+                    "All Files (*)"))
+        
+        if fname:
+            f = QFile(fname)
+            if not f.open(QIODevice.OpenModeFlag.ReadOnly):
+                E5MessageBox.critical(
+                    self,
+                    self.tr("Export Certificate"),
+                    self.tr(
+                        """<p>The certificate could not be read from file"""
+                        """ <b>{0}</b></p><p>Error: {1}</p>""")
+                    .format(fname, f.errorString()))
+                return []
+            
+            crt = f.readAll()
+            f.close()
+            cert = QSslCertificate.fromData(crt, QSsl.EncodingFormat.Pem)
+            if not cert:
+                cert = QSslCertificate.fromData(crt, QSsl.EncodingFormat.Der)
+            
+            return cert
+        
+        return []

eric ide

mercurial