Changeset: 27b6861a2493 for MonetDB
URL: https://dev.monetdb.org/hg/MonetDB/rev/27b6861a2493
Modified Files:
        clients/mapilib/Tests/systemcertificates.py
Branch: Dec2023
Log Message:

Properly test system certificates

Depends on environment variables TLSTEST_URL and TLSTEST_ALTURL


diffs (166 lines):

diff --git a/clients/mapilib/Tests/systemcertificates.py 
b/clients/mapilib/Tests/systemcertificates.py
--- a/clients/mapilib/Tests/systemcertificates.py
+++ b/clients/mapilib/Tests/systemcertificates.py
@@ -7,39 +7,134 @@
 # Copyright 1997 - July 2008 CWI, August 2008 - 2023 MonetDB B.V.
 
 
-import subprocess
-import sys
-
 # Test that certificates from the system trust store are used when no explicit
 # certificate is given.
 #
-# # If all goes well, this will give a protocol error (MAPI != HTTP),
-# not a TLS error
+# This test tries to connect to an existing TLS+MAPI server that has a publicly
+# trusted certificate. This needs to be configured externally through
+# the environment variables TLSTEST_URL and TLSTEST_ALTURL.
+#
+# These must point to the same server so they receive the same certificate.
+# However, the host name in TLSTEST_URL must match the certificate while the
+# hostname in TLSTEST_ALT doesn't match.
+
+
+import logging
+import os
+import shlex
+import socket
+import subprocess
+import ssl
+import sys
+import urllib
+from urllib.parse import urlparse
+
+level = logging.WARNING
+# if sys.platform == 'win32':
+#     level=logging.DEBUG
+if '-v' in sys.argv:
+    level = logging.DEBUG
+# level = logging.DEBUG
+logging.basicConfig(level=level)
+
+logger = logging.root
 
-# Ideally we'd use www.monetdb.org but, but at the time of writing 
www.monetdb.org
-# only returns an error after a 20 second time out.
-#
-# python.org on the other hand uses a CDN which kicks us out instantly.
-HOST = 'python.org'
+#####################################################################
+# Look for the URL configuration variables, exit if not present
+
+def getvar(name):
+    value = os.environ.get(name)
+    if value:
+        logger.debug(f"Environment variable {name}={value}")
+        return value
+    else:
+        logger.debug(f"Environment variable {name} is not set, exiting early")
+        exit(0)
+
+url = getvar('TLSTEST_URL')
+alturl = getvar('TLSTEST_ALTURL')
+
+
+#####################################################################
+# Before trying with mclient, connect directly to see if the server is
+# up and behaves the way we need for this test
+
+def try_connect(url, check_hostname, expected_error):
+    logger.info(f"Attempting to connect to {url}, 
check_hostname={check_hostname}")
+    parsed = urlparse(url)
+    assert parsed.scheme == 'monetdbs', f'Expected scheme monetdbs, not 
{parsed.scheme}'
+
+    host = parsed.hostname
+    port = parsed.port or 50000
+    logger.debug(f"Connecting to host {host!r} port {port!r}")
+
+    with socket.create_connection((host, port)) as sock:
+        logger.debug(f"Connection established")
 
-# Run mclient
-cmd = ['mclient', '-L-', '-d', f"monetdbs://{HOST}:443/demo"]
-proc = subprocess.run(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
-if proc.returncode != 2:
-    msg = str(proc.stderr, 'utf-8')
-    print(f"mclient is supposed to exit with status 2, not 
{proc.returncode}.\n--- stderr ---\n{msg}\n---end stderr ---", file=sys.stderr)
-    exit(1)
+        logger.debug(f"Verifying TLS")
+        ctx = ssl.create_default_context()
+        ctx.check_hostname = check_hostname
+        try:
+            ssl_connection = ctx.wrap_socket(sock, server_hostname=host)
+            # if we get here it succeeded
+            if expected_error:
+                raise Exception(f"Verification succeeded unexpectedly")
+            else:
+                logger.debug(f"Verification succeeded as expected")
+            logger.debug("Closing")
+            ssl_connection.close()
+        except ssl.SSLError as e:
+            if expected_error and expected_error in str(e):
+                logger.debug(f"Verification failed as expected: {e}")
+            elif expected_error:
+                logger.error(f"Verification failed but the error did not match 
{expected_error!r}: {e}")
+            else:
+                logger.error(f"Verification failed unexpectedly: {e}")
+                raise e
+
+
+# both urls should be reachable and present a TLS certificate
+try_connect(url, check_hostname=False, expected_error=None)
+try_connect(alturl, check_hostname=False, expected_error=None)
+
+# url should have the expected hostname so this should succeed
+try_connect(url, check_hostname=True, expected_error=None)
+
+# alturl should have a hostname that makes the verification fail
+try_connect(alturl, check_hostname=True, expected_error='Hostname mismatch')
+
 
-# After the TLS handshake succeeds we expect the server to send something like
-# 'HTTP/1.1 400 Bad Request' because we're sending \x00\x00 instead of an HTTP
-# request. libmapi will interpret the first two bytes 'H' and 'T' as an invalid
-# block header.
-#
-# In ASCII,  'H' + 256 * 'T'  ==  72 + 256 * 84  ==  21576.
-tls_works_but_mapi_fails = b'21576' in proc.stderr
+#####################################################################
+# The URLs and the server are configured correctly.
+# Check if mclient notices the wrong host name in alturl.
+
+def run_mclient(url, should_pass):
+    cmd = ['mclient', '-L-', '-s', 'SELECT 42', '-d', url]
+    display_cmd = ' '.join(shlex.quote(w) for w in cmd)
+    logger.info(f"Running {display_cmd}")
+    proc = subprocess.run(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+
+    status = proc.returncode
+    logger.debug(f"Mclient exited with status {status}")
 
-if not tls_works_but_mapi_fails:
-    msg = str(proc.stderr, 'utf-8')
-    print(f"Expected mclient to print an error message containing the number 
21576, got:\n--- stderr ---\n{msg}\n---end stderr ---", file=sys.stderr)
-    exit(1)
+    err = proc.stderr
+    logger.debug('--- stderr ---')
+    try:
+        for line in str(err, 'utf-8').splitlines():
+            logger.debug(line)
+    except UnicodeDecodeError:
+        logger.debug(err)
+    logger.debug('--- end of stderr ---')
 
+    passed_verification = (status == 0 or b'Sorry, this is not a real' in err)
+    msg = f"passed_verification={passed_verification}, 
should_pass={should_pass}"
+
+    if passed_verification == should_pass:
+        logger.debug(f"{msg}, as expected")
+    else:
+        logger.error(f"{msg}, unexpected!")
+        raise Exception(msg)
+
+run_mclient(url, should_pass=True)
+
+run_mclient(alturl, should_pass=False)
_______________________________________________
checkin-list mailing list -- checkin-list@monetdb.org
To unsubscribe send an email to checkin-list-le...@monetdb.org

Reply via email to