Merge pull request #1906 from reaperhulk/cdp

add CRLDistributionPoints and associated classes
diff --git a/docs/x509.rst b/docs/x509.rst
index f4ea2a5..86673e3 100644
--- a/docs/x509.rst
+++ b/docs/x509.rst
@@ -781,6 +781,8 @@
 
 .. class:: AccessDescription
 
+    .. versionadded:: 0.9
+
     .. attribute:: access_method
 
         :type: :class:`ObjectIdentifier`
@@ -798,6 +800,98 @@
 
         Where to access the information defined by the access method.
 
+.. class:: CRLDistributionPoints
+
+    .. versionadded:: 0.9
+
+    The CRL distribution points extension identifies how CRL information is
+    obtained. It is an iterable, containing one or more
+    :class:`DistributionPoint` instances.
+
+.. class:: DistributionPoint
+
+    .. versionadded:: 0.9
+
+    .. 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:`Name` 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.
+
+    .. attribute:: crl_issuer
+
+        :type: list of :class:`GeneralName` instances or None
+
+        Information about the issuer of the CRL.
+
+    .. attribute:: reasons
+
+        :type: frozenset of :class:`ReasonFlags` or None
+
+        The reasons a given distribution point may be used for when performing
+        revocation checks.
+
+.. class:: ReasonFlags
+
+    .. versionadded:: 0.9
+
+    An enumeration for CRL reasons.
+
+    .. attribute:: unspecified
+
+        It is unspecified why the certificate was revoked. This reason cannot
+        be used as a reason flag in a :class:`DistributionPoint`.
+
+    .. attribute:: key_compromise
+
+        This reason indicates that the private key was compromised.
+
+    .. attribute:: ca_compromise
+
+        This reason indicates that the CA issuing the certificate was
+        compromised.
+
+    .. attribute:: affiliation_changed
+
+        This reason indicates that the subject's name or other information has
+        changed.
+
+    .. attribute:: superseded
+
+        This reason indicates that a certificate has been superseded.
+
+    .. attribute:: cessation_of_operation
+
+        This reason indicates that the certificate is no longer required.
+
+    .. attribute:: certificate_hold
+
+        This reason indicates that the certificate is on hold.
+
+    .. attribute:: privilege_withdrawn
+
+        This reason indicates that the privilege granted by this certificate
+        have been withdrawn.
+
+    .. attribute:: aa_compromise
+
+        When an attribute authority has been compromised.
+
+    .. attribute:: remove_from_crl
+
+        This reason indicates that the certificate was on hold and should be
+        removed from the CRL. This reason cannot be used as a reason flag
+        in a :class:`DistributionPoint`.
+
 Object Identifiers
 ~~~~~~~~~~~~~~~~~~
 
diff --git a/src/cryptography/x509.py b/src/cryptography/x509.py
index 0d87cd5..dfc0af8 100644
--- a/src/cryptography/x509.py
+++ b/src/cryptography/x509.py
@@ -481,6 +481,126 @@
         return not self == other
 
 
+class CRLDistributionPoints(object):
+    def __init__(self, distribution_points):
+        if not all(
+            isinstance(x, DistributionPoint) for x in distribution_points
+        ):
+            raise TypeError(
+                "distribution_points must be a list of DistributionPoint "
+                "objects"
+            )
+
+        self._distribution_points = distribution_points
+
+    def __iter__(self):
+        return iter(self._distribution_points)
+
+    def __len__(self):
+        return len(self._distribution_points)
+
+    def __repr__(self):
+        return "<CRLDistributionPoints({0})>".format(self._distribution_points)
+
+    def __eq__(self, other):
+        if not isinstance(other, CRLDistributionPoints):
+            return NotImplemented
+
+        return self._distribution_points == other._distribution_points
+
+    def __ne__(self, other):
+        return not self == other
+
+
+class DistributionPoint(object):
+    def __init__(self, full_name, relative_name, reasons, crl_issuer):
+        if full_name and relative_name:
+            raise ValueError(
+                "At least one of full_name and relative_name must be None"
+            )
+
+        if full_name and not all(
+            isinstance(x, GeneralName) for x in full_name
+        ):
+            raise TypeError(
+                "full_name must be a list of GeneralName objects"
+            )
+
+        if relative_name and not isinstance(relative_name, Name):
+            raise TypeError("relative_name must be a Name")
+
+        if crl_issuer and not all(
+            isinstance(x, GeneralName) for x in crl_issuer
+        ):
+            raise TypeError(
+                "crl_issuer must be None or a list of general names"
+            )
+
+        if reasons and (not isinstance(reasons, frozenset) or not all(
+            isinstance(x, ReasonFlags) for x in reasons
+        )):
+            raise TypeError("reasons must be None or frozenset of ReasonFlags")
+
+        if reasons and (
+            ReasonFlags.unspecified in reasons or
+            ReasonFlags.remove_from_crl in reasons
+        ):
+            raise ValueError(
+                "unspecified and remove_from_crl are not valid reasons in a "
+                "DistributionPoint"
+            )
+
+        if reasons and not crl_issuer and not (full_name or relative_name):
+            raise ValueError(
+                "You must supply crl_issuer, full_name, or relative_name when "
+                "reasons is not None"
+            )
+
+        self._full_name = full_name
+        self._relative_name = relative_name
+        self._reasons = reasons
+        self._crl_issuer = crl_issuer
+
+    def __repr__(self):
+        return (
+            "<DistributionPoint(full_name={0.full_name}, relative_name={0.rela"
+            "tive_name}, reasons={0.reasons}, crl_issuer={0.crl_is"
+            "suer})>".format(self)
+        )
+
+    def __eq__(self, other):
+        if not isinstance(other, DistributionPoint):
+            return NotImplemented
+
+        return (
+            self.full_name == other.full_name and
+            self.relative_name == other.relative_name and
+            self.reasons == other.reasons and
+            self.crl_issuer == other.crl_issuer
+        )
+
+    def __ne__(self, other):
+        return not self == other
+
+    full_name = utils.read_only_property("_full_name")
+    relative_name = utils.read_only_property("_relative_name")
+    reasons = utils.read_only_property("_reasons")
+    crl_issuer = utils.read_only_property("_crl_issuer")
+
+
+class ReasonFlags(Enum):
+    unspecified = "unspecified"
+    key_compromise = "keyCompromise"
+    ca_compromise = "cACompromise"
+    affiliation_changed = "affiliationChanged"
+    superseded = "superseded"
+    cessation_of_operation = "cessationOfOperation"
+    certificate_hold = "certificateHold"
+    privilege_withdrawn = "privilegeWithdrawn"
+    aa_compromise = "aACompromise"
+    remove_from_crl = "removeFromCRL"
+
+
 @six.add_metaclass(abc.ABCMeta)
 class GeneralName(object):
     @abc.abstractproperty
diff --git a/tests/test_x509_ext.py b/tests/test_x509_ext.py
index 8a22795..06a6860 100644
--- a/tests/test_x509_ext.py
+++ b/tests/test_x509_ext.py
@@ -1318,3 +1318,295 @@
             )
         ]
         assert ext.value.authority_cert_serial_number == 3
+
+
+class TestDistributionPoint(object):
+    def test_distribution_point_full_name_not_general_names(self):
+        with pytest.raises(TypeError):
+            x509.DistributionPoint(["notgn"], None, None, None)
+
+    def test_distribution_point_relative_name_not_name(self):
+        with pytest.raises(TypeError):
+            x509.DistributionPoint(None, "notname", None, None)
+
+    def test_distribution_point_full_and_relative_not_none(self):
+        with pytest.raises(ValueError):
+            x509.DistributionPoint("data", "notname", None, None)
+
+    def test_crl_issuer_not_general_names(self):
+        with pytest.raises(TypeError):
+            x509.DistributionPoint(None, None, None, ["notgn"])
+
+    def test_reason_not_reasonflags(self):
+        with pytest.raises(TypeError):
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(u"http://crypt.og/crl")],
+                None,
+                frozenset(["notreasonflags"]),
+                None
+            )
+
+    def test_reason_not_frozenset(self):
+        with pytest.raises(TypeError):
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(u"http://crypt.og/crl")],
+                None,
+                [x509.ReasonFlags.ca_compromise],
+                None
+            )
+
+    def test_disallowed_reasons(self):
+        with pytest.raises(ValueError):
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(u"http://crypt.og/crl")],
+                None,
+                frozenset([x509.ReasonFlags.unspecified]),
+                None
+            )
+
+        with pytest.raises(ValueError):
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(u"http://crypt.og/crl")],
+                None,
+                frozenset([x509.ReasonFlags.remove_from_crl]),
+                None
+            )
+
+    def test_reason_only(self):
+        with pytest.raises(ValueError):
+            x509.DistributionPoint(
+                None,
+                None,
+                frozenset([x509.ReasonFlags.aa_compromise]),
+                None
+            )
+
+    def test_eq(self):
+        dp = x509.DistributionPoint(
+            [x509.UniformResourceIdentifier(u"http://crypt.og/crl")],
+            None,
+            frozenset([x509.ReasonFlags.superseded]),
+            [
+                x509.DirectoryName(
+                    x509.Name([
+                        x509.NameAttribute(
+                            x509.OID_COMMON_NAME, "Important CA"
+                        )
+                    ])
+                )
+            ],
+        )
+        dp2 = x509.DistributionPoint(
+            [x509.UniformResourceIdentifier(u"http://crypt.og/crl")],
+            None,
+            frozenset([x509.ReasonFlags.superseded]),
+            [
+                x509.DirectoryName(
+                    x509.Name([
+                        x509.NameAttribute(
+                            x509.OID_COMMON_NAME, "Important CA"
+                        )
+                    ])
+                )
+            ],
+        )
+        assert dp == dp2
+
+    def test_ne(self):
+        dp = x509.DistributionPoint(
+            [x509.UniformResourceIdentifier(u"http://crypt.og/crl")],
+            None,
+            frozenset([x509.ReasonFlags.superseded]),
+            [
+                x509.DirectoryName(
+                    x509.Name([
+                        x509.NameAttribute(
+                            x509.OID_COMMON_NAME, "Important CA"
+                        )
+                    ])
+                )
+            ],
+        )
+        dp2 = x509.DistributionPoint(
+            [x509.UniformResourceIdentifier(u"http://crypt.og/crl")],
+            None,
+            None,
+            None
+        )
+        assert dp != dp2
+        assert dp != object()
+
+    def test_repr(self):
+        dp = x509.DistributionPoint(
+            None,
+            x509.Name([
+                x509.NameAttribute(x509.OID_COMMON_NAME, "myCN")
+            ]),
+            frozenset([x509.ReasonFlags.ca_compromise]),
+            [
+                x509.DirectoryName(
+                    x509.Name([
+                        x509.NameAttribute(
+                            x509.OID_COMMON_NAME, "Important CA"
+                        )
+                    ])
+                )
+            ],
+        )
+        if six.PY3:
+            assert repr(dp) == (
+                "<DistributionPoint(full_name=None, relative_name=<Name([<Name"
+                "Attribute(oid=<ObjectIdentifier(oid=2.5.4.3, name=commonName)"
+                ">, value='myCN')>])>, reasons=frozenset({<ReasonFlags.ca_comp"
+                "romise: 'cACompromise'>}), crl_issuer=[<DirectoryName(value=<"
+                "Name([<NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.3, name="
+                "commonName)>, value='Important CA')>])>)>])>"
+            )
+        else:
+            assert repr(dp) == (
+                "<DistributionPoint(full_name=None, relative_name=<Name([<Name"
+                "Attribute(oid=<ObjectIdentifier(oid=2.5.4.3, name=commonName)"
+                ">, value='myCN')>])>, reasons=frozenset([<ReasonFlags.ca_comp"
+                "romise: 'cACompromise'>]), crl_issuer=[<DirectoryName(value=<"
+                "Name([<NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.3, name="
+                "commonName)>, value='Important CA')>])>)>])>"
+            )
+
+
+class TestCRLDistributionPoints(object):
+    def test_invalid_distribution_points(self):
+        with pytest.raises(TypeError):
+            x509.CRLDistributionPoints(["notadistributionpoint"])
+
+    def test_iter_len(self):
+        cdp = x509.CRLDistributionPoints([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(u"http://domain")],
+                None,
+                None,
+                None
+            ),
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(u"ftp://domain")],
+                None,
+                frozenset([
+                    x509.ReasonFlags.key_compromise,
+                    x509.ReasonFlags.ca_compromise,
+                ]),
+                None
+            ),
+        ])
+        assert len(cdp) == 2
+        assert list(cdp) == [
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(u"http://domain")],
+                None,
+                None,
+                None
+            ),
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(u"ftp://domain")],
+                None,
+                frozenset([
+                    x509.ReasonFlags.key_compromise,
+                    x509.ReasonFlags.ca_compromise,
+                ]),
+                None
+            ),
+        ]
+
+    def test_repr(self):
+        cdp = x509.CRLDistributionPoints([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(u"ftp://domain")],
+                None,
+                frozenset([x509.ReasonFlags.key_compromise]),
+                None
+            ),
+        ])
+        if six.PY3:
+            assert repr(cdp) == (
+                "<CRLDistributionPoints([<DistributionPoint(full_name=[<Unifo"
+                "rmResourceIdentifier(value=ftp://domain)>], relative_name=No"
+                "ne, reasons=frozenset({<ReasonFlags.key_compromise: 'keyComp"
+                "romise'>}), crl_issuer=None)>])>"
+            )
+        else:
+            assert repr(cdp) == (
+                "<CRLDistributionPoints([<DistributionPoint(full_name=[<Unifo"
+                "rmResourceIdentifier(value=ftp://domain)>], relative_name=No"
+                "ne, reasons=frozenset([<ReasonFlags.key_compromise: 'keyComp"
+                "romise'>]), crl_issuer=None)>])>"
+            )
+
+    def test_eq(self):
+        cdp = x509.CRLDistributionPoints([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(u"ftp://domain")],
+                None,
+                frozenset([
+                    x509.ReasonFlags.key_compromise,
+                    x509.ReasonFlags.ca_compromise,
+                ]),
+                [x509.UniformResourceIdentifier(u"uri://thing")],
+            ),
+        ])
+        cdp2 = x509.CRLDistributionPoints([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(u"ftp://domain")],
+                None,
+                frozenset([
+                    x509.ReasonFlags.key_compromise,
+                    x509.ReasonFlags.ca_compromise,
+                ]),
+                [x509.UniformResourceIdentifier(u"uri://thing")],
+            ),
+        ])
+        assert cdp == cdp2
+
+    def test_ne(self):
+        cdp = x509.CRLDistributionPoints([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(u"ftp://domain")],
+                None,
+                frozenset([
+                    x509.ReasonFlags.key_compromise,
+                    x509.ReasonFlags.ca_compromise,
+                ]),
+                [x509.UniformResourceIdentifier(u"uri://thing")],
+            ),
+        ])
+        cdp2 = x509.CRLDistributionPoints([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(u"ftp://domain2")],
+                None,
+                frozenset([
+                    x509.ReasonFlags.key_compromise,
+                    x509.ReasonFlags.ca_compromise,
+                ]),
+                [x509.UniformResourceIdentifier(u"uri://thing")],
+            ),
+        ])
+        cdp3 = x509.CRLDistributionPoints([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(u"ftp://domain")],
+                None,
+                frozenset([x509.ReasonFlags.key_compromise]),
+                [x509.UniformResourceIdentifier(u"uri://thing")],
+            ),
+        ])
+        cdp4 = x509.CRLDistributionPoints([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(u"ftp://domain")],
+                None,
+                frozenset([
+                    x509.ReasonFlags.key_compromise,
+                    x509.ReasonFlags.ca_compromise,
+                ]),
+                [x509.UniformResourceIdentifier(u"uri://thing2")],
+            ),
+        ])
+        assert cdp != cdp2
+        assert cdp != cdp3
+        assert cdp != cdp4
+        assert cdp != object()