Changeset: c8fbd1c7e8ce for MonetDB
URL: https://dev.monetdb.org/hg/MonetDB/rev/c8fbd1c7e8ce
Added Files:
clients/mapilib/Tests/tlssecurity.py
clients/mapilib/Tests/tlstester.py
Modified Files:
clients/mapilib/Tests/All
testing/CMakeLists.txt
testing/Mconvert.py.in
testing/Mtest.py.in
Branch: monetdburl
Log Message:
Use tlstester to check verification is done correctly
diffs (truncated from 938 to 300 lines):
diff --git a/clients/mapilib/Tests/All b/clients/mapilib/Tests/All
--- a/clients/mapilib/Tests/All
+++ b/clients/mapilib/Tests/All
@@ -1,1 +1,2 @@
murltest
+HAVE_OPENSSL?tlssecurity
diff --git a/clients/mapilib/Tests/tlssecurity.py
b/clients/mapilib/Tests/tlssecurity.py
new file mode 100755
--- /dev/null
+++ b/clients/mapilib/Tests/tlssecurity.py
@@ -0,0 +1,162 @@
+
+
+import logging
+import os
+import subprocess
+import threading
+import time
+
+import tlstester
+
+logging.basicConfig(level=logging.WARNING)
+# logging.basicConfig(level=logging.DEBUG)
+
+tgtdir = os.environ['TSTTRGDIR']
+assert os.path.isdir(tgtdir)
+
+hostnames = ['localhost']
+# Generate certificates and write them to the scratch dir
+# Write them to the scratch dir for inspection by the user.
+certs = tlstester.Certs(hostnames)
+certsdir = os.path.join(tgtdir, "certs")
+try:
+ os.mkdir(certsdir)
+except FileExistsError:
+ pass
+count = 0
+for name, content in certs.all().items():
+ with open(os.path.join(certsdir, name), "wb") as a:
+ a.write(content)
+ count += 1
+logging.debug(f"Wrote {count} files to {certsdir}")
+
+def certpath(name):
+ return os.path.join(certsdir, name)
+def certbytes(name):
+ filename = certpath(name)
+ with open(filename, 'rb') as f:
+ return f.read()
+
+# Start the worker threads
+
+server = tlstester.TLSTester(
+ certs=certs,
+ listen_addr='127.0.0.1',
+ preassigned=dict(),
+ sequential=False,
+ hostnames=hostnames)
+server_thread = threading.Thread(target=server.serve_forever, daemon=True)
+server_thread.start()
+
+def attempt(portname: str, expected_error: str, /, tls=True, **params):
+ port = server.get_port(portname)
+ scheme = 'monetdbs' if tls else 'monetdb'
+ url = f"{scheme}://localhost:{port}/demo"
+ if params:
+ # should be percent-escaped
+ url += '?' + '&'.join(f"{k}={v}" for k, v in params.items())
+ logging.debug(f"Connecting to {url}, {expected_error=}")
+ cmd = ['mclient', '-d', url]
+ logging.debug(f"{cmd=}")
+ proc = subprocess.run(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+ logging.debug(f"mclient exited with code {proc.returncode},
err={proc.stderr}")
+ assert proc.returncode == 2 and "mclient is not expected to succeed"
+ output = str(proc.stderr, 'utf-8').rstrip()
+ actual_error = None if 'Sorry, this is not' in output else output
+
+ if expected_error is None and actual_error is None:
+ logging.debug("Test succeeded")
+ return
+ if expected_error is not None and actual_error is not None and
expected_error in actual_error:
+ logging.debug("Test succeeded")
+ return
+ logging.error(f"Unexpected result when connecting to port {port}
('{portname}')")
+ logging.error(f"Using URL {url}")
+ message = f"{expected_error=} but {actual_error=}"
+ logging.error(message)
+ raise Exception(message)
+
+
+# Follow the test cases laid out in the README of the tlstester
+# https://github.com/MonetDB/monetdb-tlstester#suggested-test-cases
+
+
+# connect_plain
+#
+# Connect to port 'plain', without using TLS. Have a succesful MAPI exchange.
+
+attempt('plain', None, tls=False)
+
+# connect_tls
+#
+# Connect to port 'server1' over TLS, verifying the connection using ca1.crt.
+# Have a succesful MAPI exchange.
+
+attempt('server1', None, cert=certpath('ca1.crt'))
+
+# refuse_no_cert
+#
+# Connect to port 'server1' over TLS, without passing a certificate. The
+# connection should fail because ca1.crt is not in the system trust root store.
+
+attempt('server1', "verify failed")
+
+# refuse_wrong_cert
+#
+# Connect to port 'server1' over TLS, verifying the connection using ca2.crt.
+# The client should refuse to let the connection proceed.
+
+attempt('server1', 'verify failed', cert=certpath('ca2.crt'))
+
+# refuse_tlsv12
+#
+# Connect to port 'tls12' over TLS, verifying the connection using ca1.crt. The
+# client should refuse to let the connection proceed because it should require
+# at least TLSv1.3.
+
+attempt('tls12', 'protocol version', cert=certpath('ca1.crt'))
+
+# refuse_expired
+#
+# Connect to port 'expiredcert' over TLS, verifying the connection using
+# ca1.crt. The client should refuse to let the connection proceed.
+
+attempt('expiredcert', 'verify failed', cert=certpath('ca1.crt'))
+
+# connect_client_auth
+#
+# Connect to port 'clientauth' over TLS, verifying the connection using
ca1.crt.
+# Authenticate using client2.key and client2.crt. Have a succesful MAPI
+# exchange.
+
+# TODO
+#attempt('clientauth', None,
cert=certpath('ca1.crt'),clientcert=certpath('client2.crt'),
clientkey=certpath('client2.key'))
+
+# fail_plain_to_tls
+#
+# Connect to port 'plain' over TLS. This should fail, not hang.
+
+attempt('plain', 'wrong version number', tls=True)
+
+# fail_tls_to_plain
+#
+# Make a plain MAPI connection to port 'server1'. This should fail.
+
+attempt('server1', 'terminated', tls=False)
+
+# connect_trusted
+#
+# Only when running in a throwaway environment such as a Docker container:
+# Install ca3.crt in the system root certificate store. This is highly
+# system-specific. Connect to port 'server3' over TLS without passing a
+# certificate to check. The implementation should pick it up from the system
+# store. Have a succesful MAPI exchange.
+
+# TODO
+#attempt('server3', None)
+
+
+# Uncomment to keep the server running so you
+# can run some experiments from the command line
+
+# logging.warning("sleeping"); time.sleep(86400)
diff --git a/clients/mapilib/Tests/tlstester.py
b/clients/mapilib/Tests/tlstester.py
new file mode 100755
--- /dev/null
+++ b/clients/mapilib/Tests/tlstester.py
@@ -0,0 +1,725 @@
+#!/usr/bin/env python3
+
+from argparse import ArgumentParser
+from datetime import datetime, timedelta
+import hashlib
+import http.server
+import io
+import logging
+import os
+import socket
+import socketserver
+import ssl
+from ssl import AlertDescription, SSLContext, SSLEOFError, SSLError, TLSVersion
+import struct
+import sys
+import tempfile
+from threading import Thread
+import threading
+from typing import Any, Callable, List, Optional, Tuple, Union
+
+from cryptography import x509
+from cryptography.hazmat.primitives import serialization, hashes
+from cryptography.hazmat.primitives.asymmetric import rsa
+
+VERSION = "0.3.1"
+
+DESCRIPTION = f"tlstester.py version {VERSION}: a utility to help test TLS
MAPI client implementations."
+
+log = logging.getLogger("tlstester")
+
+argparser = ArgumentParser("tlstester", description=DESCRIPTION)
+argparser.add_argument(
+ "-p",
+ "--base-port",
+ type=int,
+ help="base port on which this utility is reachable",
+)
+argparser.add_argument(
+ "-w",
+ "--write",
+ type=str,
+ metavar="DIR",
+ help="Write generated keys and certs to this directory",
+)
+argparser.add_argument(
+ "-l",
+ "--listen-addr",
+ type=str,
+ default="localhost",
+ help="interface to listen on, default=localhost",
+)
+argparser.add_argument(
+ "-n",
+ "--hostname",
+ action="append",
+ type=str,
+ default=[],
+ help="server name to sign certificates for, can be repeated,
default=localhost.localdomain",
+)
+argparser.add_argument(
+ "--sequential",
+ action="store_true",
+ help="allocate ports sequentially after BASE_PORT, instead of whatever the
OS decides",
+)
+argparser.add_argument(
+ "-a",
+ "--assign",
+ action="append",
+ metavar="NAME=PORTNUM",
+ default=[],
+ help="force port assignment",
+)
+argparser.add_argument(
+ "-f",
+ "--forward",
+ metavar="LOCALPORT:FORWARDHOST:FORWARDPORT",
+ type=str,
+ help="forward decrypted traffic somewhere else",
+)
+argparser.add_argument(
+ "-v", "--verbose", action="store_true", help="Log more information"
+)
+
+
+class Certs:
+ hostnames: str
+ _files: dict[str, bytes]
+ _keys: dict[x509.Name, rsa.RSAPrivateKey]
+ _certs: dict[x509.Name, x509.Certificate]
+ _parents: dict[x509.Name, x509.Name]
+
+ def __init__(self, hostnames: List[str]):
+ self.hostnames = hostnames
+ self._files = {}
+ self._keys = {}
+ self._certs = {}
+ self._parents = {}
+ self.gen_keys()
+
+ def get_file(self, name):
+ return self._files.get(name)
+
+ def all(self) -> dict[str, str]:
+ return self._files.copy()
+
+ def gen_keys(self):
+ ca1 = self.gen_ca("ca1")
+ self.gen_server("server1", ca1)
+ self.gen_server("server1x", ca1, not_before=-15, not_after=-1)
+ ca2 = self.gen_ca("ca2")
+ self.gen_server("server2", ca2)
+ self.gen_server("client2", ca2, keycrt=True)
+ ca3 = self.gen_ca("ca3")
+ self.gen_server("server3", ca3)
+
+ def gen_ca(self, name: str):
+ ca_name = x509.Name(
+ [
+ x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, f"Org
{name}"),
+ x509.NameAttribute(
+ x509.NameOID.COMMON_NAME, f"The Certificate Authority"
+ ),
_______________________________________________
checkin-list mailing list -- [email protected]
To unsubscribe send an email to [email protected]