summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAndrej Shadura <andrew.shadura@collabora.co.uk>2019-06-21 08:25:22 -0500
committerAndrej Shadura <andrew.shadura@collabora.co.uk>2019-06-21 08:25:22 -0500
commitf836f2af1d9ec6af5188950bfb5624fa04888586 (patch)
treeacf370edefab66dd0a2bf6dd45aa180c37948fcd /src
parentcc7e801a5bfb7e276585349cc878259d4cb71c56 (diff)
New upstream version 18.1.0
Diffstat (limited to 'src')
-rw-r--r--src/service_identity.egg-info/PKG-INFO76
-rw-r--r--src/service_identity.egg-info/SOURCES.txt12
-rw-r--r--src/service_identity.egg-info/requires.txt23
-rw-r--r--src/service_identity/__init__.py9
-rw-r--r--src/service_identity/_common.py174
-rw-r--r--src/service_identity/_compat.py1
-rw-r--r--src/service_identity/cryptography.py161
-rw-r--r--src/service_identity/exceptions.py21
-rw-r--r--src/service_identity/pyopenssl.py88
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&amp;hostname=irc.freenode.net&amp;port=6697&amp;ssl=1
:target: https://www.irccloud.com/invite?channel=%23cryptography-dev&amp;hostname=irc.freenode.net&amp;port=6697&amp;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",
-]