diff options
author | Paul Kehrer <paul.l.kehrer@gmail.com> | 2018-12-01 12:15:20 +0800 |
---|---|---|
committer | Alex Gaynor <alex.gaynor@gmail.com> | 2018-11-30 23:15:20 -0500 |
commit | eb3e2e0d73c86d876d48aa6bde9fcf01c761c98f (patch) | |
tree | 1b20470ee016a3d1a35e7b5680fe38f38c538574 | |
parent | e4e7b89fb627b372cde4158ceb7078d8769497cb (diff) | |
download | cryptography-eb3e2e0d73c86d876d48aa6bde9fcf01c761c98f.tar.gz cryptography-eb3e2e0d73c86d876d48aa6bde9fcf01c761c98f.tar.bz2 cryptography-eb3e2e0d73c86d876d48aa6bde9fcf01c761c98f.zip |
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
-rw-r--r-- | CHANGELOG.rst | 1 | ||||
-rw-r--r-- | docs/x509/reference.rst | 71 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/decode_asn1.py | 25 | ||||
-rw-r--r-- | src/cryptography/x509/__init__.py | 7 | ||||
-rw-r--r-- | src/cryptography/x509/extensions.py | 130 | ||||
-rw-r--r-- | tests/x509/test_x509_ext.py | 288 |
6 files changed, 519 insertions, 3 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0cc468c5..b75836d4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,7 @@ Changelog 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 5e814916..15891059 100644 --- a/docs/x509/reference.rst +++ b/docs/x509/reference.rst @@ -2319,6 +2319,77 @@ X.509 Extensions :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 e06e8cd6..007675d4 100644 --- a/src/cryptography/hazmat/backends/openssl/decode_asn1.py +++ b/src/cryptography/hazmat/backends/openssl/decode_asn1.py @@ -464,6 +464,30 @@ def _decode_general_subtrees(backend, stack_subtrees): 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 @@ _CRL_EXTENSION_HANDLERS = { 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 fd019455..b761e264 100644 --- a/src/cryptography/x509/__init__.py +++ b/src/cryptography/x509/__init__.py @@ -21,9 +21,9 @@ from cryptography.x509.extensions import ( 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 @@ __all__ = [ "Extension", "ExtendedKeyUsage", "FreshestCRL", + "IssuingDistributionPoint", "TLSFeature", "TLSFeatureType", "OCSPNoCheck", diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py index fc5c17a9..12071b66 100644 --- a/src/cryptography/x509/extensions.py +++ b/src/cryptography/x509/extensions.py @@ -1447,6 +1447,136 @@ class OCSPNonce(object): @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 9eac9a27..5ff3bdd6 100644 --- a/tests/x509/test_x509_ext.py +++ b/tests/x509/test_x509_ext.py @@ -4440,6 +4440,294 @@ class TestInhibitAnyPolicyExtension(object): 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): |