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 -- checkin-list@monetdb.org
To unsubscribe send an email to checkin-list-le...@monetdb.org

Reply via email to