diff --git a/doc/src/release_notes.rst b/doc/src/release_notes.rst index e4d8708..3761b05 100644 --- a/doc/src/release_notes.rst +++ b/doc/src/release_notes.rst @@ -17,6 +17,17 @@ oracledb 2.6.0 (TBD) Thin Mode Changes +++++++++++++++++ +#) Perform TLS server matching in python-oracledb instead of the Python SSL + library to allow alternate names to be checked + (`issue 415 `__). +#) Error ``DPY-6002: The distinguished name (DN) on the server certificate + does not match the expected value: "{expected_dn}"`` now shows the expected + value. +#) Error ``DPY-6006: The name on the server certificate does not match the + expected value: "{expected_name}"`` is now raised when neither the common + name (CN) nor any of the subject alternative names (SANs) found on the + server certificate match the host name used to connect to the database. + Thick Mode Changes ++++++++++++++++++ diff --git a/src/oracledb/errors.py b/src/oracledb/errors.py index fcc8be4..a88f780 100644 --- a/src/oracledb/errors.py +++ b/src/oracledb/errors.py @@ -359,6 +359,7 @@ def _raise_not_supported(feature: str) -> None: ERR_INVALID_SID = 6003 ERR_PROXY_FAILURE = 6004 ERR_CONNECTION_FAILED = 6005 +ERR_INVALID_SERVER_NAME = 6006 # error numbers that result in Warning WRN_COMPILATION_ERROR = 7000 @@ -592,7 +593,11 @@ def _raise_not_supported(feature: str) -> None: ERR_INVALID_REF_CURSOR: "invalid REF CURSOR: never opened in PL/SQL", ERR_INVALID_SERVER_CERT_DN: ( "The distinguished name (DN) on the server certificate does not " - "match the expected value" + "match the expected value: {expected_dn}" + ), + ERR_INVALID_SERVER_NAME: ( + "The name on the server certificate does not match the expected " + 'value: "{expected_name}"' ), ERR_INVALID_SERVER_TYPE: "invalid server_type: {server_type}", ERR_INVALID_SERVICE_NAME: ( diff --git a/src/oracledb/impl/thin/crypto.pyx b/src/oracledb/impl/thin/crypto.pyx index 09e654f..b00d0ff 100644 --- a/src/oracledb/impl/thin/crypto.pyx +++ b/src/oracledb/impl/thin/crypto.pyx @@ -42,6 +42,40 @@ except ImportError: DN_REGEX = '(?:^|,\s?)(?:(?P[A-Z]+)=(?P"(?:[^"]|"")+"|[^,]+))+' PEM_WALLET_FILE_NAME = "ewallet.pem" + +def check_server_dn(sock, expected_dn, expected_name): + """ + Validates the server distinguished name (if one is specified) or the + simple name (if a distinguished name is not present). + """ + cert_data = sock.getpeercert(binary_form=True) + cert = x509.load_der_x509_certificate(cert_data) + if expected_dn is not None: + server_dn = cert.subject.rfc4514_string() + expected_dn_dict = dict(re.findall(DN_REGEX, expected_dn)) + server_dn_dict = dict(re.findall(DN_REGEX, server_dn)) + if server_dn_dict != expected_dn_dict: + errors._raise_err(errors.ERR_INVALID_SERVER_CERT_DN, + expected_dn=expected_dn) + else: + for name in cert.subject.get_attributes_for_oid( + x509.oid.NameOID.COMMON_NAME + ): + if name.value == expected_name: + return + try: + ext = cert.extensions.get_extension_for_oid( + x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME + ) + for name in ext.value.get_values_for_type(x509.DNSName): + if name == expected_name: + return + except x509.ExtensionNotFound: + pass + errors._raise_err(errors.ERR_INVALID_SERVER_NAME, + expected_name=expected_name) + + def decrypt_cbc(key, encrypted_text): """ Decrypt the given text using the given key. @@ -81,19 +115,6 @@ def get_derived_key(key, salt, length, iterations): return kdf.derive(key) -def get_server_dn_matches(sock, expected_dn): - """ - Return a boolean indicating if the server distinguished name (DN) matches - the expected distinguished name (DN). - """ - cert_data = sock.getpeercert(binary_form=True) - cert = x509.load_der_x509_certificate(cert_data) - server_dn = cert.subject.rfc4514_string() - expected_dn_dict = dict(re.findall(DN_REGEX, expected_dn)) - server_dn_dict = dict(re.findall(DN_REGEX, server_dn)) - return server_dn_dict == expected_dn_dict - - def get_signature(private_key_str, text): """ Returns a signed version of the given text (used for IAM token diff --git a/src/oracledb/impl/thin/protocol.pyx b/src/oracledb/impl/thin/protocol.pyx index e28de23..19b231c 100644 --- a/src/oracledb/impl/thin/protocol.pyx +++ b/src/oracledb/impl/thin/protocol.pyx @@ -265,14 +265,12 @@ cdef class Protocol(BaseProtocol): self._write_buf._size_for_sdu() break - # for TCPS connections, OOB processing is not supported; if the - # packet flags indicate that TLS renegotiation is required, this is - # performed now + # for TCPS connections, if the packet flags indicate that TLS + # renegotiation is required, this is performed now if address.protocol == "tcps": - self._caps.supports_oob = False packet_flags = self._read_buf._current_packet.packet_flags if packet_flags & TNS_PACKET_FLAG_TLS_RENEG: - self._transport.renegotiate_tls(description) + self._transport.renegotiate_tls(address, description) cdef int _connect_phase_two(self, ThinConnImpl conn_impl, Description description, @@ -374,10 +372,12 @@ cdef class Protocol(BaseProtocol): # set socket on transport self._transport.set_from_socket(sock, params, description, address) - # negotiate TLS, if applicable + # for TCPS connections, OOB processing is not supported and TLS + # negotiation is required if use_tcps: + self._caps.supports_oob = False self._transport.create_ssl_context(params, description, address) - self._transport.negotiate_tls(sock, description) + self._transport.negotiate_tls(sock, address, description) cdef int _process_message(self, Message message) except -1: cdef uint32_t timeout = message.conn_impl._call_timeout @@ -632,7 +632,7 @@ cdef class BaseAsyncProtocol(BaseProtocol): packet_flags = self._read_buf._current_packet.packet_flags if packet_flags & TNS_PACKET_FLAG_TLS_RENEG: self._transport._transport = orig_transport - await self._transport.negotiate_tls_async(self, + await self._transport.negotiate_tls_async(self, address, description) async def _connect_phase_two(self, AsyncThinConnImpl conn_impl, @@ -736,7 +736,8 @@ cdef class BaseAsyncProtocol(BaseProtocol): # negotiate TLS, if applicable if use_tcps: self._transport.create_ssl_context(params, description, address) - return await self._transport.negotiate_tls_async(self, description) + return await self._transport.negotiate_tls_async(self, address, + description) async def _process_message(self, Message message): """ diff --git a/src/oracledb/impl/thin/transport.pyx b/src/oracledb/impl/thin/transport.pyx index 50e3ffd..ef194a9 100644 --- a/src/oracledb/impl/thin/transport.pyx +++ b/src/oracledb/impl/thin/transport.pyx @@ -36,7 +36,6 @@ cdef class Transport: cdef: object _transport object _ssl_context - str _ssl_server_hostname uint32_t _transport_num ssize_t _max_packet_size uint32_t _op_num @@ -156,14 +155,10 @@ cdef class Transport: except ssl.SSLError: pass - # determine the SSL server host name to use if desired; otherwise, mark - # the SSL context to indicate that server host name matching is not - # desired by the client (should generally be avoided) - if description.ssl_server_dn_match \ - and description.ssl_server_cert_dn is None: - self._ssl_server_hostname = address.host - else: - self._ssl_context.check_hostname = False + # the SSL host name is not checked but the SSL server DN matching + # algorithm is run instead after the TLS connection has been + # established + self._ssl_context.check_hostname = False cdef Packet extract_packet(self, bytes data=None): """ @@ -236,31 +231,28 @@ cdef class Transport: read_socks, _, _ = select.select(socket_list, [], [], 0) data_ready[0] = bool(read_socks) - cdef int negotiate_tls(self, object sock, + cdef int negotiate_tls(self, object sock, Address address, Description description) except -1: """ Negotiate TLS on the socket. """ - self._transport = self._ssl_context.wrap_socket( - sock, server_hostname=self._ssl_server_hostname - ) - if description.ssl_server_dn_match \ - and description.ssl_server_cert_dn is not None: - if not get_server_dn_matches(self._transport, - description.ssl_server_cert_dn): - errors._raise_err(errors.ERR_INVALID_SERVER_CERT_DN) + self._transport = self._ssl_context.wrap_socket(sock) + if description.ssl_server_dn_match: + check_server_dn(self._transport, description.ssl_server_cert_dn, + address.host) - cdef int renegotiate_tls(self, Description description) except -1: + cdef int renegotiate_tls(self, Address address, + Description description) except -1: """ Renegotiate TLS on the socket. """ orig_sock = self._transport sock = socket.socket(family=orig_sock.family, type=orig_sock.type, proto=orig_sock.proto, fileno=orig_sock.detach()) - self.negotiate_tls(sock, description) + self.negotiate_tls(sock, address, description) async def negotiate_tls_async(self, BaseAsyncProtocol protocol, - Description description): + Address address, Description description): """ Negotiate TLS on the socket asynchronously. """ @@ -269,13 +261,10 @@ cdef class Transport: self._transport = await loop.start_tls( self._transport, protocol, self._ssl_context, - server_hostname=self._ssl_server_hostname ) - if description.ssl_server_dn_match \ - and description.ssl_server_cert_dn is not None: + if description.ssl_server_dn_match: sock = self._transport.get_extra_info("ssl_object") - if not get_server_dn_matches(sock, description.ssl_server_cert_dn): - errors._raise_err(errors.ERR_INVALID_SERVER_CERT_DN) + check_server_dn(sock, description.ssl_server_cert_dn, address.host) return orig_transport cdef int send_oob_break(self) except -1: