Skip to content

Commit

Permalink
Perform TLS server matching in the driver instead of in the SSL library
Browse files Browse the repository at this point in the history
(#415).
  • Loading branch information
anthony-tuininga committed Nov 8, 2024
1 parent 53a4806 commit f5f2cf7
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 49 deletions.
11 changes: 11 additions & 0 deletions doc/src/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/oracle/python-oracledb/issues/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
++++++++++++++++++

Expand Down
7 changes: 6 additions & 1 deletion src/oracledb/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: (
Expand Down
47 changes: 34 additions & 13 deletions src/oracledb/impl/thin/crypto.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,40 @@ except ImportError:
DN_REGEX = '(?:^|,\s?)(?:(?P<name>[A-Z]+)=(?P<val>"(?:[^"]|"")+"|[^,]+))+'
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.
Expand Down Expand Up @@ -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
Expand Down
19 changes: 10 additions & 9 deletions src/oracledb/impl/thin/protocol.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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):
"""
Expand Down
41 changes: 15 additions & 26 deletions src/oracledb/impl/thin/transport.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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.
"""
Expand All @@ -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:
Expand Down

0 comments on commit f5f2cf7

Please sign in to comment.