Module cevast.analysis.modules.validation_clients.gnutls

This module contains an implementation of a certificate chain validation client for command-line GnuTLS.

The validation client can be both imported and used externally (as a standalone module) through the provided command-line interface.

Important

The validation client must be set up correctly before use. It is necessary to ensure, that GnuTLS is installed correctly, "libfaketime.so.1" binary (used for setting reference time) is present, and that TRUST_STORE_FILE is set to the path to the local trust store.

When using the validation client externally, these prerequisites are always checked before validation is performed. When the validation client is imported, it is necessary to do these checks manually (i.e., using the method is_setup_correctly()).

Expand source code
#!/usr/bin/python3

"""
This module contains an implementation of a certificate chain validation client for command-line GnuTLS.

The validation client can be both imported and used externally (as a standalone module) through the provided
command-line interface.

..Important::
  The validation client must be set up correctly before use. It is necessary to ensure, that GnuTLS is installed
  correctly, "libfaketime.so.1" binary (used for setting reference time) is present, and that `TRUST_STORE_FILE` is
  set to the path to the local trust store.

  When using the validation client externally, these prerequisites are always checked before validation is performed.
  When the validation client is imported, it is necessary to do these checks manually (i.e., using the method `is_setup_correctly()`).
"""

import argparse
import datetime
import os
import subprocess


# noinspection PyBroadException
class GnuTLS:
    """
    A class for certificate chain validation client utilizing command-line GnuTLS.

    Special error messages:

    "Error": any error / exception not related to the certificate chain validation (algorithm) itself

    "NotVerified": validation is unsuccessful for an unknown reason
    """

    TRUST_STORE_FILE = "/etc/pki/tls/cert.pem"

    @staticmethod
    def verify(chain, reference_time=None, crl=None, **kwargs):
        """
        Validates a certificate chain.

        `chain` is a list of paths to certificates forming a chain.
        `reference_time` is a reference time of validation in seconds since the epoch.
        `crl` is a path to CRL.
        `kwargs` are other, unexpected arguments.

        The returned result is a list of a set of error messages returned by command-line GnuTLS.
        """

        chain = list(chain)

        try:
            command = []

            if reference_time:
                command += GnuTLS.__get_faketime_command(reference_time)

            command += ["certtool", "--load-ca-certificate", GnuTLS.TRUST_STORE_FILE]

            if crl:
                command += ["--load-crl", crl]

            command += ["--verify-profile", "low", "--verify"]

            command = [" ".join(command)]

            for i, certificate_path in enumerate(chain):
                with open(certificate_path) as input_file:
                    chain[i] = input_file.read()

            process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)

            process.stdin.write("".join(chain).encode())
            process.stdin.close()

            command_output = process.stdout.read().decode(errors="ignore")

            verified_chain_substring = "Chain verification output: Verified. The certificate is trusted."
            unverified_chain_substring = "Chain verification output: Not verified. The certificate is NOT trusted."

            if command_output.find(verified_chain_substring) != -1:
                result = ["Verified"]
            else:
                message_index = command_output.find(unverified_chain_substring)

                if message_index != -1:
                    messages = []

                    for message in command_output[message_index + len(unverified_chain_substring):].strip().split("."):
                        if message != "":
                            messages.append("".join([word.capitalize() for word in message.split()]))

                    if len(messages) > 0:
                        result = sorted(set(messages))
                    else:
                        result = ["NotVerified"]

                else:
                    result = ["Error"]
        except Exception:
            result = ["Error"]

        return result

    @staticmethod
    def is_setup_correctly():
        """
        Verifies that the validation client is set up correctly. (GnuTLS is installed, reference time works,
        and trust store exists)
        """

        is_setup_correctly = True

        try:
            subprocess.check_output(" ".join(["certtool", "-v"]), stderr=subprocess.DEVNULL, shell=True)
        except Exception:
            is_setup_correctly = False

        if is_setup_correctly:
            try:
                command_output = subprocess.check_output(" ".join(GnuTLS.__get_faketime_command(0) + ["date", "-u"]),
                                                         stderr=subprocess.DEVNULL, shell=True)

                if command_output.decode().strip() != "Thu  1 Jan 01:00:00 CET 1970":
                    is_setup_correctly = False
            except Exception:
                is_setup_correctly = False

        if is_setup_correctly:
            if not os.path.isfile(GnuTLS.TRUST_STORE_FILE):
                is_setup_correctly = False

        return is_setup_correctly

    @staticmethod
    def __get_faketime_command(reference_time):
        return ["LD_PRELOAD={0}".format(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libfaketime.so.1")),
                "FAKETIME=\"{0}\"".format(
                    datetime.datetime.fromtimestamp(reference_time).strftime("%Y-%m-%d %H:%M:%S"))]


if __name__ == "__main__":
    argument_parser = argparse.ArgumentParser(description="chain format: ENDPOINT [INTERMEDIATE ...] [CA]")

    argument_parser.add_argument("-r", type=lambda s: int(datetime.datetime.strptime(s, "%Y-%m-%d").timestamp()),
                                 required=False, dest="reference_date_as_time", metavar="DATE",
                                 help="reference date in format YYYY-MM-DD (at 00:00:00)")
    argument_parser.add_argument("-t", type=int, required=False, dest="reference_time", metavar="N",
                                 help="reference time in seconds since the epoch (surpassing reference date)")
    argument_parser.add_argument("--crl", type=str, required=False, dest="crl", metavar="FILE",
                                 help="certificate revocation list")
    argument_parser.add_argument("CERTIFICATE", type=str, nargs="+")

    if GnuTLS.is_setup_correctly():
        args = argument_parser.parse_args()
        print(GnuTLS.verify(args.CERTIFICATE, reference_time=args.reference_time if args.reference_time is not None else args.reference_date_as_time, crl=args.crl))
    else:
        print("Client is not set up correctly")

Classes

class GnuTLS

A class for certificate chain validation client utilizing command-line GnuTLS.

Special error messages:

"Error": any error / exception not related to the certificate chain validation (algorithm) itself

"NotVerified": validation is unsuccessful for an unknown reason

Expand source code
class GnuTLS:
    """
    A class for certificate chain validation client utilizing command-line GnuTLS.

    Special error messages:

    "Error": any error / exception not related to the certificate chain validation (algorithm) itself

    "NotVerified": validation is unsuccessful for an unknown reason
    """

    TRUST_STORE_FILE = "/etc/pki/tls/cert.pem"

    @staticmethod
    def verify(chain, reference_time=None, crl=None, **kwargs):
        """
        Validates a certificate chain.

        `chain` is a list of paths to certificates forming a chain.
        `reference_time` is a reference time of validation in seconds since the epoch.
        `crl` is a path to CRL.
        `kwargs` are other, unexpected arguments.

        The returned result is a list of a set of error messages returned by command-line GnuTLS.
        """

        chain = list(chain)

        try:
            command = []

            if reference_time:
                command += GnuTLS.__get_faketime_command(reference_time)

            command += ["certtool", "--load-ca-certificate", GnuTLS.TRUST_STORE_FILE]

            if crl:
                command += ["--load-crl", crl]

            command += ["--verify-profile", "low", "--verify"]

            command = [" ".join(command)]

            for i, certificate_path in enumerate(chain):
                with open(certificate_path) as input_file:
                    chain[i] = input_file.read()

            process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)

            process.stdin.write("".join(chain).encode())
            process.stdin.close()

            command_output = process.stdout.read().decode(errors="ignore")

            verified_chain_substring = "Chain verification output: Verified. The certificate is trusted."
            unverified_chain_substring = "Chain verification output: Not verified. The certificate is NOT trusted."

            if command_output.find(verified_chain_substring) != -1:
                result = ["Verified"]
            else:
                message_index = command_output.find(unverified_chain_substring)

                if message_index != -1:
                    messages = []

                    for message in command_output[message_index + len(unverified_chain_substring):].strip().split("."):
                        if message != "":
                            messages.append("".join([word.capitalize() for word in message.split()]))

                    if len(messages) > 0:
                        result = sorted(set(messages))
                    else:
                        result = ["NotVerified"]

                else:
                    result = ["Error"]
        except Exception:
            result = ["Error"]

        return result

    @staticmethod
    def is_setup_correctly():
        """
        Verifies that the validation client is set up correctly. (GnuTLS is installed, reference time works,
        and trust store exists)
        """

        is_setup_correctly = True

        try:
            subprocess.check_output(" ".join(["certtool", "-v"]), stderr=subprocess.DEVNULL, shell=True)
        except Exception:
            is_setup_correctly = False

        if is_setup_correctly:
            try:
                command_output = subprocess.check_output(" ".join(GnuTLS.__get_faketime_command(0) + ["date", "-u"]),
                                                         stderr=subprocess.DEVNULL, shell=True)

                if command_output.decode().strip() != "Thu  1 Jan 01:00:00 CET 1970":
                    is_setup_correctly = False
            except Exception:
                is_setup_correctly = False

        if is_setup_correctly:
            if not os.path.isfile(GnuTLS.TRUST_STORE_FILE):
                is_setup_correctly = False

        return is_setup_correctly

    @staticmethod
    def __get_faketime_command(reference_time):
        return ["LD_PRELOAD={0}".format(os.path.join(os.path.dirname(os.path.realpath(__file__)), "libfaketime.so.1")),
                "FAKETIME=\"{0}\"".format(
                    datetime.datetime.fromtimestamp(reference_time).strftime("%Y-%m-%d %H:%M:%S"))]

Class variables

var TRUST_STORE_FILE

Static methods

def is_setup_correctly()

Verifies that the validation client is set up correctly. (GnuTLS is installed, reference time works, and trust store exists)

Expand source code
@staticmethod
def is_setup_correctly():
    """
    Verifies that the validation client is set up correctly. (GnuTLS is installed, reference time works,
    and trust store exists)
    """

    is_setup_correctly = True

    try:
        subprocess.check_output(" ".join(["certtool", "-v"]), stderr=subprocess.DEVNULL, shell=True)
    except Exception:
        is_setup_correctly = False

    if is_setup_correctly:
        try:
            command_output = subprocess.check_output(" ".join(GnuTLS.__get_faketime_command(0) + ["date", "-u"]),
                                                     stderr=subprocess.DEVNULL, shell=True)

            if command_output.decode().strip() != "Thu  1 Jan 01:00:00 CET 1970":
                is_setup_correctly = False
        except Exception:
            is_setup_correctly = False

    if is_setup_correctly:
        if not os.path.isfile(GnuTLS.TRUST_STORE_FILE):
            is_setup_correctly = False

    return is_setup_correctly
def verify(chain, reference_time=None, crl=None, **kwargs)

Validates a certificate chain.

chain is a list of paths to certificates forming a chain. reference_time is a reference time of validation in seconds since the epoch. crl is a path to CRL. kwargs are other, unexpected arguments.

The returned result is a list of a set of error messages returned by command-line GnuTLS.

Expand source code
@staticmethod
def verify(chain, reference_time=None, crl=None, **kwargs):
    """
    Validates a certificate chain.

    `chain` is a list of paths to certificates forming a chain.
    `reference_time` is a reference time of validation in seconds since the epoch.
    `crl` is a path to CRL.
    `kwargs` are other, unexpected arguments.

    The returned result is a list of a set of error messages returned by command-line GnuTLS.
    """

    chain = list(chain)

    try:
        command = []

        if reference_time:
            command += GnuTLS.__get_faketime_command(reference_time)

        command += ["certtool", "--load-ca-certificate", GnuTLS.TRUST_STORE_FILE]

        if crl:
            command += ["--load-crl", crl]

        command += ["--verify-profile", "low", "--verify"]

        command = [" ".join(command)]

        for i, certificate_path in enumerate(chain):
            with open(certificate_path) as input_file:
                chain[i] = input_file.read()

        process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)

        process.stdin.write("".join(chain).encode())
        process.stdin.close()

        command_output = process.stdout.read().decode(errors="ignore")

        verified_chain_substring = "Chain verification output: Verified. The certificate is trusted."
        unverified_chain_substring = "Chain verification output: Not verified. The certificate is NOT trusted."

        if command_output.find(verified_chain_substring) != -1:
            result = ["Verified"]
        else:
            message_index = command_output.find(unverified_chain_substring)

            if message_index != -1:
                messages = []

                for message in command_output[message_index + len(unverified_chain_substring):].strip().split("."):
                    if message != "":
                        messages.append("".join([word.capitalize() for word in message.split()]))

                if len(messages) > 0:
                    result = sorted(set(messages))
                else:
                    result = ["NotVerified"]

            else:
                result = ["Error"]
    except Exception:
        result = ["Error"]

    return result