Ausgabe der neuen DB Einträge
This commit is contained in:
parent
bad48e1627
commit
cfbbb9ee3d
2399 changed files with 843193 additions and 43 deletions
|
|
@ -0,0 +1,34 @@
|
|||
"""
|
||||
Verify service identities.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
from . import cryptography, pyopenssl
|
||||
from .exceptions import (
|
||||
CertificateError,
|
||||
SubjectAltNameWarning,
|
||||
VerificationError,
|
||||
)
|
||||
|
||||
|
||||
__version__ = "18.1.0"
|
||||
|
||||
__title__ = "service_identity"
|
||||
__description__ = "Service identity verification for pyOpenSSL & cryptography."
|
||||
__uri__ = "https://service-identity.readthedocs.io/"
|
||||
|
||||
__author__ = "Hynek Schlawack"
|
||||
__email__ = "hs@ox.cx"
|
||||
|
||||
__license__ = "MIT"
|
||||
__copyright__ = "Copyright (c) 2014 Hynek Schlawack"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CertificateError",
|
||||
"SubjectAltNameWarning",
|
||||
"VerificationError",
|
||||
"cryptography",
|
||||
"pyopenssl",
|
||||
]
|
||||
449
venv/lib/python3.9/site-packages/service_identity/_common.py
Normal file
449
venv/lib/python3.9/site-packages/service_identity/_common.py
Normal file
|
|
@ -0,0 +1,449 @@
|
|||
"""
|
||||
Common verification code.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import ipaddress
|
||||
import re
|
||||
|
||||
import attr
|
||||
|
||||
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(slots=True)
|
||||
class ServiceMatch(object):
|
||||
"""
|
||||
A match of a service id and a certificate pattern.
|
||||
"""
|
||||
|
||||
service_id = attr.ib()
|
||||
cert_pattern = attr.ib()
|
||||
|
||||
|
||||
def verify_service_identity(cert_patterns, obligatory_ids, optional_ids):
|
||||
"""
|
||||
Verify whether *cert_patterns* are valid for *obligatory_ids* and
|
||||
*optional_ids*.
|
||||
|
||||
*obligatory_ids* must be both present and match. *optional_ids* must match
|
||||
if a pattern of the respective type is present.
|
||||
"""
|
||||
errors = []
|
||||
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:
|
||||
if i not in matched_ids:
|
||||
errors.append(i.error_on_mismatch(mismatched_id=i))
|
||||
|
||||
for i in optional_ids:
|
||||
# If an optional ID is not matched by a certificate pattern *but* there
|
||||
# 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
|
||||
):
|
||||
errors.append(i.error_on_mismatch(mismatched_id=i))
|
||||
|
||||
if errors:
|
||||
raise VerificationError(errors=errors)
|
||||
|
||||
return matches
|
||||
|
||||
|
||||
def _find_matches(cert_patterns, service_ids):
|
||||
"""
|
||||
Search for matching certificate patterns and service_ids.
|
||||
|
||||
:param cert_ids: List certificate IDs like DNSPattern.
|
||||
:type cert_ids: `list`
|
||||
|
||||
:param service_ids: List of service IDs like DNS_ID.
|
||||
:type service_ids: `list`
|
||||
|
||||
:rtype: `list` of `ServiceMatch`
|
||||
"""
|
||||
matches = []
|
||||
for sid in service_ids:
|
||||
for cid in cert_patterns:
|
||||
if sid.verify(cid):
|
||||
matches.append(ServiceMatch(cert_pattern=cid, service_id=sid))
|
||||
return matches
|
||||
|
||||
|
||||
def _contains_instance_of(seq, cl):
|
||||
"""
|
||||
:type seq: iterable
|
||||
:type cl: type
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
for e in seq:
|
||||
if isinstance(e, cl):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_ip_address(pattern):
|
||||
"""
|
||||
Check whether *pattern* could be/match an IP address.
|
||||
|
||||
: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
|
||||
"""
|
||||
if isinstance(pattern, bytes):
|
||||
try:
|
||||
pattern = pattern.decode("ascii")
|
||||
except UnicodeError:
|
||||
return False
|
||||
|
||||
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, slots=True)
|
||||
class DNSPattern(object):
|
||||
"""
|
||||
A DNS pattern as extracted from certificates.
|
||||
"""
|
||||
|
||||
pattern = attr.ib()
|
||||
|
||||
_RE_LEGAL_CHARS = re.compile(br"^[a-z0-9\-_.]+$")
|
||||
|
||||
def __init__(self, pattern):
|
||||
"""
|
||||
:type pattern: `bytes`
|
||||
"""
|
||||
if not isinstance(pattern, bytes):
|
||||
raise TypeError("The DNS pattern must be a bytes string.")
|
||||
|
||||
pattern = pattern.strip()
|
||||
|
||||
if pattern == b"" or _is_ip_address(pattern) or b"\0" in pattern:
|
||||
raise CertificateError(
|
||||
"Invalid DNS pattern {0!r}.".format(pattern)
|
||||
)
|
||||
|
||||
self.pattern = pattern.translate(_TRANS_TO_LOWER)
|
||||
if b"*" in self.pattern:
|
||||
_validate_pattern(self.pattern)
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
def __init__(self, pattern):
|
||||
"""
|
||||
:type pattern: `bytes`
|
||||
"""
|
||||
if not isinstance(pattern, bytes):
|
||||
raise TypeError("The URI pattern must be a bytes string.")
|
||||
|
||||
pattern = pattern.strip().translate(_TRANS_TO_LOWER)
|
||||
|
||||
if b":" not in pattern or b"*" in pattern or _is_ip_address(pattern):
|
||||
raise CertificateError(
|
||||
"Invalid URI pattern {0!r}.".format(pattern)
|
||||
)
|
||||
self.protocol_pattern, hostname = pattern.split(b":")
|
||||
self.dns_pattern = DNSPattern(hostname)
|
||||
|
||||
|
||||
@attr.s(init=False, slots=True)
|
||||
class SRVPattern(object):
|
||||
"""
|
||||
An SRV pattern as extracted from certificates.
|
||||
"""
|
||||
|
||||
name_pattern = attr.ib()
|
||||
dns_pattern = attr.ib()
|
||||
|
||||
def __init__(self, pattern):
|
||||
"""
|
||||
:type pattern: `bytes`
|
||||
"""
|
||||
if not isinstance(pattern, bytes):
|
||||
raise TypeError("The SRV pattern must be a bytes string.")
|
||||
|
||||
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)
|
||||
):
|
||||
raise CertificateError(
|
||||
"Invalid SRV pattern {0!r}.".format(pattern)
|
||||
)
|
||||
name, hostname = pattern.split(b".", 1)
|
||||
self.name_pattern = name[1:]
|
||||
self.dns_pattern = DNSPattern(hostname)
|
||||
|
||||
|
||||
@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
|
||||
_RE_LEGAL_CHARS = re.compile(br"^[a-z0-9\-_.]+$")
|
||||
pattern_class = DNSPattern
|
||||
error_on_mismatch = DNSMismatch
|
||||
|
||||
def __init__(self, hostname):
|
||||
"""
|
||||
:type hostname: `unicode`
|
||||
"""
|
||||
if not isinstance(hostname, text_type):
|
||||
raise TypeError("DNS-ID must be a unicode string.")
|
||||
|
||||
hostname = hostname.strip()
|
||||
if hostname == u"" or _is_ip_address(hostname):
|
||||
raise ValueError("Invalid DNS-ID.")
|
||||
|
||||
if any(ord(c) > 127 for c in hostname):
|
||||
if idna:
|
||||
ascii_id = idna.encode(hostname)
|
||||
else:
|
||||
raise ImportError(
|
||||
"idna library is required for non-ASCII IDs."
|
||||
)
|
||||
else:
|
||||
ascii_id = hostname.encode("ascii")
|
||||
|
||||
self.hostname = ascii_id.translate(_TRANS_TO_LOWER)
|
||||
if self._RE_LEGAL_CHARS.match(self.hostname) is None:
|
||||
raise ValueError("Invalid DNS-ID.")
|
||||
|
||||
def verify(self, pattern):
|
||||
"""
|
||||
https://tools.ietf.org/search/rfc6125#section-6.4
|
||||
"""
|
||||
if isinstance(pattern, self.pattern_class):
|
||||
return _hostname_matches(pattern.pattern, self.hostname)
|
||||
else:
|
||||
return 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()
|
||||
|
||||
pattern_class = URIPattern
|
||||
error_on_mismatch = URIMismatch
|
||||
|
||||
def __init__(self, uri):
|
||||
"""
|
||||
:type uri: `unicode`
|
||||
"""
|
||||
if not isinstance(uri, text_type):
|
||||
raise TypeError("URI-ID must be a unicode string.")
|
||||
|
||||
uri = uri.strip()
|
||||
if u":" not in uri or _is_ip_address(uri):
|
||||
raise ValueError("Invalid URI-ID.")
|
||||
|
||||
prot, hostname = uri.split(u":")
|
||||
|
||||
self.protocol = prot.encode("ascii").translate(_TRANS_TO_LOWER)
|
||||
self.dns_id = DNS_ID(hostname.strip(u"/"))
|
||||
|
||||
def verify(self, pattern):
|
||||
"""
|
||||
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)
|
||||
)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
@attr.s(init=False, slots=True)
|
||||
class SRV_ID(object):
|
||||
"""
|
||||
An SRV service ID.
|
||||
"""
|
||||
|
||||
name = attr.ib()
|
||||
dns_id = attr.ib()
|
||||
|
||||
pattern_class = SRVPattern
|
||||
error_on_mismatch = SRVMismatch
|
||||
|
||||
def __init__(self, srv):
|
||||
"""
|
||||
:type srv: `unicode`
|
||||
"""
|
||||
if not isinstance(srv, text_type):
|
||||
raise TypeError("SRV-ID must be a unicode string.")
|
||||
|
||||
srv = srv.strip()
|
||||
if u"." not in srv or _is_ip_address(srv) or srv[0] != u"_":
|
||||
raise ValueError("Invalid SRV-ID.")
|
||||
|
||||
name, hostname = srv.split(u".", 1)
|
||||
|
||||
self.name = name[1:].encode("ascii").translate(_TRANS_TO_LOWER)
|
||||
self.dns_id = DNS_ID(hostname)
|
||||
|
||||
def verify(self, pattern):
|
||||
"""
|
||||
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
|
||||
)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _hostname_matches(cert_pattern, actual_hostname):
|
||||
"""
|
||||
:type cert_pattern: `bytes`
|
||||
:type actual_hostname: `bytes`
|
||||
|
||||
:return: `True` if *cert_pattern* matches *actual_hostname*, else `False`.
|
||||
:rtype: `bool`
|
||||
"""
|
||||
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:
|
||||
return False
|
||||
# No patterns for IDNA
|
||||
if actual_head.startswith(b"xn--"):
|
||||
return False
|
||||
|
||||
return cert_head == b"*" or cert_head == actual_head
|
||||
else:
|
||||
return cert_pattern == actual_hostname
|
||||
|
||||
|
||||
def _validate_pattern(cert_pattern):
|
||||
"""
|
||||
Check whether the usage of wildcards within *cert_pattern* conforms with
|
||||
our expectations.
|
||||
|
||||
:type hostname: `bytes`
|
||||
|
||||
:return: None
|
||||
"""
|
||||
cnt = cert_pattern.count(b"*")
|
||||
if cnt > 1:
|
||||
raise CertificateError(
|
||||
"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} 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]:
|
||||
raise CertificateError(
|
||||
"Certificate's DNS-ID {0!r} has a wildcard outside the left-most "
|
||||
"part.".format(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
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Ensure no locale magic interferes.
|
||||
_TRANS_TO_LOWER = maketrans(
|
||||
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ", b"abcdefghijklmnopqrstuvwxyz"
|
||||
)
|
||||
16
venv/lib/python3.9/site-packages/service_identity/_compat.py
Normal file
16
venv/lib/python3.9/site-packages/service_identity/_compat.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
Avoid depending on any particular Python 3 compatibility approach.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
PY3 = sys.version_info[0] == 3
|
||||
if PY3: # pragma: nocover
|
||||
maketrans = bytes.maketrans
|
||||
text_type = str
|
||||
else: # pragma: nocover
|
||||
import string
|
||||
|
||||
maketrans = string.maketrans
|
||||
text_type = unicode # noqa
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
"""
|
||||
All exceptions and warnings thrown by ``service_identity``.
|
||||
|
||||
Separated into an own package for nicer tracebacks, you should still import
|
||||
them from __init__.py.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import attr
|
||||
|
||||
|
||||
class SubjectAltNameWarning(DeprecationWarning):
|
||||
"""
|
||||
Server Certificate does not contain a ``SubjectAltName``.
|
||||
|
||||
Hostname matching is performed on the ``CommonName`` which is deprecated.
|
||||
"""
|
||||
|
||||
|
||||
@attr.s
|
||||
class VerificationError(Exception):
|
||||
"""
|
||||
Service identity verification failed.
|
||||
"""
|
||||
|
||||
errors = attr.ib()
|
||||
|
||||
def __str__(self):
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
@attr.s
|
||||
class DNSMismatch(object):
|
||||
"""
|
||||
No matching DNSPattern could be found.
|
||||
"""
|
||||
|
||||
mismatched_id = attr.ib()
|
||||
|
||||
|
||||
@attr.s
|
||||
class SRVMismatch(object):
|
||||
"""
|
||||
No matching SRVPattern could be found.
|
||||
"""
|
||||
|
||||
mismatched_id = attr.ib()
|
||||
|
||||
|
||||
@attr.s
|
||||
class URIMismatch(object):
|
||||
"""
|
||||
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()
|
||||
|
||||
|
||||
class CertificateError(Exception):
|
||||
"""
|
||||
Certificate contains invalid or unexpected data.
|
||||
"""
|
||||
146
venv/lib/python3.9/site-packages/service_identity/pyopenssl.py
Normal file
146
venv/lib/python3.9/site-packages/service_identity/pyopenssl.py
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
"""
|
||||
`pyOpenSSL <https://github.com/pyca/pyopenssl>`_-specific code.
|
||||
"""
|
||||
|
||||
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 ._common import (
|
||||
DNS_ID,
|
||||
CertificateError,
|
||||
DNSPattern,
|
||||
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 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*.
|
||||
:raises service_identity.CertificateError: If the certificate chain of
|
||||
*connection* contains a certificate that contains invalid/unexpected
|
||||
data.
|
||||
|
||||
:returns: ``None``
|
||||
"""
|
||||
verify_service_identity(
|
||||
cert_patterns=extract_ids(connection.get_peer_certificate()),
|
||||
obligatory_ids=[DNS_ID(hostname)],
|
||||
optional_ids=[],
|
||||
)
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
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 OpenSSL.SSL.X509 cert: The certificate to be dissected.
|
||||
|
||||
:return: List of IDs.
|
||||
"""
|
||||
ids = []
|
||||
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())
|
||||
for n in names:
|
||||
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":
|
||||
comp = n.getComponent()
|
||||
oid = comp.getComponentByPosition(0)
|
||||
if oid == ID_ON_DNS_SRV:
|
||||
srv, _ = decode(comp.getComponentByPosition(1))
|
||||
if isinstance(srv, IA5String):
|
||||
ids.append(SRVPattern(srv.asOctets()))
|
||||
else: # pragma: nocover
|
||||
raise CertificateError(
|
||||
"Unexpected certificate content."
|
||||
)
|
||||
else: # pragma: nocover
|
||||
pass
|
||||
else: # pragma: nocover
|
||||
pass
|
||||
|
||||
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.
|
||||
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 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,
|
||||
)
|
||||
return ids
|
||||
Loading…
Add table
Add a link
Reference in a new issue