# coding: utf-8

"""
ASN.1 type classes for certificate revocation lists (CRL). Exports the
following items:

 - CertificateList()

Other type classes are defined that help compose the types listed above.
"""

from __future__ import unicode_literals, division, absolute_import, print_function

import hashlib

from .algos import SignedDigestAlgorithm
from .core import (
    Boolean,
    Enumerated,
    GeneralizedTime,
    Integer,
    ObjectIdentifier,
    OctetBitString,
    ParsableOctetString,
    Sequence,
    SequenceOf,
)
from .x509 import (
    AuthorityInfoAccessSyntax,
    AuthorityKeyIdentifier,
    CRLDistributionPoints,
    DistributionPointName,
    GeneralNames,
    Name,
    ReasonFlags,
    Time,
)


# The structures in this file are taken from https://tools.ietf.org/html/rfc5280


class Version(Integer):
    _map = {
        0: 'v1',
        1: 'v2',
        2: 'v3',
    }


class IssuingDistributionPoint(Sequence):
    _fields = [
        ('distribution_point', DistributionPointName, {'tag_type': 'explicit', 'tag': 0, 'optional': True}),
        ('only_contains_user_certs', Boolean, {'tag_type': 'implicit', 'tag': 1, 'default': False}),
        ('only_contains_ca_certs', Boolean, {'tag_type': 'implicit', 'tag': 2, 'default': False}),
        ('only_some_reasons', ReasonFlags, {'tag_type': 'implicit', 'tag': 3, 'optional': True}),
        ('indirect_crl', Boolean, {'tag_type': 'implicit', 'tag': 4, 'default': False}),
        ('only_contains_attribute_certs', Boolean, {'tag_type': 'implicit', 'tag': 5, 'default': False}),
    ]


class TBSCertListExtensionId(ObjectIdentifier):
    _map = {
        '2.5.29.18': 'issuer_alt_name',
        '2.5.29.20': 'crl_number',
        '2.5.29.27': 'delta_crl_indicator',
        '2.5.29.28': 'issuing_distribution_point',
        '2.5.29.35': 'authority_key_identifier',
        '2.5.29.46': 'freshest_crl',
        '1.3.6.1.5.5.7.1.1': 'authority_information_access',
    }


class TBSCertListExtension(Sequence):
    _fields = [
        ('extn_id', TBSCertListExtensionId),
        ('critical', Boolean, {'default': False}),
        ('extn_value', ParsableOctetString),
    ]

    _oid_pair = ('extn_id', 'extn_value')
    _oid_specs = {
        'issuer_alt_name': GeneralNames,
        'crl_number': Integer,
        'delta_crl_indicator': Integer,
        'issuing_distribution_point': IssuingDistributionPoint,
        'authority_key_identifier': AuthorityKeyIdentifier,
        'freshest_crl': CRLDistributionPoints,
        'authority_information_access': AuthorityInfoAccessSyntax,
    }


class TBSCertListExtensions(SequenceOf):
    _child_spec = TBSCertListExtension


class CRLReason(Enumerated):
    _map = {
        0: 'unspecified',
        1: 'key_compromise',
        2: 'ca_compromise',
        3: 'affiliation_changed',
        4: 'superseded',
        5: 'cessation_of_operation',
        6: 'certificate_hold',
        8: 'remove_from_crl',
        9: 'privilege_withdrawn',
        10: 'aa_compromise',
    }

    @property
    def human_friendly(self):
        """
        :return:
            A unicode string with revocation description that is suitable to
            show to end-users. Starts with a lower case letter and phrased in
            such a way that it makes sense after the phrase "because of" or
            "due to".
        """

        return {
            'unspecified': 'an unspecified reason',
            'key_compromise': 'a compromised key',
            'ca_compromise': 'the CA being compromised',
            'affiliation_changed': 'an affiliation change',
            'superseded': 'certificate supersession',
            'cessation_of_operation': 'a cessation of operation',
            'certificate_hold': 'a certificate hold',
            'remove_from_crl': 'removal from the CRL',
            'privilege_withdrawn': 'privilege withdrawl',
            'aa_compromise': 'the AA being compromised',
        }[self.native]


class CRLEntryExtensionId(ObjectIdentifier):
    _map = {
        '2.5.29.21': 'crl_reason',
        '2.5.29.23': 'hold_instruction_code',
        '2.5.29.24': 'invalidity_date',
        '2.5.29.29': 'certificate_issuer',
    }


class CRLEntryExtension(Sequence):
    _fields = [
        ('extn_id', CRLEntryExtensionId),
        ('critical', Boolean, {'default': False}),
        ('extn_value', ParsableOctetString),
    ]

    _oid_pair = ('extn_id', 'extn_value')
    _oid_specs = {
        'crl_reason': CRLReason,
        'hold_instruction_code': ObjectIdentifier,
        'invalidity_date': GeneralizedTime,
        'certificate_issuer': GeneralNames,
    }


class CRLEntryExtensions(SequenceOf):
    _child_spec = CRLEntryExtension


class RevokedCertificate(Sequence):
    _fields = [
        ('user_certificate', Integer),
        ('revocation_date', Time),
        ('crl_entry_extensions', CRLEntryExtensions, {'optional': True}),
    ]

    _processed_extensions = False
    _critical_extensions = None
    _crl_reason_value = None
    _invalidity_date_value = None
    _certificate_issuer_value = None
    _issuer_name = False

    def _set_extensions(self):
        """
        Sets common named extensions to private attributes and creates a list
        of critical extensions
        """

        self._critical_extensions = set()

        for extension in self['crl_entry_extensions']:
            name = extension['extn_id'].native
            attribute_name = '_%s_value' % name
            if hasattr(self, attribute_name):
                setattr(self, attribute_name, extension['extn_value'].parsed)
            if extension['critical'].native:
                self._critical_extensions.add(name)

        self._processed_extensions = True

    @property
    def critical_extensions(self):
        """
        Returns a set of the names (or OID if not a known extension) of the
        extensions marked as critical

        :return:
            A set of unicode strings
        """

        if not self._processed_extensions:
            self._set_extensions()
        return self._critical_extensions

    @property
    def crl_reason_value(self):
        """
        This extension indicates the reason that a certificate was revoked.

        :return:
            None or a CRLReason object
        """

        if self._processed_extensions is False:
            self._set_extensions()
        return self._crl_reason_value

    @property
    def invalidity_date_value(self):
        """
        This extension indicates the suspected date/time the private key was
        compromised or the certificate became invalid. This would usually be
        before the revocation date, which is when the CA processed the
        revocation.

        :return:
            None or a GeneralizedTime object
        """

        if self._processed_extensions is False:
            self._set_extensions()
        return self._invalidity_date_value

    @property
    def certificate_issuer_value(self):
        """
        This extension indicates the issuer of the certificate in question,
        and is used in indirect CRLs. CRL entries without this extension are
        for certificates issued from the last seen issuer.

        :return:
            None or an x509.GeneralNames object
        """

        if self._processed_extensions is False:
            self._set_extensions()
        return self._certificate_issuer_value

    @property
    def issuer_name(self):
        """
        :return:
            None, or an asn1crypto.x509.Name object for the issuer of the cert
        """

        if self._issuer_name is False:
            self._issuer_name = None
            if self.certificate_issuer_value:
                for general_name in self.certificate_issuer_value:
                    if general_name.name == 'directory_name':
                        self._issuer_name = general_name.chosen
                        break
        return self._issuer_name


class RevokedCertificates(SequenceOf):
    _child_spec = RevokedCertificate


class TbsCertList(Sequence):
    _fields = [
        ('version', Version, {'optional': True}),
        ('signature', SignedDigestAlgorithm),
        ('issuer', Name),
        ('this_update', Time),
        ('next_update', Time),
        ('revoked_certificates', RevokedCertificates, {'optional': True}),
        ('crl_extensions', TBSCertListExtensions, {'tag_type': 'explicit', 'tag': 0, 'optional': True}),
    ]


class CertificateList(Sequence):
    _fields = [
        ('tbs_cert_list', TbsCertList),
        ('signature_algorithm', SignedDigestAlgorithm),
        ('signature', OctetBitString),
    ]

    _processed_extensions = False
    _critical_extensions = None
    _issuer_alt_name_value = None
    _crl_number_value = None
    _delta_crl_indicator_value = None
    _issuing_distribution_point_value = None
    _authority_key_identifier_value = None
    _freshest_crl_value = None
    _authority_information_access_value = None
    _issuer_cert_urls = None
    _delta_crl_distribution_points = None
    _sha1 = None
    _sha256 = None

    def _set_extensions(self):
        """
        Sets common named extensions to private attributes and creates a list
        of critical extensions
        """

        self._critical_extensions = set()

        for extension in self['tbs_cert_list']['crl_extensions']:
            name = extension['extn_id'].native
            attribute_name = '_%s_value' % name
            if hasattr(self, attribute_name):
                setattr(self, attribute_name, extension['extn_value'].parsed)
            if extension['critical'].native:
                self._critical_extensions.add(name)

        self._processed_extensions = True

    @property
    def critical_extensions(self):
        """
        Returns a set of the names (or OID if not a known extension) of the
        extensions marked as critical

        :return:
            A set of unicode strings
        """

        if not self._processed_extensions:
            self._set_extensions()
        return self._critical_extensions

    @property
    def issuer_alt_name_value(self):
        """
        This extension allows associating one or more alternative names with
        the issuer of the CRL.

        :return:
            None or an x509.GeneralNames object
        """

        if self._processed_extensions is False:
            self._set_extensions()
        return self._issuer_alt_name_value

    @property
    def crl_number_value(self):
        """
        This extension adds a monotonically increasing number to the CRL and is
        used to distinguish different versions of the CRL.

        :return:
            None or an Integer object
        """

        if self._processed_extensions is False:
            self._set_extensions()
        return self._crl_number_value

    @property
    def delta_crl_indicator_value(self):
        """
        This extension indicates a CRL is a delta CRL, and contains the CRL
        number of the base CRL that it is a delta from.

        :return:
            None or an Integer object
        """

        if self._processed_extensions is False:
            self._set_extensions()
        return self._delta_crl_indicator_value

    @property
    def issuing_distribution_point_value(self):
        """
        This extension includes information about what types of revocations
        and certificates are part of the CRL.

        :return:
            None or an IssuingDistributionPoint object
        """

        if self._processed_extensions is False:
            self._set_extensions()
        return self._issuing_distribution_point_value

    @property
    def authority_key_identifier_value(self):
        """
        This extension helps in identifying the public key with which to
        validate the authenticity of the CRL.

        :return:
            None or an AuthorityKeyIdentifier object
        """

        if self._processed_extensions is False:
            self._set_extensions()
        return self._authority_key_identifier_value

    @property
    def freshest_crl_value(self):
        """
        This extension is used in complete CRLs to indicate where a delta CRL
        may be located.

        :return:
            None or a CRLDistributionPoints object
        """

        if self._processed_extensions is False:
            self._set_extensions()
        return self._freshest_crl_value

    @property
    def authority_information_access_value(self):
        """
        This extension is used to provide a URL with which to download the
        certificate used to sign this CRL.

        :return:
            None or an AuthorityInfoAccessSyntax object
        """

        if self._processed_extensions is False:
            self._set_extensions()
        return self._authority_information_access_value

    @property
    def issuer(self):
        """
        :return:
            An asn1crypto.x509.Name object for the issuer of the CRL
        """

        return self['tbs_cert_list']['issuer']

    @property
    def authority_key_identifier(self):
        """
        :return:
            None or a byte string of the key_identifier from the authority key
            identifier extension
        """

        if not self.authority_key_identifier_value:
            return None

        return self.authority_key_identifier_value['key_identifier'].native

    @property
    def issuer_cert_urls(self):
        """
        :return:
            A list of unicode strings that are URLs that should contain either
            an individual DER-encoded X.509 certificate, or a DER-encoded CMS
            message containing multiple certificates
        """

        if self._issuer_cert_urls is None:
            self._issuer_cert_urls = []
            if self.authority_information_access_value:
                for entry in self.authority_information_access_value:
                    if entry['access_method'].native == 'ca_issuers':
                        location = entry['access_location']
                        if location.name != 'uniform_resource_identifier':
                            continue
                        url = location.native
                        if url.lower()[0:7] == 'http://':
                            self._issuer_cert_urls.append(url)
        return self._issuer_cert_urls

    @property
    def delta_crl_distribution_points(self):
        """
        Returns delta CRL URLs - only applies to complete CRLs

        :return:
            A list of zero or more DistributionPoint objects
        """

        if self._delta_crl_distribution_points is None:
            self._delta_crl_distribution_points = []

            if self.freshest_crl_value is not None:
                for distribution_point in self.freshest_crl_value:
                    distribution_point_name = distribution_point['distribution_point']
                    # RFC 5280 indicates conforming CA should not use the relative form
                    if distribution_point_name.name == 'name_relative_to_crl_issuer':
                        continue
                    # This library is currently only concerned with HTTP-based CRLs
                    for general_name in distribution_point_name.chosen:
                        if general_name.name == 'uniform_resource_identifier':
                            self._delta_crl_distribution_points.append(distribution_point)

        return self._delta_crl_distribution_points

    @property
    def signature(self):
        """
        :return:
            A byte string of the signature
        """

        return self['signature'].native

    @property
    def sha1(self):
        """
        :return:
            The SHA1 hash of the DER-encoded bytes of this certificate list
        """

        if self._sha1 is None:
            self._sha1 = hashlib.sha1(self.dump()).digest()
        return self._sha1

    @property
    def sha256(self):
        """
        :return:
            The SHA-256 hash of the DER-encoded bytes of this certificate list
        """

        if self._sha256 is None:
            self._sha256 = hashlib.sha256(self.dump()).digest()
        return self._sha256
