File check-autofirma-certificate.py of Package autofirma

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import configparser
import os.path
import subprocess
import sys
from enum import Enum

debug = (len(sys.argv) > 1 and '--debug' in sys.argv)
terminal = (len(sys.argv) > 1 and '--terminal' in sys.argv)

translations = {'es': {
    'Import': 'Importar',
    'Cancel': 'Cancelar',
    'Error': 'Error',
    'Replace': 'Reemplazar',
    'Error: Neither python3-PyQt5 not python3-GObject are available.':
        'Error: Ni python3-PyQt5 ni python3-GObject están disponibles.',
    'Cannot remove {certname} certificate':
        'No se puede eliminar el certificado {certname}',
    'Please delete the certificate manually from the {certmanagername}':
        'Por favor, elimina el certificado manualmente del {certmanagername}',
    'AutoFirma check': 'Comprobación de AutoFirma',
    'Do you want to import the AutoFirma ROOT CA into {checkername}?':
        '¿Quiere importar la CA AutoFirma ROOT en {checkername}?',
    'The AutoFirma ROOT CA was not found in the {certmanagername}.\n\n'
    '{filepath} needs to be imported for AutoFirma to work correctly.':
        'No se encontró el certificado AutoFirma ROOT en el {certmanagername}'
        '\n\nPara que AutoFirma funcione correctamente se necesita importar '
        'el fichero\n{filepath} .',
    'Do you want to replace the current AutoFirma ROOT CA certificate '
    'in {checkername} with the right one?':
        '¿Quiere reemplazar el certificado de CA AutoFirma ROOT actual de '
        '{checkername} con el correcto?',
    'The AutoFirma ROOT CA certificate found in the {certmanagername} is not '
    'the correct one, probably because it was imported from a previous '
    'AutoFirma version.\n\n{filepath} needs to be imported replacing the '
    'current certificate for AutoFirma to work correctly.':
        'El certificado de CA AutoFirma ROOT encontrado en el '
        '{certmanagername} no es el correcto, probablemente porque fue '
        'importado de una versión anterior.\n\nPara que AutoFirma funcione '
        'correctamente se necesita importar {filepath} reemplazando '
        'el certificado actual.',
    'The AutoFirma ROOT CA certificate is already installed in {name}':
        'El certificado de AutoFirma ROOT ya está instalado en {name}',
    'Not installing the AutoFirma ROOT CA certificate in {name}':
        'No se instalará el certificado de CA AutoFirma ROOT in {name}',
    'Not replacing the AutoFirma ROOT CA certificate in {name}':
        'No se reemplazará el certificado de CA AutoFirma ROOT in {name}',
    'Firefox Certificate Manager': 'Administrador de Certificados de Firefox',
    '~/.pki/nssdb directory': 'directorio ~/.pki/nssdb',
    'AutoFirma_ROOT.cer file not found.':
        'No se encontró el fichero AutoFirma_ROOT.cer',
    'Please run the following command as root:\n':
        'Por favor, ejecute el siguiente comando como root:\n',
    'Firefox profile directory not found.':
        'No se encontró el directorio de perfil de Firefox.'}}


def i18n(text):
    import locale
    locale.setlocale(locale.LC_ALL, "")
    try:
        lang = locale.getlocale(locale.LC_MESSAGES)[0].split('_')[0]
    except Exception:
        return text

    try:
        return translations[lang][text]
    except KeyError:
        return text


def get_autofirma_configurador_path():
    paths = ['/usr/share/java/autofirma/',
             '/usr/share/java/',
             '/usr/lib64/autofirma/']
    filenames = ['AutoFirmaConfigurador.jar', 'autofirmaConfigurador.jar']
    for path in paths:
        for filename in filenames:
            filepath = os.path.join(path, filename)
            if os.path.exists(filepath):
                return filepath

    return None


def get_autofirma_root_cer_path():
    paths = ['/usr/share/java/autofirma/',
             '/usr/share/java/',
             '/usr/lib64/autofirma/']
    for path in paths:
        filepath = os.path.join(path, 'AutoFirma_ROOT.cer')
        if os.path.exists(filepath):
            return filepath

    raise FileNotFoundError('AutoFirma_ROOT.cer file not found')


def get_file_certificate_contents(filepath):
    cmd = f"openssl x509 -inform der -in '{filepath}'"
    if debug:
        print(cmd)
    try:
        return subprocess.check_output(cmd, stderr=subprocess.PIPE, shell=True)
    except subprocess.CalledProcessError:
        return None


def get_db_certificate_contents(profile_path, certname):
    cmd = f"certutil -L -d sql:{profile_path} -n '{certname}' -a"
    if debug:
        print(cmd)
    try:
        return subprocess.check_output(cmd, stderr=subprocess.PIPE, shell=True)
    except subprocess.CalledProcessError:
        return None


class Qt5Ui:
    @staticmethod
    def is_available():
        try:
            from PyQt5.QtWidgets import QApplication, QMessageBox  # noqa
        except ModuleNotFoundError:
            return False
        return True

    @staticmethod
    def msg_dialog(title, text, informative_text, msgtype='Information'):
        from PyQt5.QtWidgets import QApplication, QMessageBox
        app = QApplication(sys.argv)
        msgBox = QMessageBox()
        msgBox.setWindowTitle(title)
        msgBox.setText('<h3>' + text + '</h3>')
        msgBox.setInformativeText(informative_text)
        msgBox.setStandardButtons(QMessageBox.Ok)
        msgBox.setDefaultButton(QMessageBox.Ok)
        iconForType = {'Information': QMessageBox.Information,
                       'Warning': QMessageBox.Warning,
                       'Error': QMessageBox.Critical}
        msgBox.setIcon(iconForType[msgtype])
        msgBox.exec()
        app.quit()

    @staticmethod
    def ask_user(title, text, informative_text, yes_text, no_text):
        from PyQt5.QtWidgets import QApplication, QMessageBox
        app = QApplication(sys.argv)
        msgBox = QMessageBox()
        msgBox.setWindowTitle(title)
        msgBox.setText('<h3>' + text + '</h3>')
        msgBox.setInformativeText(informative_text)
        msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
        msgBox.setDefaultButton(QMessageBox.Yes)
        msgBox.setIcon(QMessageBox.Question)
        msgBox.button(QMessageBox.Yes).setText(yes_text)
        msgBox.button(QMessageBox.No).setText(no_text)
        r = msgBox.exec()
        app.quit()
        return r == QMessageBox.Yes


class GtkUi:
    @staticmethod
    def is_available():
        try:
            import gi
            gi.require_version('Gtk', '3.0')
            from gi.repository import Gtk  # noqa
        except ModuleNotFoundError:
            return False
        return True

    @staticmethod
    def msg_dialog(title, text, informative_text, msgtype='Information'):
        import gi
        gi.require_version('Gtk', '3.0')
        from gi.repository import Gtk
        iconForType = {'Information': Gtk.MessageType.INFO,
                       'Warning': Gtk.MessageType.WARNING,
                       'Error': Gtk.MessageType.ERROR}

        dialog = Gtk.MessageDialog(None, 0, iconForType[msgtype],
                                   Gtk.ButtonsType.OK, text)
        dialog.set_title(title)
        dialog.format_secondary_text(informative_text)

        dialog.run()

        dialog.destroy()

    @staticmethod
    def ask_user(title, text, informative_text, yes_text, no_text):
        import gi
        gi.require_version('Gtk', '3.0')
        from gi.repository import Gtk

        dialog = Gtk.MessageDialog(None, 0, Gtk.MessageType.QUESTION,
                                   Gtk.ButtonsType.NONE, text)
        dialog.set_title(title)
        dialog.add_buttons(yes_text, Gtk.ResponseType.YES,
                           no_text, Gtk.ResponseType.NO)
        dialog.format_secondary_text(informative_text)

        response = dialog.run()

        dialog.destroy()

        return response == Gtk.ResponseType.YES


class TerminalUi:
    @staticmethod
    def is_available():
        return True

    @staticmethod
    def msg_dialog(title, text, informative_text, msgtype='Information'):
        print(title)
        print(text)
        print(informative_text)

    @staticmethod
    def ask_user(title, text, informative_text, yes_text, no_text):
        print(title)
        print(text)
        print(informative_text)
        print(f'1) {yes_text}')
        print(f'2) {no_text}')
        response = None
        while response not in ['1', '2']:
            response = input('Choose 1 or 2?')
        return response == '1'


def getUi():
    if terminal:
        return TerminalUi

    if 'GNOME' in os.getenv('XDG_CURRENT_DESKTOP'):
        if GtkUi.is_available():
            return GtkUi
        elif Qt5Ui.is_available():
            return Qt5Ui
    else:
        if Qt5Ui.is_available():
            return Qt5Ui
        elif GtkUi.is_available():
            return GtkUi

    print(i18n('Error: Neither python3-PyQt5 not python3-GObject are '
               'available.'))
    sys.exit(0)


class InstalledState(Enum):
    NotInstalled = 0
    InstalledOK = 1
    InstalledButDifferent = 2


class CertificateChecker:
    def __init__(self, certificate_path):
        """Create a CertificateChecker class."""
        self.certdir = None
        self.certificate_path = certificate_path

    def is_autofirma_certificate_installed(self):
        autofirma_cer_contents = \
            get_file_certificate_contents(self.certificate_path)
        if debug:
            print(autofirma_cer_contents)

        certname = 'AutoFirma ROOT'
        certificate = get_db_certificate_contents(self.certdir, certname)
        if not certificate:
            certname = 'SocketAutoFirma'
            certificate = get_db_certificate_contents(self.certdir, certname)

        if not certificate:
            return (InstalledState.NotInstalled, None)

        certificate = certificate.replace(b'\r', b'')
        if debug:
            print(certificate)

        if certificate != autofirma_cer_contents:
            return (InstalledState.InstalledButDifferent, certname)

        return (InstalledState.InstalledOK, certname)

    def import_certificate(self):
        cmd = (f"certutil -A -d sql:{self.certdir} -i {self.certificate_path} "
               "-n 'AutoFirma ROOT' -t 'C,,'")
        if debug:
            print(cmd)

        try:
            subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
        except subprocess.CalledProcessError:
            return False
        return True

    def delete_certificate(self, certname):
        cmd = (f"certutil -D -d sql:{self.certdir} -n '{certname}'")
        if debug:
            print(cmd)

        try:
            subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
        except subprocess.CalledProcessError:
            return False
        return True

    def replace_certificate(self, certname):
        r = self.delete_certificate(certname)
        if not r:
            title = i18n('Error')
            msg = i18n('Cannot remove {certname} certificate')
            msg = msg.format(certname=certname)
            msg2 = i18n('Please delete the certificate manually from the '
                        '{certmanagername}')
            msg2 = msg2.format(certmanagername=self.certmanagername)
            print(title + '\n' + msg + '\n' + msg2)

            ui = getUi()
            ui.msg_dialog(title, msg, msg2, 'Error')

            return False
        else:
            # There might be more than one certificate installed
            # with that name, so let's remove them all
            c = 0
            while self.delete_certificate(certname) and c < 10:
                c += 1

        return self.import_certificate()

    def ask_user_if_import_certificate(self):
        title = i18n('AutoFirma check')
        text = i18n('Do you want to import the AutoFirma ROOT CA into '
                    '{checkername}?')
        text = text.format(checkername=self.name)
        informative_text = i18n('The AutoFirma ROOT CA was not found in the '
                                '{certmanagername}.\n\n{filepath} needs to '
                                'be imported for AutoFirma to work correctly.')
        informative_text = (informative_text.format(
                            certmanagername=self.certmanagername,
                            filepath=self.certificate_path))
        yes_text = i18n('Import')
        no_text = i18n('Cancel')

        ui = getUi()
        return ui.ask_user(title, text, informative_text, yes_text, no_text)

    def ask_user_if_replace_certificate(self):
        title = i18n('AutoFirma check')
        text = i18n('Do you want to replace the current AutoFirma ROOT CA '
                    'certificate in {checkername} with the right one?')
        text = text.format(checkername=self.name)
        msg = ('The AutoFirma ROOT CA certificate found in the '
               '{certmanagername} is not the correct one, probably because it '
               'was imported from a previous AutoFirma version.\n\n{filepath} '
               'needs to be imported replacing the current certificate for '
               'AutoFirma to work correctly.')
        informative_text = i18n(msg)
        informative_text = (informative_text.format(
                            certmanagername=self.certmanagername,
                            filepath=self.certificate_path))
        yes_text = i18n('Replace')
        no_text = i18n('Cancel')

        ui = getUi()
        return ui.ask_user(title, text, informative_text, yes_text, no_text)

    def check(self):
        installed_state, certname = self.is_autofirma_certificate_installed()

        if installed_state == InstalledState.InstalledOK:
            msg = i18n('The AutoFirma ROOT CA certificate is already '
                       'installed in {name}')
            print(msg.format(name=self.name))
            return True

        if installed_state == InstalledState.NotInstalled:
            if self.ask_user_if_import_certificate():
                self.import_certificate()
            else:
                msg = i18n('Not installing the AutoFirma ROOT CA certificate '
                           'in {name}')
                print(msg.format(name=self.name))
        else:
            if self.ask_user_if_replace_certificate():
                self.replace_certificate(certname)
            else:
                msg = i18n('Not replacing the AutoFirma ROOT CA certificate '
                           'in {name}')
                print(msg.format(name=self.name))


class FirefoxCertificateChecker(CertificateChecker):
    def __init__(self, certificate_path):
        """Create a PkiCertificateChecker class."""
        super(FirefoxCertificateChecker, self).__init__(certificate_path)

        self.certdir = self.get_profile_directory('default')
        self.name = 'Firefox'
        self.certmanagername = i18n('Firefox Certificate Manager')

    def get_profile_directory(self, profile_name):
        dirname = os.path.expanduser('~/.mozilla/firefox')
        config = configparser.ConfigParser(allow_no_value=True)
        config.read(os.path.join(dirname, 'profiles.ini'))

        is_relative = None
        profile_path = None
        for section in config.sections():
            if section.startswith('Profile'):
                if config.get(section, 'Name', fallback=None) == profile_name:
                    is_relative = config.get(section, 'IsRelative')
                    profile_path = config.get(section, 'Path')
                    break
        else:
            return None

        if is_relative:
            profile_path = os.path.join(dirname, profile_path)

        return profile_path


class PkiCertificateChecker(CertificateChecker):
    def __init__(self, certificate_path):
        """Create a PkiCertificateChecker class."""
        super(PkiCertificateChecker, self).__init__(certificate_path)

        self.certdir = os.path.expanduser('~/.pki/nssdb')
        self.name = '~/.pki/nssdb'
        self.certmanagername = i18n('~/.pki/nssdb directory')


def main():
    try:
        filepath = get_autofirma_root_cer_path()
    except FileNotFoundError:
        configurador = get_autofirma_configurador_path()
        title = i18n('Error')
        msg = i18n('AutoFirma_ROOT.cer file not found.')
        msg2 = (i18n('Please run the following command as root:\n') +
                f'    java -jar {configurador} -install')
        print(title + '\n' + msg + '\n' + msg2)

        ui = getUi()
        ui.msg_dialog(title, msg, msg2, 'Error')
        sys.exit(1)

    checker = FirefoxCertificateChecker(filepath)
    if checker.certdir:
        checker.check()
    else:
        msg = i18n('Firefox profile directory not found.')

    checker = PkiCertificateChecker(filepath)
    checker.check()


if __name__ == '__main__':
    main()
openSUSE Build Service is sponsored by