IssuingDistributionPoint support (parse only) (#4552)

* IssuingDistributionPoint support

h/t to Irina Renteria for the initial work here

* python 2 unfortunately still exists

* py2 repr

* typo caught by flake8

* add docs

* review feedback

* reorder args, other fixes

* use the alex name

* add changelog
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 0cc468c..b75836d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -18,6 +18,7 @@
   1.1.1.
 * Added initial support for parsing PKCS12 files with
   :func:`~cryptography.hazmat.primitives.serialization.pkcs12.load_key_and_certificates`.
+* Added support for :class:`~cryptography.x509.IssuingDistributionPoint`.
 
 .. _v2-4-2:
 
diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst
index 5e81491..1589105 100644
--- a/docs/x509/reference.rst
+++ b/docs/x509/reference.rst
@@ -2319,6 +2319,77 @@
 
         :type: int
 
+.. class:: IssuingDistributionPoint(full_name, relative_name,\
+           only_contains_user_certs, only_contains_ca_certs, only_some_reasons,\
+           indirect_crl, only_contains_attribute_certs)
+
+    .. versionadded:: 2.5
+
+    Issuing distribution point is a CRL extension that identifies the CRL
+    distribution point and scope for a particular CRL. It indicates whether
+    the CRL covers revocation for end entity certificates only, CA certificates
+    only, attribute certificates only, or a limited set of reason codes. For
+    specific details on the way this extension should be processed see
+    :rfc:`5280`.
+
+    .. attribute:: oid
+
+        :type: :class:`ObjectIdentifier`
+
+        Returns
+        :attr:`~cryptography.x509.oid.ExtensionOID.ISSUING_DISTRIBUTION_POINT`.
+
+    .. attribute:: only_contains_user_certs
+
+        :type: bool
+
+        Set to ``True`` if the CRL this extension is embedded within only
+        contains information about user certificates.
+
+    .. attribute:: only_contains_ca_certs
+
+        :type: bool
+
+        Set to ``True`` if the CRL this extension is embedded within only
+        contains information about CA certificates.
+
+    .. attribute:: indirect_crl
+
+        :type: bool
+
+        Set to ``True`` if the CRL this extension is embedded within includes
+        certificates issued by one or more authorities other than the CRL
+        issuer.
+
+    .. attribute:: only_contains_attribute_certs
+
+        :type: bool
+
+        Set to ``True`` if the CRL this extension is embedded within only
+        contains information about attribute certificates.
+
+    .. attribute:: only_some_reasons
+
+        :type: frozenset of :class:`ReasonFlags` or None
+
+        The reasons for which the issuing distribution point is valid. None
+        indicates that it is valid for all reasons.
+
+    .. attribute:: full_name
+
+        :type: list of :class:`GeneralName` instances or None
+
+        This field describes methods to retrieve the CRL. At most one of
+        ``full_name`` or ``relative_name`` will be non-None.
+
+    .. attribute:: relative_name
+
+        :type: :class:`RelativeDistinguishedName` or None
+
+        This field describes methods to retrieve the CRL relative to the CRL
+        issuer. At most one of ``full_name`` or ``relative_name`` will be
+        non-None.
+
 .. class:: UnrecognizedExtension
 
     .. versionadded:: 1.2
diff --git a/src/cryptography/hazmat/backends/openssl/decode_asn1.py b/src/cryptography/hazmat/backends/openssl/decode_asn1.py
index e06e8cd..007675d 100644
--- a/src/cryptography/hazmat/backends/openssl/decode_asn1.py
+++ b/src/cryptography/hazmat/backends/openssl/decode_asn1.py
@@ -464,6 +464,30 @@
     return subtrees
 
 
+def _decode_issuing_dist_point(backend, idp):
+    idp = backend._ffi.cast("ISSUING_DIST_POINT *", idp)
+    idp = backend._ffi.gc(idp, backend._lib.ISSUING_DIST_POINT_free)
+    if idp.distpoint != backend._ffi.NULL:
+        full_name, relative_name = _decode_distpoint(backend, idp.distpoint)
+    else:
+        full_name = None
+        relative_name = None
+
+    only_user = idp.onlyuser == 255
+    only_ca = idp.onlyCA == 255
+    indirect_crl = idp.indirectCRL == 255
+    only_attr = idp.onlyattr == 255
+    if idp.onlysomereasons != backend._ffi.NULL:
+        only_some_reasons = _decode_reasons(backend, idp.onlysomereasons)
+    else:
+        only_some_reasons = None
+
+    return x509.IssuingDistributionPoint(
+        full_name, relative_name, only_user, only_ca, only_some_reasons,
+        indirect_crl, only_attr
+    )
+
+
 def _decode_policy_constraints(backend, pc):
     pc = backend._ffi.cast("POLICY_CONSTRAINTS *", pc)
     pc = backend._ffi.gc(pc, backend._lib.POLICY_CONSTRAINTS_free)
@@ -814,6 +838,7 @@
     ExtensionOID.AUTHORITY_INFORMATION_ACCESS: (
         _decode_authority_information_access
     ),
+    ExtensionOID.ISSUING_DISTRIBUTION_POINT: _decode_issuing_dist_point,
 }
 
 _OCSP_REQ_EXTENSION_HANDLERS = {
diff --git a/src/cryptography/x509/__init__.py b/src/cryptography/x509/__init__.py
index fd01945..b761e26 100644
--- a/src/cryptography/x509/__init__.py
+++ b/src/cryptography/x509/__init__.py
@@ -21,9 +21,9 @@
     DeltaCRLIndicator, DistributionPoint, DuplicateExtension, ExtendedKeyUsage,
     Extension, ExtensionNotFound, ExtensionType, Extensions, FreshestCRL,
     GeneralNames, InhibitAnyPolicy, InvalidityDate, IssuerAlternativeName,
-    KeyUsage, NameConstraints, NoticeReference, OCSPNoCheck, OCSPNonce,
-    PolicyConstraints, PolicyInformation, PrecertPoison,
-    PrecertificateSignedCertificateTimestamps, ReasonFlags,
+    IssuingDistributionPoint, KeyUsage, NameConstraints, NoticeReference,
+    OCSPNoCheck, OCSPNonce, PolicyConstraints, PolicyInformation,
+    PrecertPoison, PrecertificateSignedCertificateTimestamps, ReasonFlags,
     SubjectAlternativeName, SubjectKeyIdentifier, TLSFeature, TLSFeatureType,
     UnrecognizedExtension, UserNotice
 )
@@ -134,6 +134,7 @@
     "Extension",
     "ExtendedKeyUsage",
     "FreshestCRL",
+    "IssuingDistributionPoint",
     "TLSFeature",
     "TLSFeatureType",
     "OCSPNoCheck",
diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py
index fc5c17a..12071b6 100644
--- a/src/cryptography/x509/extensions.py
+++ b/src/cryptography/x509/extensions.py
@@ -1447,6 +1447,136 @@
 
 
 @utils.register_interface(ExtensionType)
+class IssuingDistributionPoint(object):
+    oid = ExtensionOID.ISSUING_DISTRIBUTION_POINT
+
+    def __init__(self, full_name, relative_name, only_contains_user_certs,
+                 only_contains_ca_certs, only_some_reasons, indirect_crl,
+                 only_contains_attribute_certs):
+        if (
+            only_some_reasons and (
+                not isinstance(only_some_reasons, frozenset) or not all(
+                    isinstance(x, ReasonFlags) for x in only_some_reasons
+                )
+            )
+        ):
+            raise TypeError(
+                "only_some_reasons must be None or frozenset of ReasonFlags"
+            )
+
+        if only_some_reasons and (
+            ReasonFlags.unspecified in only_some_reasons or
+            ReasonFlags.remove_from_crl in only_some_reasons
+        ):
+            raise ValueError(
+                "unspecified and remove_from_crl are not valid reasons in an "
+                "IssuingDistributionPoint"
+            )
+
+        if not (
+            isinstance(only_contains_user_certs, bool) and
+            isinstance(only_contains_ca_certs, bool) and
+            isinstance(indirect_crl, bool) and
+            isinstance(only_contains_attribute_certs, bool)
+        ):
+            raise TypeError(
+                "only_contains_user_certs, only_contains_ca_certs, "
+                "indirect_crl and only_contains_attribute_certs "
+                "must all be boolean."
+            )
+
+        crl_constraints = [
+            only_contains_user_certs, only_contains_ca_certs,
+            indirect_crl, only_contains_attribute_certs
+        ]
+
+        if len([x for x in crl_constraints if x]) > 1:
+            raise ValueError(
+                "Only one of the following can be set to True: "
+                "only_contains_user_certs, only_contains_ca_certs, "
+                "indirect_crl, only_contains_attribute_certs"
+            )
+
+        if (
+            not any([
+                only_contains_user_certs, only_contains_ca_certs,
+                indirect_crl, only_contains_attribute_certs, full_name,
+                relative_name, only_some_reasons
+            ])
+        ):
+            raise ValueError(
+                "Cannot create empty extension: "
+                "if only_contains_user_certs, only_contains_ca_certs, "
+                "indirect_crl, and only_contains_attribute_certs are all False"
+                ", then either full_name, relative_name, or only_some_reasons "
+                "must have a value."
+            )
+
+        self._only_contains_user_certs = only_contains_user_certs
+        self._only_contains_ca_certs = only_contains_ca_certs
+        self._indirect_crl = indirect_crl
+        self._only_contains_attribute_certs = only_contains_attribute_certs
+        self._only_some_reasons = only_some_reasons
+        self._full_name = full_name
+        self._relative_name = relative_name
+
+    def __repr__(self):
+        return (
+            "<IssuingDistributionPoint(full_name={0.full_name}, "
+            "relative_name={0.relative_name}, "
+            "only_contains_user_certs={0.only_contains_user_certs}, "
+            "only_contains_ca_certs={0.only_contains_ca_certs}, "
+            "only_some_reasons={0.only_some_reasons}, "
+            "indirect_crl={0.indirect_crl}, "
+            "only_contains_attribute_certs="
+            "{0.only_contains_attribute_certs})>".format(self)
+        )
+
+    def __eq__(self, other):
+        if not isinstance(other, IssuingDistributionPoint):
+            return NotImplemented
+
+        return (
+            self.full_name == other.full_name and
+            self.relative_name == other.relative_name and
+            self.only_contains_user_certs == other.only_contains_user_certs and
+            self.only_contains_ca_certs == other.only_contains_ca_certs and
+            self.only_some_reasons == other.only_some_reasons and
+            self.indirect_crl == other.indirect_crl and
+            self.only_contains_attribute_certs ==
+            other.only_contains_attribute_certs
+        )
+
+    def __ne__(self, other):
+        return not self == other
+
+    def __hash__(self):
+        return hash((
+            self.full_name,
+            self.relative_name,
+            self.only_contains_user_certs,
+            self.only_contains_ca_certs,
+            self.only_some_reasons,
+            self.indirect_crl,
+            self.only_contains_attribute_certs,
+        ))
+
+    full_name = utils.read_only_property("_full_name")
+    relative_name = utils.read_only_property("_relative_name")
+    only_contains_user_certs = utils.read_only_property(
+        "_only_contains_user_certs"
+    )
+    only_contains_ca_certs = utils.read_only_property(
+        "_only_contains_ca_certs"
+    )
+    only_some_reasons = utils.read_only_property("_only_some_reasons")
+    indirect_crl = utils.read_only_property("_indirect_crl")
+    only_contains_attribute_certs = utils.read_only_property(
+        "_only_contains_attribute_certs"
+    )
+
+
+@utils.register_interface(ExtensionType)
 class UnrecognizedExtension(object):
     def __init__(self, oid, value):
         if not isinstance(oid, ObjectIdentifier):
diff --git a/tests/x509/test_x509_ext.py b/tests/x509/test_x509_ext.py
index 9eac9a2..5ff3bdd 100644
--- a/tests/x509/test_x509_ext.py
+++ b/tests/x509/test_x509_ext.py
@@ -4440,6 +4440,294 @@
         assert iap.skip_certs == 5
 
 
+class TestIssuingDistributionPointExtension(object):
+    @pytest.mark.parametrize(
+        ("filename", "expected"),
+        [
+            (
+                "crl_idp_fullname_indirect_crl.pem",
+                x509.IssuingDistributionPoint(
+                    full_name=[
+                        x509.UniformResourceIdentifier(
+                            u"http://myhost.com/myca.crl")
+                    ],
+                    relative_name=None,
+                    only_contains_user_certs=False,
+                    only_contains_ca_certs=False,
+                    only_some_reasons=None,
+                    indirect_crl=True,
+                    only_contains_attribute_certs=False,
+                )
+            ),
+            (
+                "crl_idp_fullname_only.pem",
+                x509.IssuingDistributionPoint(
+                    full_name=[
+                        x509.UniformResourceIdentifier(
+                            u"http://myhost.com/myca.crl")
+                    ],
+                    relative_name=None,
+                    only_contains_user_certs=False,
+                    only_contains_ca_certs=False,
+                    only_some_reasons=None,
+                    indirect_crl=False,
+                    only_contains_attribute_certs=False,
+                )
+            ),
+            (
+                "crl_idp_fullname_only_aa.pem",
+                x509.IssuingDistributionPoint(
+                    full_name=[
+                        x509.UniformResourceIdentifier(
+                            u"http://myhost.com/myca.crl")
+                    ],
+                    relative_name=None,
+                    only_contains_user_certs=False,
+                    only_contains_ca_certs=False,
+                    only_some_reasons=None,
+                    indirect_crl=False,
+                    only_contains_attribute_certs=True,
+                )
+            ),
+            (
+                "crl_idp_fullname_only_user.pem",
+                x509.IssuingDistributionPoint(
+                    full_name=[
+                        x509.UniformResourceIdentifier(
+                            u"http://myhost.com/myca.crl")
+                    ],
+                    relative_name=None,
+                    only_contains_user_certs=True,
+                    only_contains_ca_certs=False,
+                    only_some_reasons=None,
+                    indirect_crl=False,
+                    only_contains_attribute_certs=False,
+                )
+            ),
+            (
+                "crl_idp_only_ca.pem",
+                x509.IssuingDistributionPoint(
+                    full_name=None,
+                    relative_name=x509.RelativeDistinguishedName([
+                        x509.NameAttribute(
+                            oid=x509.NameOID.ORGANIZATION_NAME, value=u"PyCA"
+                        )
+                    ]),
+                    only_contains_user_certs=False,
+                    only_contains_ca_certs=True,
+                    only_some_reasons=None,
+                    indirect_crl=False,
+                    only_contains_attribute_certs=False,
+                )
+            ),
+            (
+                "crl_idp_reasons_only.pem",
+                x509.IssuingDistributionPoint(
+                    full_name=None,
+                    relative_name=None,
+                    only_contains_user_certs=False,
+                    only_contains_ca_certs=False,
+                    only_some_reasons=frozenset([
+                        x509.ReasonFlags.key_compromise
+                    ]),
+                    indirect_crl=False,
+                    only_contains_attribute_certs=False,
+                )
+            ),
+            (
+                "crl_idp_relative_user_all_reasons.pem",
+                x509.IssuingDistributionPoint(
+                    full_name=None,
+                    relative_name=x509.RelativeDistinguishedName([
+                        x509.NameAttribute(
+                            oid=x509.NameOID.ORGANIZATION_NAME, value=u"PyCA"
+                        )
+                    ]),
+                    only_contains_user_certs=True,
+                    only_contains_ca_certs=False,
+                    only_some_reasons=frozenset([
+                        x509.ReasonFlags.key_compromise,
+                        x509.ReasonFlags.ca_compromise,
+                        x509.ReasonFlags.affiliation_changed,
+                        x509.ReasonFlags.superseded,
+                        x509.ReasonFlags.cessation_of_operation,
+                        x509.ReasonFlags.certificate_hold,
+                        x509.ReasonFlags.privilege_withdrawn,
+                        x509.ReasonFlags.aa_compromise,
+                    ]),
+                    indirect_crl=False,
+                    only_contains_attribute_certs=False,
+                )
+            ),
+            (
+                "crl_idp_relativename_only.pem",
+                x509.IssuingDistributionPoint(
+                    full_name=None,
+                    relative_name=x509.RelativeDistinguishedName([
+                        x509.NameAttribute(
+                            oid=x509.NameOID.ORGANIZATION_NAME, value=u"PyCA"
+                        )
+                    ]),
+                    only_contains_user_certs=False,
+                    only_contains_ca_certs=False,
+                    only_some_reasons=None,
+                    indirect_crl=False,
+                    only_contains_attribute_certs=False,
+                )
+            ),
+        ]
+    )
+    @pytest.mark.requires_backend_interface(interface=RSABackend)
+    @pytest.mark.requires_backend_interface(interface=X509Backend)
+    def test_vectors(self, filename, expected, backend):
+        crl = _load_cert(
+            os.path.join("x509", "custom", filename),
+            x509.load_pem_x509_crl, backend
+        )
+        idp = crl.extensions.get_extension_for_class(
+            x509.IssuingDistributionPoint
+        ).value
+        assert idp == expected
+
+    @pytest.mark.parametrize(
+        (
+            "error", "only_contains_user_certs", "only_contains_ca_certs",
+            "indirect_crl", "only_contains_attribute_certs",
+            "only_some_reasons", "full_name", "relative_name"
+        ),
+        [
+            (
+                TypeError, False, False, False, False, 'notafrozenset', None,
+                None
+            ),
+            (
+                TypeError, False, False, False, False, frozenset(['bad']),
+                None, None
+            ),
+            (
+                ValueError, False, False, False, False,
+                frozenset([x509.ReasonFlags.unspecified]), None, None
+            ),
+            (
+                ValueError, False, False, False, False,
+                frozenset([x509.ReasonFlags.remove_from_crl]), None, None
+            ),
+            (TypeError, 'notabool', False, False, False, None, None, None),
+            (TypeError, False, 'notabool', False, False, None, None, None),
+            (TypeError, False, False, 'notabool', False, None, None, None),
+            (TypeError, False, False, False, 'notabool', None, None, None),
+            (ValueError, True, True, False, False, None, None, None),
+            (ValueError, False, False, True, True, None, None, None),
+            (ValueError, False, False, False, False, None, None, None),
+        ]
+    )
+    def test_invalid_init(self, error, only_contains_user_certs,
+                          only_contains_ca_certs, indirect_crl,
+                          only_contains_attribute_certs, only_some_reasons,
+                          full_name, relative_name):
+        with pytest.raises(error):
+            x509.IssuingDistributionPoint(
+                full_name, relative_name, only_contains_user_certs,
+                only_contains_ca_certs, only_some_reasons, indirect_crl,
+                only_contains_attribute_certs
+            )
+
+    def test_repr(self):
+        idp = x509.IssuingDistributionPoint(
+            None, None, False, False,
+            frozenset([x509.ReasonFlags.key_compromise]), False, False
+        )
+        if not six.PY2:
+            assert repr(idp) == (
+                "<IssuingDistributionPoint(full_name=None, relative_name=None,"
+                " only_contains_user_certs=False, only_contains_ca_certs=False"
+                ", only_some_reasons=frozenset({<ReasonFlags.key_compromise: '"
+                "keyCompromise'>}), indirect_crl=False, only_contains_attribut"
+                "e_certs=False)>"
+            )
+        else:
+            assert repr(idp) == (
+                "<IssuingDistributionPoint(full_name=None, relative_name=None,"
+                " only_contains_user_certs=False, only_contains_ca_certs=False"
+                ", only_some_reasons=frozenset([<ReasonFlags.key_compromise: '"
+                "keyCompromise'>]), indirect_crl=False, only_contains_attribut"
+                "e_certs=False)>"
+            )
+
+    def test_eq(self):
+        idp1 = x509.IssuingDistributionPoint(
+            only_contains_user_certs=False,
+            only_contains_ca_certs=False,
+            indirect_crl=False,
+            only_contains_attribute_certs=False,
+            only_some_reasons=None,
+            full_name=None,
+            relative_name=x509.RelativeDistinguishedName([
+                x509.NameAttribute(
+                    oid=x509.NameOID.ORGANIZATION_NAME, value=u"PyCA")
+            ])
+        )
+        idp2 = x509.IssuingDistributionPoint(
+            only_contains_user_certs=False,
+            only_contains_ca_certs=False,
+            indirect_crl=False,
+            only_contains_attribute_certs=False,
+            only_some_reasons=None,
+            full_name=None,
+            relative_name=x509.RelativeDistinguishedName([
+                x509.NameAttribute(
+                    oid=x509.NameOID.ORGANIZATION_NAME, value=u"PyCA")
+            ])
+        )
+        assert idp1 == idp2
+
+    def test_ne(self):
+        idp1 = x509.IssuingDistributionPoint(
+            only_contains_user_certs=False,
+            only_contains_ca_certs=False,
+            indirect_crl=False,
+            only_contains_attribute_certs=False,
+            only_some_reasons=None,
+            full_name=None,
+            relative_name=x509.RelativeDistinguishedName([
+                x509.NameAttribute(
+                    oid=x509.NameOID.ORGANIZATION_NAME, value=u"PyCA")
+            ])
+        )
+        idp2 = x509.IssuingDistributionPoint(
+            only_contains_user_certs=True,
+            only_contains_ca_certs=False,
+            indirect_crl=False,
+            only_contains_attribute_certs=False,
+            only_some_reasons=None,
+            full_name=None,
+            relative_name=x509.RelativeDistinguishedName([
+                x509.NameAttribute(
+                    oid=x509.NameOID.ORGANIZATION_NAME, value=u"PyCA")
+            ])
+        )
+        assert idp1 != idp2
+        assert idp1 != object()
+
+    def test_hash(self):
+        idp1 = x509.IssuingDistributionPoint(
+            None, None, True, False, None, False, False
+        )
+        idp2 = x509.IssuingDistributionPoint(
+            None, None, True, False, None, False, False
+        )
+        idp3 = x509.IssuingDistributionPoint(
+            None,
+            x509.RelativeDistinguishedName([
+                x509.NameAttribute(
+                    oid=x509.NameOID.ORGANIZATION_NAME, value=u"PyCA")
+            ]),
+            True, False, None, False, False
+        )
+        assert hash(idp1) == hash(idp2)
+        assert hash(idp1) != hash(idp3)
+
+
 @pytest.mark.requires_backend_interface(interface=RSABackend)
 @pytest.mark.requires_backend_interface(interface=X509Backend)
 class TestPrecertPoisonExtension(object):