Ausgabe der neuen DB Einträge

This commit is contained in:
hubobel 2022-01-02 21:50:48 +01:00
parent bad48e1627
commit cfbbb9ee3d
2399 changed files with 843193 additions and 43 deletions

View file

@ -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",
]

View 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"
)

View 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

View file

@ -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

View file

@ -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.
"""

View 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