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