diff options
author | Andrej Shadura <andrew.shadura@collabora.co.uk> | 2019-06-21 08:25:22 -0500 |
---|---|---|
committer | Andrej Shadura <andrew.shadura@collabora.co.uk> | 2019-06-21 08:25:22 -0500 |
commit | f836f2af1d9ec6af5188950bfb5624fa04888586 (patch) | |
tree | acf370edefab66dd0a2bf6dd45aa180c37948fcd /src | |
parent | cc7e801a5bfb7e276585349cc878259d4cb71c56 (diff) |
New upstream version 18.1.0
Diffstat (limited to 'src')
-rw-r--r-- | src/service_identity.egg-info/PKG-INFO | 76 | ||||
-rw-r--r-- | src/service_identity.egg-info/SOURCES.txt | 12 | ||||
-rw-r--r-- | src/service_identity.egg-info/requires.txt | 23 | ||||
-rw-r--r-- | src/service_identity/__init__.py | 9 | ||||
-rw-r--r-- | src/service_identity/_common.py | 174 | ||||
-rw-r--r-- | src/service_identity/_compat.py | 1 | ||||
-rw-r--r-- | src/service_identity/cryptography.py | 161 | ||||
-rw-r--r-- | src/service_identity/exceptions.py | 21 | ||||
-rw-r--r-- | src/service_identity/pyopenssl.py | 88 |
9 files changed, 413 insertions, 152 deletions
diff --git a/src/service_identity.egg-info/PKG-INFO b/src/service_identity.egg-info/PKG-INFO index 55b0ed2..d64402e 100644 --- a/src/service_identity.egg-info/PKG-INFO +++ b/src/service_identity.egg-info/PKG-INFO @@ -1,71 +1,73 @@ -Metadata-Version: 1.1 +Metadata-Version: 2.1 Name: service-identity -Version: 16.0.0 -Summary: Service identity verification for pyOpenSSL. -Home-page: https://service-identity.readthedocs.org/ +Version: 18.1.0 +Summary: Service identity verification for pyOpenSSL & cryptography. +Home-page: https://service-identity.readthedocs.io/ Author: Hynek Schlawack Author-email: hs@ox.cx +Maintainer: Hynek Schlawack +Maintainer-email: hs@ox.cx License: MIT -Description: =========================================== - Service Identity Verification for pyOpenSSL - =========================================== +Description: ============================= + Service Identity Verification + ============================= + + .. image:: https://readthedocs.org/projects/service-identity/badge/?version=stable + :target: https://service-identity.readthedocs.io/en/stable/?badge=stable + :alt: Documentation Status .. image:: https://travis-ci.org/pyca/service_identity.svg?branch=master - :target: https://travis-ci.org/pyca/service_identity + :target: https://travis-ci.org/pyca/service_identity + :alt: CI status + + .. image:: https://codecov.io/github/pyca/service_identity/branch/master/graph/badge.svg + :target: https://codecov.io/github/pyca/service_identity + :alt: Test Coverage - .. image:: https://codecov.io/github/pyca/service_identity/coverage.svg?branch=master - :target: https://codecov.io/github/pyca/service_identity + .. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/ambv/black + :alt: Code style: black .. image:: https://www.irccloud.com/invite-svg?channel=%23cryptography-dev&hostname=irc.freenode.net&port=6697&ssl=1 :target: https://www.irccloud.com/invite?channel=%23cryptography-dev&hostname=irc.freenode.net&port=6697&ssl=1 .. begin - **TL;DR**: Use this package if you use pyOpenSSL_ and don’t want to be MITM_\ ed. + Use this package if: + + - you use pyOpenSSL_ and don’t want to be MITM_\ ed or + - if you want to verify that a `PyCA cryptography`_ certificate is valid for a certain hostname or IP address. ``service_identity`` aspires to give you all the tools you need for verifying whether a certificate is valid for the intended purposes. In the simplest case, this means *host name verification*. However, ``service_identity`` implements `RFC 6125`_ fully and plans to add other relevant RFCs too. - ``service_identity``\ ’s documentation lives at `Read the Docs <https://service-identity.readthedocs.org/>`_, the code on `GitHub <https://github.com/pyca/service_identity>`_. + ``service_identity``\ ’s documentation lives at `Read the Docs <https://service-identity.readthedocs.io/>`_, the code on `GitHub <https://github.com/pyca/service_identity>`_. .. _Twisted: https://twistedmatrix.com/ - .. _pyOpenSSL: https://pypi.python.org/pypi/pyOpenSSL/ + .. _pyOpenSSL: https://pypi.org/project/pyOpenSSL/ .. _MITM: https://en.wikipedia.org/wiki/Man-in-the-middle_attack - .. _`RFC 6125`: http://www.rfc-editor.org/info/rfc6125 + .. _RFC 6125: https://www.rfc-editor.org/info/rfc6125 + .. _PyCA cryptography: https://cryptography.io/ Release Information =================== - 16.0.0 (2016-02-18) + 18.1.0 (2018-12-05) ------------------- - Backward-incompatible changes: - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - - Python 3.3 and 2.6 aren't supported anymore. - They may work by chance but any effort to keep them working has ceased. - - The last Python 2.6 release was on October 29, 2013 and isn't supported by the CPython core team anymore. - Major Python packages like Django and Twisted dropped Python 2.6 a while ago already. - - Python 3.3 never had a significant user base and wasn't part of any distribution's LTS release. - - pyOpenSSL versions older than 0.14 are not tested anymore. - They don't even build with recent OpenSSL versions. - Changes: ^^^^^^^^ - - Officially support Python 3.5. - - ``service_identity.SubjectAltNameWarning`` is now raised if the server certicate lacks a proper ``SubjectAltName``. - [`#9 <https://github.com/pyca/service_identity/issues/9>`_] - - Add a ``__str__`` method to ``VerificationError``. - - Port from ``characteristic`` to its spiritual successor `attrs <https://attrs.readthedocs.org/>`_. + - pyOpenSSL is optional now if you use ``service_identity.cryptography.*`` only. + - Added support for ``iPAddress`` ``subjectAltName``\ s. + You can now verify whether a connection or a certificate is valid for an IP address using ``service_identity.pyopenssl.verify_ip_address()`` and ``service_identity.cryptography.verify_certificate_ip_address()``. + `#12 <https://github.com/pyca/service_identity/pull/12>`_ - `Full changelog <https://service-identity.readthedocs.org/en/stable/changelog.html>`_. + `Full changelog <https://service-identity.readthedocs.io/en/stable/changelog.html>`_. Authors ======= @@ -92,8 +94,14 @@ Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python Classifier: Topic :: Security :: Cryptography Classifier: Topic :: Software Development :: Libraries :: Python Modules +Provides-Extra: tests +Provides-Extra: dev +Provides-Extra: docs +Provides-Extra: idna diff --git a/src/service_identity.egg-info/SOURCES.txt b/src/service_identity.egg-info/SOURCES.txt index 3acc7e1..9ac2915 100644 --- a/src/service_identity.egg-info/SOURCES.txt +++ b/src/service_identity.egg-info/SOURCES.txt @@ -1,13 +1,12 @@ .coveragerc +.pre-commit-config.yaml .travis.yml AUTHORS.rst CHANGELOG.rst -CODE_OF_CONDUCT.rst -CONTRIBUTING.rst LICENSE MANIFEST.in README.rst -docs-requirements.txt +pyproject.toml setup.cfg setup.py tox.ini @@ -24,6 +23,7 @@ docs/license.rst src/service_identity/__init__.py src/service_identity/_common.py src/service_identity/_compat.py +src/service_identity/cryptography.py src/service_identity/exceptions.py src/service_identity/pyopenssl.py src/service_identity.egg-info/PKG-INFO @@ -31,8 +31,4 @@ src/service_identity.egg-info/SOURCES.txt src/service_identity.egg-info/dependency_links.txt src/service_identity.egg-info/not-zip-safe src/service_identity.egg-info/requires.txt -src/service_identity.egg-info/top_level.txt -tests/__init__.py -tests/test_common.py -tests/test_pyopenssl.py -tests/util.py
\ No newline at end of file +src/service_identity.egg-info/top_level.txt
\ No newline at end of file diff --git a/src/service_identity.egg-info/requires.txt b/src/service_identity.egg-info/requires.txt index 5b96c3a..147e397 100644 --- a/src/service_identity.egg-info/requires.txt +++ b/src/service_identity.egg-info/requires.txt @@ -1,7 +1,24 @@ -attrs -pyasn1 +attrs>=16.0.0 pyasn1-modules -pyopenssl>=0.12 +pyasn1 +cryptography + +[:python_version < "3.3"] +ipaddress + +[dev] +coverage>=4.2.0 +pytest +sphinx +idna +pyOpenSSL + +[docs] +sphinx [idna] idna + +[tests] +coverage>=4.2.0 +pytest diff --git a/src/service_identity/__init__.py b/src/service_identity/__init__.py index c6c7eb1..f79673e 100644 --- a/src/service_identity/__init__.py +++ b/src/service_identity/__init__.py @@ -4,7 +4,7 @@ Verify service identities. from __future__ import absolute_import, division, print_function -from . import pyopenssl +from . import cryptography, pyopenssl from .exceptions import ( CertificateError, SubjectAltNameWarning, @@ -12,11 +12,11 @@ from .exceptions import ( ) -__version__ = "16.0.0" +__version__ = "18.1.0" __title__ = "service_identity" -__description__ = "Service identity verification for pyOpenSSL." -__uri__ = "https://service-identity.readthedocs.org/" +__description__ = "Service identity verification for pyOpenSSL & cryptography." +__uri__ = "https://service-identity.readthedocs.io/" __author__ = "Hynek Schlawack" __email__ = "hs@ox.cx" @@ -29,5 +29,6 @@ __all__ = [ "CertificateError", "SubjectAltNameWarning", "VerificationError", + "cryptography", "pyopenssl", ] diff --git a/src/service_identity/_common.py b/src/service_identity/_common.py index fa8a359..9b4e773 100644 --- a/src/service_identity/_common.py +++ b/src/service_identity/_common.py @@ -4,6 +4,7 @@ Common verification code. from __future__ import absolute_import, division, print_function +import ipaddress import re import attr @@ -12,22 +13,25 @@ from ._compat import maketrans, text_type from .exceptions import ( CertificateError, DNSMismatch, + IPAddressMismatch, SRVMismatch, URIMismatch, VerificationError, ) + try: import idna except ImportError: # pragma: nocover idna = None -@attr.s +@attr.s(slots=True) class ServiceMatch(object): """ A match of a service id and a certificate pattern. """ + service_id = attr.ib() cert_pattern = attr.ib() @@ -41,8 +45,9 @@ def verify_service_identity(cert_patterns, obligatory_ids, optional_ids): if a pattern of the respective type is present. """ errors = [] - matches = (_find_matches(cert_patterns, obligatory_ids) + - _find_matches(cert_patterns, optional_ids)) + matches = _find_matches(cert_patterns, obligatory_ids) + _find_matches( + cert_patterns, optional_ids + ) matched_ids = [match.service_id for match in matches] for i in obligatory_ids: @@ -54,9 +59,8 @@ def verify_service_identity(cert_patterns, obligatory_ids, optional_ids): # is a pattern of the same type , it is an error and the verification # fails. Example: the user passes a SRV-ID for "_mail.domain.com" but # the certificate contains an SRV-Pattern for "_xmpp.domain.com". - if ( - i not in matched_ids and - _contains_instance_of(cert_patterns, i.pattern_class) + if i not in matched_ids and _contains_instance_of( + cert_patterns, i.pattern_class ): errors.append(i.error_on_mismatch(mismatched_id=i)) @@ -82,9 +86,7 @@ def _find_matches(cert_patterns, service_ids): for sid in service_ids: for cid in cert_patterns: if sid.verify(cid): - matches.append( - ServiceMatch(cert_pattern=cid, service_id=sid) - ) + matches.append(ServiceMatch(cert_pattern=cid, service_id=sid)) return matches @@ -101,43 +103,42 @@ def _contains_instance_of(seq, cl): return False -_RE_IPv4 = re.compile(br"^([0-9*]{1,3}\.){3}[0-9*]{1,3}$") -_RE_IPv6 = re.compile(br"^([a-f0-9*]{0,4}:)+[a-f0-9*]{1,4}$") -_RE_NUMBER = re.compile(br"^[0-9]+$") - - def _is_ip_address(pattern): """ Check whether *pattern* could be/match an IP address. - Does *not* guarantee that pattern is in fact a valid IP address; especially - the checks for IPv6 are rather coarse. This function is for security - checks, not for validating IP addresses. - :param pattern: A pattern for a host name. :type pattern: `bytes` or `unicode` :return: `True` if *pattern* could be an IP address, else `False`. - :rtype: `bool` + :rtype: bool """ - if isinstance(pattern, text_type): + if isinstance(pattern, bytes): try: - pattern = pattern.encode('ascii') + pattern = pattern.decode("ascii") except UnicodeError: return False - return ( - _RE_IPv4.match(pattern) is not None or - _RE_IPv6.match(pattern) is not None or - _RE_NUMBER.match(pattern) is not None - ) + try: + int(pattern) + return True + except ValueError: + pass + + try: + ipaddress.ip_address(pattern.replace("*", "1")) + except ValueError: + return False + + return True -@attr.s(init=False) +@attr.s(init=False, slots=True) class DNSPattern(object): """ A DNS pattern as extracted from certificates. """ + pattern = attr.ib() _RE_LEGAL_CHARS = re.compile(br"^[a-z0-9\-_.]+$") @@ -157,15 +158,34 @@ class DNSPattern(object): ) self.pattern = pattern.translate(_TRANS_TO_LOWER) - if b'*' in self.pattern: + if b"*" in self.pattern: _validate_pattern(self.pattern) -@attr.s(init=False) +@attr.s(slots=True) +class IPAddressPattern(object): + """ + An IP address pattern as extracted from certificates. + """ + + pattern = attr.ib() + + @classmethod + def from_bytes(cls, bs): + try: + return cls(pattern=ipaddress.ip_address(bs)) + except ValueError: + raise CertificateError( + "Invalid IP address pattern {!r}.".format(bs) + ) + + +@attr.s(init=False, slots=True) class URIPattern(object): """ An URI pattern as extracted from certificates. """ + protocol_pattern = attr.ib() dns_pattern = attr.ib() @@ -178,11 +198,7 @@ class URIPattern(object): pattern = pattern.strip().translate(_TRANS_TO_LOWER) - if ( - b":" not in pattern or - b"*" in pattern or - _is_ip_address(pattern) - ): + if b":" not in pattern or b"*" in pattern or _is_ip_address(pattern): raise CertificateError( "Invalid URI pattern {0!r}.".format(pattern) ) @@ -190,11 +206,12 @@ class URIPattern(object): self.dns_pattern = DNSPattern(hostname) -@attr.s(init=False) +@attr.s(init=False, slots=True) class SRVPattern(object): """ An SRV pattern as extracted from certificates. """ + name_pattern = attr.ib() dns_pattern = attr.ib() @@ -208,10 +225,10 @@ class SRVPattern(object): pattern = pattern.strip().translate(_TRANS_TO_LOWER) if ( - pattern[0] != b"_"[0] or - b"." not in pattern or - b"*" in pattern or - _is_ip_address(pattern) + pattern[0] != b"_"[0] + or b"." not in pattern + or b"*" in pattern + or _is_ip_address(pattern) ): raise CertificateError( "Invalid SRV pattern {0!r}.".format(pattern) @@ -221,11 +238,12 @@ class SRVPattern(object): self.dns_pattern = DNSPattern(hostname) -@attr.s(init=False) +@attr.s(init=False, slots=True) class DNS_ID(object): """ A DNS service ID, aka hostname. """ + hostname = attr.ib() # characters that are legal in a normalized hostname @@ -260,7 +278,7 @@ class DNS_ID(object): def verify(self, pattern): """ - http://tools.ietf.org/search/rfc6125#section-6.4 + https://tools.ietf.org/search/rfc6125#section-6.4 """ if isinstance(pattern, self.pattern_class): return _hostname_matches(pattern.pattern, self.hostname) @@ -268,11 +286,30 @@ class DNS_ID(object): return False -@attr.s(init=False) +@attr.s(slots=True) +class IPAddress_ID(object): + """ + An IP address service ID. + """ + + ip = attr.ib(converter=ipaddress.ip_address) + + pattern_class = IPAddressPattern + error_on_mismatch = IPAddressMismatch + + def verify(self, pattern): + """ + https://tools.ietf.org/search/rfc2818#section-3.1 + """ + return self.ip == pattern.pattern + + +@attr.s(init=False, slots=True) class URI_ID(object): """ An URI service ID. """ + protocol = attr.ib() dns_id = attr.ib() @@ -297,22 +334,23 @@ class URI_ID(object): def verify(self, pattern): """ - http://tools.ietf.org/search/rfc6125#section-6.5.2 + https://tools.ietf.org/search/rfc6125#section-6.5.2 """ if isinstance(pattern, self.pattern_class): return ( - pattern.protocol_pattern == self.protocol and - self.dns_id.verify(pattern.dns_pattern) + pattern.protocol_pattern == self.protocol + and self.dns_id.verify(pattern.dns_pattern) ) else: return False -@attr.s(init=False) +@attr.s(init=False, slots=True) class SRV_ID(object): """ An SRV service ID. """ + name = attr.ib() dns_id = attr.ib() @@ -337,12 +375,11 @@ class SRV_ID(object): def verify(self, pattern): """ - http://tools.ietf.org/search/rfc6125#section-6.5.1 + https://tools.ietf.org/search/rfc6125#section-6.5.1 """ if isinstance(pattern, self.pattern_class): - return ( - self.name == pattern.name_pattern and - self.dns_id.verify(pattern.dns_pattern) + return self.name == pattern.name_pattern and self.dns_id.verify( + pattern.dns_pattern ) else: return False @@ -356,7 +393,7 @@ def _hostname_matches(cert_pattern, actual_hostname): :return: `True` if *cert_pattern* matches *actual_hostname*, else `False`. :rtype: `bool` """ - if b'*' in cert_pattern: + if b"*" in cert_pattern: cert_head, cert_tail = cert_pattern.split(b".", 1) actual_head, actual_tail = actual_hostname.split(b".", 1) if cert_tail != actual_tail: @@ -365,20 +402,7 @@ def _hostname_matches(cert_pattern, actual_hostname): if actual_head.startswith(b"xn--"): return False - if cert_head == b"*": - return True - - start, end = cert_head.split(b"*") - if start == b"": - # *oo - return actual_head.endswith(end) - elif end == b"": - # f* - return actual_head.startswith(start) - else: - # f*o - return actual_head.startswith(start) and actual_head.endswith(end) - + return cert_head == b"*" or cert_head == actual_head else: return cert_pattern == actual_hostname @@ -395,15 +419,15 @@ def _validate_pattern(cert_pattern): cnt = cert_pattern.count(b"*") if cnt > 1: raise CertificateError( - "Certificate's DNS-ID {0!r} contains too many wildcards." - .format(cert_pattern) + "Certificate's DNS-ID {0!r} contains too many wildcards.".format( + cert_pattern + ) ) parts = cert_pattern.split(b".") if len(parts) < 3: raise CertificateError( - "Certificate's DNS-ID {0!r} hast too few host components for " - "wildcard usage." - .format(cert_pattern) + "Certificate's DNS-ID {0!r} has too few host components for " + "wildcard usage.".format(cert_pattern) ) # We assume there will always be only one wildcard allowed. if b"*" not in parts[0]: @@ -413,11 +437,13 @@ def _validate_pattern(cert_pattern): ) if any(not len(p) for p in parts): raise CertificateError( - "Certificate's DNS-ID {0!r} contains empty parts." - .format(cert_pattern) + "Certificate's DNS-ID {0!r} contains empty parts.".format( + cert_pattern + ) ) # Ensure no locale magic interferes. -_TRANS_TO_LOWER = maketrans(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ", - b"abcdefghijklmnopqrstuvwxyz") +_TRANS_TO_LOWER = maketrans( + b"ABCDEFGHIJKLMNOPQRSTUVWXYZ", b"abcdefghijklmnopqrstuvwxyz" +) diff --git a/src/service_identity/_compat.py b/src/service_identity/_compat.py index 65febe8..317b675 100644 --- a/src/service_identity/_compat.py +++ b/src/service_identity/_compat.py @@ -11,5 +11,6 @@ if PY3: # pragma: nocover text_type = str else: # pragma: nocover import string + maketrans = string.maketrans text_type = unicode # noqa diff --git a/src/service_identity/cryptography.py b/src/service_identity/cryptography.py new file mode 100644 index 0000000..9e174a0 --- /dev/null +++ b/src/service_identity/cryptography.py @@ -0,0 +1,161 @@ +""" +`cryptography.x509 <https://github.com/pyca/cryptography>`_-specific code. +""" + +from __future__ import absolute_import, division, print_function + +import warnings + +from cryptography.x509 import ( + DNSName, + ExtensionOID, + IPAddress, + NameOID, + ObjectIdentifier, + OtherName, + UniformResourceIdentifier, +) +from cryptography.x509.extensions import ExtensionNotFound +from pyasn1.codec.der.decoder import decode +from pyasn1.type.char import IA5String + +from ._common import ( + DNS_ID, + CertificateError, + DNSPattern, + IPAddress_ID, + IPAddressPattern, + SRVPattern, + URIPattern, + verify_service_identity, +) +from .exceptions import SubjectAltNameWarning + + +__all__ = ["verify_certificate_hostname"] + + +def verify_certificate_hostname(certificate, hostname): + """ + Verify whether *certificate* is valid for *hostname*. + + .. note:: Nothing is verified about the *authority* of the certificate; + the caller must verify that the certificate chains to an appropriate + trust root themselves. + + :param cryptography.x509.Certificate certificate: A cryptography X509 + certificate object. + :param unicode hostname: The hostname that *certificate* should be valid + for. + + :raises service_identity.VerificationError: If *certificate* is not valid + for *hostname*. + :raises service_identity.CertificateError: If *certificate* contains + invalid/unexpected data. + + :returns: ``None`` + """ + verify_service_identity( + cert_patterns=extract_ids(certificate), + obligatory_ids=[DNS_ID(hostname)], + optional_ids=[], + ) + + +def verify_certificate_ip_address(certificate, ip_address): + """ + Verify whether *certificate* is valid for *ip_address*. + + .. note:: Nothing is verified about the *authority* of the certificate; + the caller must verify that the certificate chains to an appropriate + trust root themselves. + + :param cryptography.x509.Certificate certificate: A cryptography X509 + certificate object. + :param unicode ip_address: The IP address that *connection* should be valid + for. Can be an IPv4 or IPv6 address. + + :raises service_identity.VerificationError: If *certificate* is not valid + for *ip_address*. + :raises service_identity.CertificateError: If *certificate* contains + invalid/unexpected data. + + :returns: ``None`` + + .. versionadded:: 18.1.0 + """ + verify_service_identity( + cert_patterns=extract_ids(certificate), + obligatory_ids=[IPAddress_ID(ip_address)], + optional_ids=[], + ) + + +ID_ON_DNS_SRV = ObjectIdentifier("1.3.6.1.5.5.7.8.7") # id_on_dnsSRV + + +def extract_ids(cert): + """ + Extract all valid IDs from a certificate for service verification. + + If *cert* doesn't contain any identifiers, the ``CN``s are used as DNS-IDs + as fallback. + + :param cryptography.x509.Certificate cert: The certificate to be dissected. + + :return: List of IDs. + """ + ids = [] + try: + ext = cert.extensions.get_extension_for_oid( + ExtensionOID.SUBJECT_ALTERNATIVE_NAME + ) + except ExtensionNotFound: + pass + else: + ids.extend( + [ + DNSPattern(name.encode("utf-8")) + for name in ext.value.get_values_for_type(DNSName) + ] + ) + ids.extend( + [ + URIPattern(uri.encode("utf-8")) + for uri in ext.value.get_values_for_type( + UniformResourceIdentifier + ) + ] + ) + ids.extend( + [ + IPAddressPattern(ip) + for ip in ext.value.get_values_for_type(IPAddress) + ] + ) + for other in ext.value.get_values_for_type(OtherName): + if other.type_id == ID_ON_DNS_SRV: + srv, _ = decode(other.value) + if isinstance(srv, IA5String): + ids.append(SRVPattern(srv.asOctets())) + else: # pragma: nocover + raise CertificateError("Unexpected certificate content.") + + if not ids: + # https://tools.ietf.org/search/rfc6125#section-6.4.4 + # A client MUST NOT seek a match for a reference identifier of CN-ID if + # the presented identifiers include a DNS-ID, SRV-ID, URI-ID, or any + # application-specific identifier types supported by the client. + cns = [ + n.value + for n in cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME) + ] + cn = next(iter(cns), b"<not given>") + ids = [DNSPattern(n.encode("utf-8")) for n in cns] + warnings.warn( + "Certificate with CN {!r} has no `subjectAltName`, falling back " + "to check for a `commonName` for now. This feature is being " + "removed by major browsers and deprecated by RFC 2818.".format(cn), + SubjectAltNameWarning, + ) + return ids diff --git a/src/service_identity/exceptions.py b/src/service_identity/exceptions.py index 1dfc108..852abc1 100644 --- a/src/service_identity/exceptions.py +++ b/src/service_identity/exceptions.py @@ -10,7 +10,7 @@ from __future__ import absolute_import, division, print_function import attr -class SubjectAltNameWarning(Warning): +class SubjectAltNameWarning(DeprecationWarning): """ Server Certificate does not contain a ``SubjectAltName``. @@ -23,6 +23,7 @@ class VerificationError(Exception): """ Service identity verification failed. """ + errors = attr.ib() def __str__(self): @@ -32,24 +33,36 @@ class VerificationError(Exception): @attr.s class DNSMismatch(object): """ - Not matching DNSPattern could be found. + No matching DNSPattern could be found. """ + mismatched_id = attr.ib() @attr.s class SRVMismatch(object): """ - Not matching SRVPattern could be found. + No matching SRVPattern could be found. """ + mismatched_id = attr.ib() @attr.s class URIMismatch(object): """ - Not matching URIPattern could be found. + No matching URIPattern could be found. """ + + mismatched_id = attr.ib() + + +@attr.s +class IPAddressMismatch(object): + """ + No matching IPAddressPattern could be found. + """ + mismatched_id = attr.ib() diff --git a/src/service_identity/pyopenssl.py b/src/service_identity/pyopenssl.py index fb39954..7fc090d 100644 --- a/src/service_identity/pyopenssl.py +++ b/src/service_identity/pyopenssl.py @@ -6,31 +6,36 @@ from __future__ import absolute_import, division, print_function import warnings +import six + from pyasn1.codec.der.decoder import decode from pyasn1.type.char import IA5String from pyasn1.type.univ import ObjectIdentifier from pyasn1_modules.rfc2459 import GeneralNames -from .exceptions import SubjectAltNameWarning from ._common import ( + DNS_ID, CertificateError, DNSPattern, - DNS_ID, + IPAddress_ID, + IPAddressPattern, SRVPattern, URIPattern, verify_service_identity, ) +from .exceptions import SubjectAltNameWarning + + +__all__ = ["verify_hostname"] def verify_hostname(connection, hostname): """ Verify whether the certificate of *connection* is valid for *hostname*. - :param connection: A pyOpenSSL connection object. - :type connection: :class:`OpenSSL.SSL.Connection` - - :param hostname: The hostname that *connection* should be connected to. - :type hostname: :class:`unicode` + :param OpenSSL.SSL.Connection connection: A pyOpenSSL connection object. + :param unicode hostname: The hostname that *connection* should be connected + to. :raises service_identity.VerificationError: If *connection* does not provide a certificate that is valid for *hostname*. @@ -47,7 +52,32 @@ def verify_hostname(connection, hostname): ) -ID_ON_DNS_SRV = ObjectIdentifier('1.3.6.1.5.5.7.8.7') # id_on_dnsSRV +def verify_ip_address(connection, ip_address): + """ + Verify whether the certificate of *connection* is valid for *ip_address*. + + :param OpenSSL.SSL.Connection connection: A pyOpenSSL connection object. + :param unicode ip_address: The IP address that *connection* should be + connected to. Can be an IPv4 or IPv6 address. + + :raises service_identity.VerificationError: If *connection* does not + provide a certificate that is valid for *ip_address*. + :raises service_identity.CertificateError: If the certificate chain of + *connection* contains a certificate that contains invalid/unexpected + data. + + :returns: ``None`` + + .. versionadded:: 18.1.0 + """ + verify_service_identity( + cert_patterns=extract_ids(connection.get_peer_certificate()), + obligatory_ids=[IPAddress_ID(ip_address)], + optional_ids=[], + ) + + +ID_ON_DNS_SRV = ObjectIdentifier("1.3.6.1.5.5.7.8.7") # id_on_dnsSRV def extract_ids(cert): @@ -57,13 +87,12 @@ def extract_ids(cert): If *cert* doesn't contain any identifiers, the ``CN``s are used as DNS-IDs as fallback. - :param cert: The certificate to be dissected. - :type cert: :class:`OpenSSL.SSL.X509` + :param OpenSSL.SSL.X509 cert: The certificate to be dissected. :return: List of IDs. """ ids = [] - for i in range(cert.get_extension_count()): + for i in six.moves.range(cert.get_extension_count()): ext = cert.get_extension(i) if ext.get_short_name() == b"subjectAltName": names, _ = decode(ext.get_data(), asn1Spec=GeneralNames()) @@ -71,6 +100,12 @@ def extract_ids(cert): name_string = n.getName() if name_string == "dNSName": ids.append(DNSPattern(n.getComponent().asOctets())) + elif name_string == "iPAddress": + ids.append( + IPAddressPattern.from_bytes( + n.getComponent().asOctets() + ) + ) elif name_string == "uniformResourceIdentifier": ids.append(URIPattern(n.getComponent().asOctets())) elif name_string == "otherName": @@ -84,25 +119,28 @@ def extract_ids(cert): raise CertificateError( "Unexpected certificate content." ) + else: # pragma: nocover + pass + else: # pragma: nocover + pass if not ids: - # http://tools.ietf.org/search/rfc6125#section-6.4.4 + # https://tools.ietf.org/search/rfc6125#section-6.4.4 # A client MUST NOT seek a match for a reference identifier of CN-ID if # the presented identifiers include a DNS-ID, SRV-ID, URI-ID, or any # application-specific identifier types supported by the client. + components = [ + c[1] for c in cert.get_subject().get_components() if c[0] == b"CN" + ] + cn = next(iter(components), b"<not given>") + ids = [DNSPattern(c) for c in components] warnings.warn( - "Certificate has no `subjectAltName`, falling back to check for a " - "`commonName` for now. This feature is being removed by major " - "browsers and deprecated by RFC 2818.", - SubjectAltNameWarning + "Certificate with CN '%s' has no `subjectAltName`, falling back " + "to check for a `commonName` for now. This feature is being " + "removed by major browsers and deprecated by RFC 2818. " + "service_identity will remove the support for it in mid-2018." + % (cn.decode("utf-8"),), + SubjectAltNameWarning, + stacklevel=2, ) - ids = [DNSPattern(c[1]) - for c - in cert.get_subject().get_components() - if c[0] == b"CN"] return ids - - -__all__ = [ - "verify_hostname", -] |