Skip to content

Commit a934485

Browse files
authored
verify server certificate (#136)
1 parent 66213ad commit a934485

File tree

3 files changed

+110
-33
lines changed

3 files changed

+110
-33
lines changed

docs/security.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ hide:
44
- toc
55
---
66

7+
## SSL Verification
8+
9+
Use environment variable `SESSION_SSL_VERIFY=cert` to require that the
10+
server provide a trusted certificate.
11+
12+
Use environment variable `SESSION_SSL_VERIFY=hostname` to require that
13+
the certificate hostname match the requested hostname. Note that this
14+
also requires that the server provide a trusted certificate.
15+
16+
## Cipher Issues
17+
718
Python 3.10 is aggressive in causing failures for algorithms/options that are not secure enough. If you receive an SSL-related message, there is a good chance of a security weakness in the host/server.
819

920
The best course of action is to request that the server be updated to support security best practices in terms of supported encryption algorithms and key sizes.

tnz/tnz.py

Lines changed: 80 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
88
Environment variables used:
99
SESSION_PS_SIZE
10+
SESSION_SECLEVEL
11+
SESSION_SSL_VERIFY
1012
TNZ_COLORS
1113
TNZ_LOGGING
1214
ZTI_SECLEVEL
1315
14-
Copyright 2021, 2023 IBM Inc. All Rights Reserved.
16+
Copyright 2021, 2024 IBM Inc. All Rights Reserved.
1517
1618
SPDX-License-Identifier: Apache-2.0
1719
"""
@@ -94,6 +96,8 @@ def __init__(self, name=None):
9496
self.colors = 768
9597

9698
self.__secure = False
99+
self.__cert_verified = False
100+
self.__start_tls_hostname = None
97101
self.__host_verified = False
98102
self._event = None
99103
self.__loop = None
@@ -372,6 +376,12 @@ def connect(self, host=None, port=None,
372376

373377
if host is None:
374378
host = "127.0.0.1" # default host
379+
elif not secure: # if might need hostname later for start_tls
380+
import socket
381+
try:
382+
self.__start_tls_hostname = socket.getfqdn(host)
383+
except socket.gaierror:
384+
pass
375385

376386
if port is None:
377387
if secure is False:
@@ -383,13 +393,20 @@ def connect(self, host=None, port=None,
383393
self._event = event
384394

385395
self.__secure = False
396+
self.__cert_verified = False
386397
self.__host_verified = False
387398

399+
def _connection_made(_, transport):
400+
self._transport = transport
401+
self.seslost = False
402+
if context:
403+
self.__secure = True
404+
if context.verify_mode == ssl.CERT_REQUIRED:
405+
self.__cert_verified = True
406+
self.__host_verified = context.check_hostname
407+
388408
class _TnzProtocol(asyncio.BaseProtocol):
389-
@staticmethod
390-
def connection_made(transport):
391-
self._transport = transport
392-
self.seslost = False
409+
connection_made = _connection_made
393410

394411
@staticmethod
395412
def connection_lost(exc):
@@ -425,22 +442,7 @@ def resume_writing():
425442
"""
426443
self._log_warn("resume_writing")
427444

428-
if secure:
429-
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
430-
if os.getenv("ZTI_SECLEVEL", "2") == "1":
431-
context.set_ciphers("DEFAULT@SECLEVEL=1")
432-
433-
if verifycert:
434-
context.load_verify_locations("ibm-cacerts.pem")
435-
self.__host_verified = True # ? too soon ?
436-
437-
else:
438-
context.check_hostname = False # insecure FIXME
439-
context.verify_mode = ssl.CERT_NONE # insecure FIXME
440-
441-
else:
442-
context = None
443-
445+
context = self.__create_context(verifycert) if secure else None
444446
coro = self.__connect(_TnzProtocol, host, port, context)
445447
loop = self.__get_event_loop()
446448
task = loop.create_task(coro)
@@ -2293,9 +2295,7 @@ def _process(self, data):
22932295

22942296
elif data == b"\xff\xfa\x2e\x01": # IAC SB ...
22952297
self.__log_info("i<< START_TLS FOLLOWS")
2296-
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
2297-
context.check_hostname = False
2298-
context.verify_mode = ssl.CERT_NONE
2298+
context = self.__create_context()
22992299
coro = self.__start_tls(context)
23002300
task = self.__loop.create_task(coro)
23012301
self.__connect_task = task
@@ -3713,6 +3713,37 @@ async def __connect(self, protocol, host, port, ssl_context):
37133713
if self.__connect_task is task:
37143714
self.__connect_task = None
37153715

3716+
def __create_context(self, verifycert=None):
3717+
"""Create an SSL context for a session.
3718+
3719+
Uses environment variables to determine context options.
3720+
"""
3721+
context = ssl.create_default_context()
3722+
3723+
getenv = os.environ.get
3724+
seclevel = getenv("SESSION_SECLEVEL")
3725+
if not seclevel and getenv("ZTI_SECLEVEL") == "1":
3726+
seclevel = "1"
3727+
3728+
if seclevel:
3729+
context.set_ciphers(f"DEFAULT@SECLEVEL={seclevel}")
3730+
3731+
ssl_verify = getenv("SESSION_SSL_VERIFY", "")
3732+
context.check_hostname = ssl_verify == "hostname"
3733+
3734+
if verifycert is None:
3735+
if not ssl_verify and context.check_hostname:
3736+
verifycert = True
3737+
else:
3738+
verifycert = ssl_verify in ("cert", "hostname")
3739+
3740+
if verifycert:
3741+
context.verify_mode = ssl.CERT_REQUIRED
3742+
else:
3743+
context.verify_mode = ssl.CERT_NONE
3744+
3745+
return context
3746+
37163747
def __erase(self, saddr, eaddr):
37173748
"""Process erase function.
37183749
@@ -4447,10 +4478,19 @@ async def __start_tls(self, context):
44474478
transport = self._transport
44484479
protocol = transport.get_protocol()
44494480
self._transport = None
4481+
server_hostname = None
4482+
if context.check_hostname:
4483+
server_hostname = self.__start_tls_hostname
4484+
if server_hostname is None:
4485+
raise TnzError("no hostname for check_hostname")
4486+
44504487
try:
4451-
transport = await loop.start_tls(transport,
4452-
protocol,
4453-
context)
4488+
transport = await loop.start_tls(
4489+
transport,
4490+
protocol,
4491+
context,
4492+
server_hostname=server_hostname,
4493+
)
44544494
except asyncio.CancelledError:
44554495
self.seslost = True
44564496
self._event.set()
@@ -4463,6 +4503,10 @@ async def __start_tls(self, context):
44634503
else:
44644504
self._transport = transport
44654505
self.__secure = True
4506+
if context.verify_mode == ssl.CERT_REQUIRED:
4507+
self.__cert_verified = True
4508+
self.__host_verified = context.check_hostname
4509+
44664510
self.__log_debug("__start_tls transport: %r", transport)
44674511
self.send() # in case send() ignored for _transport = None
44684512

@@ -4702,6 +4746,12 @@ def __repl(mat):
47024746

47034747
# Readonly properties
47044748

4749+
@property
4750+
def cert_verified(self):
4751+
"""Bool indicating if secure and cert was verified as trusted.
4752+
"""
4753+
return self.__cert_verified
4754+
47054755
@property
47064756
def host_verified(self):
47074757
"""Bool indicating if secure and host was verified.
@@ -4871,15 +4921,16 @@ def connect(host=None, port=None,
48714921
secure = True for encrypted connection
48724922
verifycert only has meaning when secure is True
48734923
"""
4924+
ssl_verify = os.environ.get("SESSION_SSL_VERIFY", "")
48744925
tnz = Tnz(name=name)
48754926

48764927
if port is None and secure is not False:
48774928
port = 992
4878-
if verifycert is None:
4929+
if verifycert is None and secure is None and not ssl_verify:
48794930
verifycert = False
48804931

48814932
if secure and verifycert is None:
4882-
verifycert = True
4933+
verifycert = ssl_verify in ("cert", "hostname")
48834934

48844935
if secure is None:
48854936
secure = bool(port != 23)

tnz/zti.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
ZTI_TITLE
3131
_BPX_TERMPATH (see _termlib.py)
3232
33-
Copyright 2021, 2023 IBM Inc. All Rights Reserved.
33+
Copyright 2021, 2024 IBM Inc. All Rights Reserved.
3434
3535
SPDX-License-Identifier: Apache-2.0
3636
"""
@@ -772,6 +772,22 @@ def do_session(self, arg):
772772

773773
print(f" SESSION_CODE_PAGE={tns.codec_info[0].name}")
774774
print(f" SESSION_PS_SIZE={tns.amaxrow}x{tns.amaxcol}")
775+
776+
if tns.secure:
777+
verify = ""
778+
if tns.host_verified:
779+
verify = "hostname"
780+
elif tns.cert_verified:
781+
verify = "cert"
782+
783+
if verify:
784+
print(f" SESSION_SSL_VERIFY={verify}")
785+
else:
786+
print(f" SESSION_SSL=1")
787+
print(f" SESSION_SSL_VERIFY=none")
788+
else:
789+
print(f" SESSION_SSL=0")
790+
775791
print(f" SESSION_TN_ENHANCED={tns.tn3270e:d}")
776792
print(f" SESSION_DEVICE_TYPE={tns.terminal_type}")
777793

@@ -780,8 +796,6 @@ def do_session(self, arg):
780796
else:
781797
print(" Alternate code page not supported")
782798

783-
print(" socket type: "+repr(tns.getsockettype()))
784-
785799
if tns.extended_color_mode():
786800
print(" Extended color mode")
787801
else:
@@ -1061,10 +1075,11 @@ def help_vars(self):
10611075
print("""Variables used when creating a new session:
10621076
SESSION_CODE_PAGE - code page, e.g. cp037
10631077
SESSION_LU_NAME - LU name for TN3270E CONNECT
1064-
SESSION_HOST - tcp/ip hostname
1078+
SESSION_HOST - tcp/ip hostname or IP address
10651079
SESSION_PORT - tcp/ip port, default is 992
10661080
SESSION_PS_SIZE - terminal size, e.g. 62x160
10671081
SESSION_SSL - set to 0 to not force SSL
1082+
SESSION_SSL_VERIFY - set to cert or hostname to require verification
10681083
SESSION_TN_ENHANCED - set to 1 allow TN3270E
10691084
SESSION_DEVICE_TYPE - device-type, e.g. IBM-DYNAMIC
10701085
""")

0 commit comments

Comments
 (0)