diff options
Diffstat (limited to 'src/service_identity/pyopenssl.py')
-rw-r--r-- | src/service_identity/pyopenssl.py | 108 |
1 files changed, 108 insertions, 0 deletions
diff --git a/src/service_identity/pyopenssl.py b/src/service_identity/pyopenssl.py new file mode 100644 index 0000000..fb39954 --- /dev/null +++ b/src/service_identity/pyopenssl.py @@ -0,0 +1,108 @@ +""" +`pyOpenSSL <https://github.com/pyca/pyopenssl>`_-specific code. +""" + +from __future__ import absolute_import, division, print_function + +import warnings + +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 ( + CertificateError, + DNSPattern, + DNS_ID, + SRVPattern, + URIPattern, + verify_service_identity, +) + + +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` + + :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=[], + ) + + +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 cert: The certificate to be dissected. + :type cert: :class:`OpenSSL.SSL.X509` + + :return: List of IDs. + """ + ids = [] + for i in 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 == "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." + ) + + if not ids: + # http://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. + 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 + ) + ids = [DNSPattern(c[1]) + for c + in cert.get_subject().get_components() + if c[0] == b"CN"] + return ids + + +__all__ = [ + "verify_hostname", +] |