#!/usr/bin/env python3 """Real TLS, document work, external client and CLI interop for the optional peer.""" import argparse import json from pathlib import Path import select import socket import ssl import struct import subprocess import tempfile ROOT = Path(__file__).resolve().parent def openssl(*args): subprocess.run(['openssl', *map(str, args)], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) def certificates(root): ca = root / 'ca.pem' openssl('req', '-x509', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256', '-nodes', '-days', '1', '-subj', '/CN=Pure test CA', '-addext', 'basicConstraints=critical,CA:TRUE', '-addext', 'keyUsage=critical,keyCertSign,cRLSign', '-keyout', root / 'ca.key', '-out', ca) for role, name, eku in [('server', 'localhost', 'serverAuth'), ('client', 'pure-client', 'clientAuth'), ('other', 'wrong-client', 'clientAuth')]: ext = root / (role + '.ext') ext.write_text('basicConstraints=critical,CA:FALSE\n' 'keyUsage=critical,digitalSignature\n' f'subjectAltName=DNS:{name}\nextendedKeyUsage={eku}\n') openssl('req', '-newkey', 'ec', '-pkeyopt', 'ec_paramgen_curve:P-256', '-nodes', '-subj', '/CN=' + name, '-keyout', root / (role + '.key'), '-out', root / (role + '.csr')) openssl('x509', '-req', '-in', root / (role + '.csr'), '-CA', ca, '-CAkey', root / 'ca.key', '-CAcreateserial', '-days', '1', '-extfile', ext, '-out', root / (role + '.pem')) return ca def start(binary, program, store, root, ca): proc = subprocess.Popen([str(binary), 'serve', '127.0.0.1', '0', str(program), str(store), str(ca), str(root / 'server.pem'), str(root / 'server.key'), 'pure-client'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1) try: assert select.select([proc.stdout], [], [], 5)[0], 'server did not become ready' line = proc.stdout.readline() assert line.startswith('READY '), (line, proc.poll()) return proc, int(line.split()[1]) except BaseException as error: proc.kill() stdout, stderr = proc.communicate(timeout=3) raise AssertionError(f'server start failed: {error}; stdout={stdout!r}, stderr={stderr!r}') from error def stop(proc): proc.terminate() stdout, stderr = proc.communicate(timeout=5) assert proc.returncode == 0, (proc.returncode, stdout, stderr) def connect(root, ca, port, *, cert='client', alpn='pure-doc/1', name='localhost'): ctx = ssl.create_default_context(cafile=str(ca)) ctx.minimum_version = ssl.TLSVersion.TLSv1_3 ctx.maximum_version = ssl.TLSVersion.TLSv1_3 if cert: ctx.load_cert_chain(str(root / (cert + '.pem')), str(root / (cert + '.key'))) ctx.set_alpn_protocols([alpn]) plain = socket.create_connection(('127.0.0.1', port), timeout=5) try: secure = ctx.wrap_socket(plain, server_hostname=name) secure.settimeout(7) if secure.selected_alpn_protocol() != alpn: raise ssl.SSLError('ALPN mismatch') return secure except BaseException: plain.close() raise def exact(sock, count): parts = [] while count: part = sock.recv(count) if not part: raise EOFError('unexpected EOF') parts.append(part) count -= len(part) return b''.join(parts) def hello(sock, program): import hashlib assert exact(sock, 40) == b'PDOC\x01\0\0\0' + hashlib.sha256(program.read_bytes()).digest() def request(sock, op, key, expected=0, body=b'', *, pieces=False, want_body=True): key = key.encode('ascii') wire = struct.pack('!BBQI', op | (0x80 if want_body else 0), len(key), expected, len(body)) + key + body if pieces: for at in range(0, len(wire), 4093): sock.sendall(wire[at:at + 4093]) else: sock.sendall(wire) status, revision, length = struct.unpack('!BQI', exact(sock, 13)) assert length <= 1450000, length return status, revision, exact(sock, length if want_body else 0), length def edit(offset, remove, inserted): return struct.pack('