FreshestCRL extension support (#3937)

* add freshest CRL support

* add tests

* add changelog

* add tests for FreshestCRL generation
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index d239d75..a56c67b 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -45,6 +45,7 @@
 * Add support for the :class:`~cryptography.x509.TLSFeature`
   extension. This is commonly used for enabling ``OCSP Must-Staple`` in
   certificates.
+* Add support for the :class:`~cryptography.x509.FreshestCRL` extension.
 
 
 .. _v2-0-3:
diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst
index dea7ee3..951e6b7 100644
--- a/docs/x509/reference.rst
+++ b/docs/x509/reference.rst
@@ -2027,6 +2027,24 @@
 
         Where to access the information defined by the access method.
 
+.. class:: FreshestCRL(distribution_points)
+
+    .. versionadded:: 2.1
+
+    The freshest CRL extension (also known as Delta CRL Distribution Point)
+    identifies how delta CRL information is obtained. It is an iterable,
+    containing one or more :class:`DistributionPoint` instances.
+
+    :param list distribution_points: A list of :class:`DistributionPoint`
+        instances.
+
+    .. attribute:: oid
+
+        :type: :class:`ObjectIdentifier`
+
+        Returns
+        :attr:`~cryptography.x509.oid.ExtensionOID.FRESHEST_CRL`.
+
 .. class:: CRLDistributionPoints(distribution_points)
 
     .. versionadded:: 0.9
@@ -2792,6 +2810,11 @@
         Corresponds to the dotted string ``"2.5.29.36"``. The identifier for the
         :class:`~cryptography.x509.PolicyConstraints` extension type.
 
+    .. attribute:: FRESHEST_CRL
+
+        Corresponds to the dotted string ``"2.5.29.46"``. The identifier for the
+        :class:`~cryptography.x509.FreshestCRL` extension type.
+
 
 .. class:: CRLEntryExtensionOID
 
diff --git a/src/cryptography/hazmat/backends/openssl/decode_asn1.py b/src/cryptography/hazmat/backends/openssl/decode_asn1.py
index 1326a94..ec55a9e 100644
--- a/src/cryptography/hazmat/backends/openssl/decode_asn1.py
+++ b/src/cryptography/hazmat/backends/openssl/decode_asn1.py
@@ -463,7 +463,7 @@
 _DISTPOINT_TYPE_RELATIVENAME = 1
 
 
-def _decode_crl_distribution_points(backend, cdps):
+def _decode_dist_points(backend, cdps):
     cdps = backend._ffi.cast("Cryptography_STACK_OF_DIST_POINT *", cdps)
     cdps = backend._ffi.gc(cdps, backend._lib.CRL_DIST_POINTS_free)
 
@@ -554,9 +554,19 @@
             )
         )
 
+    return dist_points
+
+
+def _decode_crl_distribution_points(backend, cdps):
+    dist_points = _decode_dist_points(backend, cdps)
     return x509.CRLDistributionPoints(dist_points)
 
 
+def _decode_freshest_crl(backend, cdps):
+    dist_points = _decode_dist_points(backend, cdps)
+    return x509.FreshestCRL(dist_points)
+
+
 def _decode_inhibit_any_policy(backend, asn1_int):
     asn1_int = backend._ffi.cast("ASN1_INTEGER *", asn1_int)
     asn1_int = backend._ffi.gc(asn1_int, backend._lib.ASN1_INTEGER_free)
@@ -728,6 +738,7 @@
     ),
     ExtensionOID.CERTIFICATE_POLICIES: _decode_certificate_policies,
     ExtensionOID.CRL_DISTRIBUTION_POINTS: _decode_crl_distribution_points,
+    ExtensionOID.FRESHEST_CRL: _decode_freshest_crl,
     ExtensionOID.OCSP_NO_CHECK: _decode_ocsp_no_check,
     ExtensionOID.INHIBIT_ANY_POLICY: _decode_inhibit_any_policy,
     ExtensionOID.ISSUER_ALTERNATIVE_NAME: _decode_issuer_alt_name,
diff --git a/src/cryptography/hazmat/backends/openssl/encode_asn1.py b/src/cryptography/hazmat/backends/openssl/encode_asn1.py
index 5ceb29c..6b86768 100644
--- a/src/cryptography/hazmat/backends/openssl/encode_asn1.py
+++ b/src/cryptography/hazmat/backends/openssl/encode_asn1.py
@@ -484,10 +484,10 @@
 }
 
 
-def _encode_crl_distribution_points(backend, crl_distribution_points):
+def _encode_cdps_freshest_crl(backend, cdps):
     cdp = backend._lib.sk_DIST_POINT_new_null()
     cdp = backend._ffi.gc(cdp, backend._lib.sk_DIST_POINT_free)
-    for point in crl_distribution_points:
+    for point in cdps:
         dp = backend._lib.DIST_POINT_new()
         backend.openssl_assert(dp != backend._ffi.NULL)
 
@@ -585,7 +585,8 @@
     ExtensionOID.AUTHORITY_INFORMATION_ACCESS: (
         _encode_authority_information_access
     ),
-    ExtensionOID.CRL_DISTRIBUTION_POINTS: _encode_crl_distribution_points,
+    ExtensionOID.CRL_DISTRIBUTION_POINTS: _encode_cdps_freshest_crl,
+    ExtensionOID.FRESHEST_CRL: _encode_cdps_freshest_crl,
     ExtensionOID.INHIBIT_ANY_POLICY: _encode_inhibit_any_policy,
     ExtensionOID.OCSP_NO_CHECK: _encode_ocsp_nocheck,
     ExtensionOID.NAME_CONSTRAINTS: _encode_name_constraints,
diff --git a/src/cryptography/x509/__init__.py b/src/cryptography/x509/__init__.py
index e168adb..224c9af 100644
--- a/src/cryptography/x509/__init__.py
+++ b/src/cryptography/x509/__init__.py
@@ -19,9 +19,9 @@
     AuthorityKeyIdentifier, BasicConstraints, CRLDistributionPoints,
     CRLNumber, CRLReason, CertificateIssuer, CertificatePolicies,
     DeltaCRLIndicator, DistributionPoint, DuplicateExtension, ExtendedKeyUsage,
-    Extension, ExtensionNotFound, ExtensionType, Extensions, GeneralNames,
-    InhibitAnyPolicy, InvalidityDate, IssuerAlternativeName, KeyUsage,
-    NameConstraints, NoticeReference, OCSPNoCheck, PolicyConstraints,
+    Extension, ExtensionNotFound, ExtensionType, Extensions, FreshestCRL,
+    GeneralNames, InhibitAnyPolicy, InvalidityDate, IssuerAlternativeName,
+    KeyUsage, NameConstraints, NoticeReference, OCSPNoCheck, PolicyConstraints,
     PolicyInformation, PrecertificateSignedCertificateTimestamps, ReasonFlags,
     SubjectAlternativeName, SubjectKeyIdentifier, TLSFeature, TLSFeatureType,
     UnrecognizedExtension, UserNotice
@@ -131,6 +131,7 @@
     "Extensions",
     "Extension",
     "ExtendedKeyUsage",
+    "FreshestCRL",
     "TLSFeature",
     "TLSFeatureType",
     "OCSPNoCheck",
diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py
index beb20ba..eb4b927 100644
--- a/src/cryptography/x509/extensions.py
+++ b/src/cryptography/x509/extensions.py
@@ -444,6 +444,47 @@
         return hash(tuple(self._distribution_points))
 
 
+@utils.register_interface(ExtensionType)
+class FreshestCRL(object):
+    oid = ExtensionOID.FRESHEST_CRL
+
+    def __init__(self, distribution_points):
+        distribution_points = list(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 "<FreshestCRL({0})>".format(self._distribution_points)
+
+    def __eq__(self, other):
+        if not isinstance(other, FreshestCRL):
+            return NotImplemented
+
+        return self._distribution_points == other._distribution_points
+
+    def __ne__(self, other):
+        return not self == other
+
+    def __getitem__(self, idx):
+        return self._distribution_points[idx]
+
+    def __hash__(self):
+        return hash(tuple(self._distribution_points))
+
+
 class DistributionPoint(object):
     def __init__(self, full_name, relative_name, reasons, crl_issuer):
         if full_name and relative_name:
diff --git a/tests/x509/test_x509.py b/tests/x509/test_x509.py
index d0ce46d..06aef66 100644
--- a/tests/x509/test_x509.py
+++ b/tests/x509/test_x509.py
@@ -2406,6 +2406,38 @@
                     crl_issuer=None
                 )
             ]),
+            x509.FreshestCRL([
+                x509.DistributionPoint(
+                    full_name=[x509.UniformResourceIdentifier(
+                        u"http://domain.com/some.crl"
+                    )],
+                    relative_name=None,
+                    reasons=frozenset([
+                        x509.ReasonFlags.key_compromise,
+                        x509.ReasonFlags.ca_compromise,
+                        x509.ReasonFlags.affiliation_changed,
+                        x509.ReasonFlags.superseded,
+                        x509.ReasonFlags.privilege_withdrawn,
+                        x509.ReasonFlags.cessation_of_operation,
+                        x509.ReasonFlags.aa_compromise,
+                        x509.ReasonFlags.certificate_hold,
+                    ]),
+                    crl_issuer=None
+                )
+            ]),
+            x509.FreshestCRL([
+                x509.DistributionPoint(
+                    full_name=None,
+                    relative_name=x509.RelativeDistinguishedName([
+                        x509.NameAttribute(
+                            NameOID.COMMON_NAME,
+                            u"indirect CRL for indirectCRL CA3"
+                        ),
+                    ]),
+                    reasons=None,
+                    crl_issuer=None,
+                )
+            ]),
         ]
     )
     def test_ext(self, add_ext, backend):
diff --git a/tests/x509/test_x509_ext.py b/tests/x509/test_x509_ext.py
index 9f0b1b0..11e06ea 100644
--- a/tests/x509/test_x509_ext.py
+++ b/tests/x509/test_x509_ext.py
@@ -3700,6 +3700,193 @@
         assert hash(dp) != hash(dp3)
 
 
+class TestFreshestCRL(object):
+    def test_invalid_distribution_points(self):
+        with pytest.raises(TypeError):
+            x509.FreshestCRL(["notadistributionpoint"])
+
+    def test_iter_len(self):
+        fcrl = x509.FreshestCRL([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(b"http://domain")],
+                None, None, None
+            ),
+        ])
+        assert len(fcrl) == 1
+        assert list(fcrl) == [
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(b"http://domain")],
+                None, None, None
+            ),
+        ]
+
+    def test_iter_input(self):
+        points = [
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(b"http://domain")],
+                None, None, None
+            ),
+        ]
+        fcrl = x509.FreshestCRL(iter(points))
+        assert list(fcrl) == points
+
+    def test_repr(self):
+        fcrl = x509.FreshestCRL([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(b"ftp://domain")],
+                None,
+                frozenset([x509.ReasonFlags.key_compromise]),
+                None
+            ),
+        ])
+        if six.PY3:
+            assert repr(fcrl) == (
+                "<FreshestCRL([<DistributionPoint(full_name=[<Unifo"
+                "rmResourceIdentifier(bytes_value=b'ftp://domain')>], relative"
+                "_name=None, reasons=frozenset({<ReasonFlags.key_compromise: "
+                "'keyCompromise'>}), crl_issuer=None)>])>"
+            )
+        else:
+            assert repr(fcrl) == (
+                "<FreshestCRL([<DistributionPoint(full_name=[<Unifo"
+                "rmResourceIdentifier(bytes_value='ftp://domain')>], relative"
+                "_name=None, reasons=frozenset([<ReasonFlags.key_compromise: "
+                "'keyCompromise'>]), crl_issuer=None)>])>"
+            )
+
+    def test_eq(self):
+        fcrl = x509.FreshestCRL([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(b"ftp://domain")],
+                None,
+                frozenset([
+                    x509.ReasonFlags.key_compromise,
+                    x509.ReasonFlags.ca_compromise,
+                ]),
+                [x509.UniformResourceIdentifier(b"uri://thing")],
+            ),
+        ])
+        fcrl2 = x509.FreshestCRL([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(b"ftp://domain")],
+                None,
+                frozenset([
+                    x509.ReasonFlags.key_compromise,
+                    x509.ReasonFlags.ca_compromise,
+                ]),
+                [x509.UniformResourceIdentifier(b"uri://thing")],
+            ),
+        ])
+        assert fcrl == fcrl2
+
+    def test_ne(self):
+        fcrl = x509.FreshestCRL([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(b"ftp://domain")],
+                None,
+                frozenset([
+                    x509.ReasonFlags.key_compromise,
+                    x509.ReasonFlags.ca_compromise,
+                ]),
+                [x509.UniformResourceIdentifier(b"uri://thing")],
+            ),
+        ])
+        fcrl2 = x509.FreshestCRL([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(b"ftp://domain2")],
+                None,
+                frozenset([
+                    x509.ReasonFlags.key_compromise,
+                    x509.ReasonFlags.ca_compromise,
+                ]),
+                [x509.UniformResourceIdentifier(b"uri://thing")],
+            ),
+        ])
+        fcrl3 = x509.FreshestCRL([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(b"ftp://domain")],
+                None,
+                frozenset([x509.ReasonFlags.key_compromise]),
+                [x509.UniformResourceIdentifier(b"uri://thing")],
+            ),
+        ])
+        fcrl4 = x509.FreshestCRL([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(b"ftp://domain")],
+                None,
+                frozenset([
+                    x509.ReasonFlags.key_compromise,
+                    x509.ReasonFlags.ca_compromise,
+                ]),
+                [x509.UniformResourceIdentifier(b"uri://thing2")],
+            ),
+        ])
+        assert fcrl != fcrl2
+        assert fcrl != fcrl3
+        assert fcrl != fcrl4
+        assert fcrl != object()
+
+    def test_hash(self):
+        fcrl = x509.FreshestCRL([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(b"ftp://domain")],
+                None,
+                frozenset([
+                    x509.ReasonFlags.key_compromise,
+                    x509.ReasonFlags.ca_compromise,
+                ]),
+                [x509.UniformResourceIdentifier(b"uri://thing")],
+            ),
+        ])
+        fcrl2 = x509.FreshestCRL([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(b"ftp://domain")],
+                None,
+                frozenset([
+                    x509.ReasonFlags.key_compromise,
+                    x509.ReasonFlags.ca_compromise,
+                ]),
+                [x509.UniformResourceIdentifier(b"uri://thing")],
+            ),
+        ])
+        fcrl3 = x509.FreshestCRL([
+            x509.DistributionPoint(
+                [x509.UniformResourceIdentifier(b"ftp://domain")],
+                None,
+                frozenset([x509.ReasonFlags.key_compromise]),
+                [x509.UniformResourceIdentifier(b"uri://thing")],
+            ),
+        ])
+        assert hash(fcrl) == hash(fcrl2)
+        assert hash(fcrl) != hash(fcrl3)
+
+    def test_indexing(self):
+        fcrl = x509.FreshestCRL([
+            x509.DistributionPoint(
+                None, None, None,
+                [x509.UniformResourceIdentifier(b"uri://thing")],
+            ),
+            x509.DistributionPoint(
+                None, None, None,
+                [x509.UniformResourceIdentifier(b"uri://thing2")],
+            ),
+            x509.DistributionPoint(
+                None, None, None,
+                [x509.UniformResourceIdentifier(b"uri://thing3")],
+            ),
+            x509.DistributionPoint(
+                None, None, None,
+                [x509.UniformResourceIdentifier(b"uri://thing4")],
+            ),
+            x509.DistributionPoint(
+                None, None, None,
+                [x509.UniformResourceIdentifier(b"uri://thing5")],
+            ),
+        ])
+        assert fcrl[-1] == fcrl[4]
+        assert fcrl[2:6:2] == [fcrl[2], fcrl[4]]
+
+
 class TestCRLDistributionPoints(object):
     def test_invalid_distribution_points(self):
         with pytest.raises(TypeError):
@@ -4152,6 +4339,46 @@
 
 @pytest.mark.requires_backend_interface(interface=RSABackend)
 @pytest.mark.requires_backend_interface(interface=X509Backend)
+class TestFreshestCRLExtension(object):
+    def test_vector(self, backend):
+        cert = _load_cert(
+            os.path.join(
+                "x509", "custom", "freshestcrl.pem"
+            ),
+            x509.load_pem_x509_certificate,
+            backend
+        )
+
+        fcrl = cert.extensions.get_extension_for_class(x509.FreshestCRL).value
+        assert fcrl == x509.FreshestCRL([
+            x509.DistributionPoint(
+                full_name=[
+                    x509.UniformResourceIdentifier(
+                        b'http://myhost.com/myca.crl'
+                    ),
+                    x509.UniformResourceIdentifier(
+                        b'http://backup.myhost.com/myca.crl'
+                    )
+                ],
+                relative_name=None,
+                reasons=frozenset([
+                    x509.ReasonFlags.ca_compromise,
+                    x509.ReasonFlags.key_compromise
+                ]),
+                crl_issuer=[x509.DirectoryName(
+                    x509.Name([
+                        x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"),
+                        x509.NameAttribute(
+                            NameOID.COMMON_NAME, u"cryptography CA"
+                        ),
+                    ])
+                )]
+            )
+        ])
+
+
+@pytest.mark.requires_backend_interface(interface=RSABackend)
+@pytest.mark.requires_backend_interface(interface=X509Backend)
 class TestOCSPNoCheckExtension(object):
     def test_nocheck(self, backend):
         cert = _load_cert(