source: ntrip/trunk/BNC/tests/tools/ssl_check.py

Last change on this file was 11127, checked in by stuerze, 2 weeks ago

test suite extended regardiing security aspects

  • Property svn:executable set to *
File size: 8.9 KB
Line 
1#!/usr/bin/env python3
2"""TLS checks of BNC's Ntrip 2s client and upload (option sslIgnoreErrors,
3CA certificate path, client certificate) against tools/fakecaster.py.
4
5BNC uploads with Ntrip 2s always to port 443, so the checks run in an
6unprivileged user and network namespace (unshare -rn), where binding port 443
7is allowed and nothing leaves the machine. Certificates are created with
8openssl in tests/work/ssl_check/.
9
10 tests/tools/ssl_check.py [--bnc PATH] exit code 1 if a check fails
11
12Checks (host name localhost, self-signed test CA):
13 upload : server certificate of an unknown CA is rejected / accepted with
14 the CA in 'sslCaCertPath' / accepted with sslIgnoreErrors, the
15 ignored errors being logged; certificate for another host name
16 rejected; client certificate presented also with sslIgnoreErrors
17 download: unknown CA rejected / accepted with the CA / with sslIgnoreErrors
18"""
19
20import argparse
21import glob
22import os
23import shutil
24import subprocess
25import sys
26
27TOOLS = os.path.dirname(os.path.abspath(__file__))
28TESTS = os.path.dirname(TOOLS)
29ROOT = os.path.dirname(TESTS)
30WORK = os.path.join(TESTS, "work", "ssl_check")
31UPLOAD_RAW = os.path.join(TESTS, "data", "upload_rtnet_5min.raw")
32SERVE_RAW = os.path.join(TESTS, "data", "crafted_limits.raw")
33
34
35def sh(*cmd):
36 subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
37
38
39def make_certs(d):
40 os.makedirs(d)
41 sh("openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "30",
42 "-subj", "/CN=BNC Test CA", "-keyout", d + "/ca.key", "-out", d + "/ca.crt")
43
44 def signed(name, cn, san, ext="serverAuth"):
45 with open(d + "/" + name + ".ext", "w") as f:
46 f.write("subjectAltName=%s\nextendedKeyUsage=%s\n" % (san, ext))
47 sh("openssl", "genrsa", "-traditional", "-out", d + "/" + name + ".key", "2048")
48 sh("openssl", "req", "-new", "-key", d + "/" + name + ".key", "-subj", "/CN=" + cn,
49 "-out", d + "/" + name + ".csr")
50 sh("openssl", "x509", "-req", "-in", d + "/" + name + ".csr", "-CA", d + "/ca.crt",
51 "-CAkey", d + "/ca.key", "-CAcreateserial", "-days", "30",
52 "-extfile", d + "/" + name + ".ext", "-out", d + "/" + name + ".crt")
53
54 signed("server", "localhost", "DNS:localhost,IP:127.0.0.1")
55 signed("other", "other.example", "DNS:other.example")
56 signed("client", "bnc-client", "DNS:bnc-client", "clientAuth")
57 os.makedirs(d + "/cadir")
58 shutil.copy(d + "/ca.crt", d + "/cadir/ca.crt")
59 os.makedirs(d + "/emptydir")
60 # client certificate naming convention of BNC: <host>.<port>.crt/.key
61 os.makedirs(d + "/clientdir")
62 shutil.copy(d + "/client.crt", d + "/clientdir/localhost.443.crt")
63 shutil.copy(d + "/client.key", d + "/clientdir/localhost.443.key")
64
65
66def run(bnc, name, certs, server, caster_args, bnc_args, live):
67 """Start the fake caster on 443, run BNC; returns (caster output, BNC log)."""
68 d = os.path.join(WORK, name)
69 os.makedirs(os.path.join(d, "Output"))
70 os.symlink(os.path.join(ROOT, "Example_Configs", "Input"), os.path.join(d, "Input"))
71 fc = subprocess.Popen(
72 [sys.executable, "-I", os.path.join(TOOLS, "fakecaster.py"), "--port", "443",
73 "--out", os.path.join(d, "Output", "upload.raw"), "--sta", "SSL",
74 "--time", "2026-10-01T09:33:00", "--timeout", "8",
75 "--tls-cert", certs + "/" + server + ".crt", "--tls-key", certs + "/" + server + ".key"]
76 + caster_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
77 fc.stdout.readline() # "listening"
78 env = dict(os.environ, QT_QPA_PLATFORM="offscreen", LC_ALL="C", TZ="UTC")
79 cmd = [bnc, "--nw", "--platform", "offscreen"] + bnc_args
80 with open(os.path.join(d, "bnc.out"), "wb") as out:
81 if live: # Ntrip client: runs until stopped
82 p = subprocess.Popen(cmd, cwd=d, stdout=out, stderr=subprocess.STDOUT, env=env)
83 try:
84 p.wait(12)
85 except subprocess.TimeoutExpired:
86 p.send_signal(2)
87 p.wait(30)
88 else:
89 subprocess.run(cmd, cwd=d, stdout=out, stderr=subprocess.STDOUT, env=env, timeout=300)
90 caster = fc.communicate(timeout=120)[0]
91 log = ""
92 for f in sorted(glob.glob(os.path.join(d, "Output", "*.log*"))):
93 with open(f, errors="replace") as inp:
94 log += inp.read()
95 with open(os.path.join(d, "caster.out"), "w") as f:
96 f.write(caster)
97 return caster, log
98
99
100def upload(bnc, name, certs, server="server", ignore=0, cadir="emptydir", clientdir="emptydir",
101 caster_args=()):
102 mp = ("localhost,443,CLOCK,2s,user,pass,IGS20,IGS-SSR,0,,,,,,,0 byte(s),")
103 return run(bnc, name, certs, server, list(caster_args),
104 ["--conf", os.path.join(ROOT, "Example_Configs", "16_Upload.bnc"),
105 "--file", UPLOAD_RAW,
106 "-key", "uploadMountpointsOut", mp,
107 "-key", "sslIgnoreErrors", str(ignore),
108 "-key", "sslCaCertPath", certs + "/" + cadir,
109 "-key", "sslClientCertPath", certs + "/" + clientdir + "/"], live=False)
110
111
112def download(bnc, name, certs, ignore=0, cadir="emptydir"):
113 return run(bnc, name, certs, "server", ["--serve", SERVE_RAW],
114 ["--conf", os.path.join(ROOT, "Example_Configs", "22_Empty.bnc"),
115 "-key", "mountPoints", "//user:pass@localhost:443/TEST RTCM_3 DEU 50.00 8.00 no 2s",
116 "-key", "logFile", "Output/bnc.log",
117 "-key", "sslIgnoreErrors", str(ignore),
118 "-key", "sslCaCertPath", certs + "/" + cadir], live=True)
119
120
121def main():
122 ap = argparse.ArgumentParser(description=__doc__,
123 formatter_class=argparse.RawDescriptionHelpFormatter)
124 ap.add_argument("--bnc", default=os.path.join(ROOT, "bnc"))
125 args = ap.parse_args()
126 bnc = os.path.abspath(args.bnc)
127
128 if os.environ.get("BNC_SSL_CHECK_NS") != "1":
129 # re-run inside a new user + network namespace
130 env = dict(os.environ, BNC_SSL_CHECK_NS="1")
131 return subprocess.run(["unshare", "-rn", "sh", "-c",
132 'ip link set lo up && exec "$@"', "sh",
133 sys.executable, "-I", os.path.abspath(__file__), "--bnc", bnc],
134 env=env).returncode
135
136 shutil.rmtree(WORK, ignore_errors=True)
137 certs = os.path.join(WORK, "certs")
138 make_certs(certs)
139
140 failed = 0
141
142 def check(name, ok, detail):
143 nonlocal failed
144 print("%-6s %-40s %s" % ("PASS" if ok else "FAIL", name, detail), flush=True)
145 failed += not ok
146
147 def received(caster):
148 for line in caster.splitlines():
149 if line.startswith("received "):
150 return int(line.split()[1])
151 return 0
152
153 def served(caster):
154 for line in caster.splitlines():
155 if line.startswith("served "):
156 return int(line.split()[1])
157 return 0
158
159 c, log = upload(bnc, "up_unknown_ca", certs)
160 check("upload: unknown CA rejected", received(c) == 0 and "SSL Error" in log
161 and "ignored" not in log, "%d bytes received" % received(c))
162
163 c, log = upload(bnc, "up_ca", certs, cadir="cadir")
164 check("upload: CA in sslCaCertPath accepted", received(c) > 0 and "SSL Error" not in log,
165 "%d bytes received" % received(c))
166
167 c, log = upload(bnc, "up_ignore", certs, ignore=2)
168 check("upload: sslIgnoreErrors, errors logged", received(c) > 0 and "(ignored as configured)" in log,
169 "%d bytes received" % received(c))
170
171 c, log = upload(bnc, "up_other_host", certs, server="other", cadir="cadir")
172 check("upload: other host name rejected", received(c) == 0 and "SSL Error" in log,
173 "%d bytes received" % received(c))
174
175 c, log = upload(bnc, "up_client_cert", certs, cadir="cadir", clientdir="clientdir",
176 caster_args=["--tls-client-ca", certs + "/ca.crt"])
177 check("upload: client certificate", received(c) > 0, "%d bytes received" % received(c))
178
179 c, log = upload(bnc, "up_client_cert_ignore", certs, ignore=2, clientdir="clientdir",
180 caster_args=["--tls-client-ca", certs + "/ca.crt"])
181 check("upload: client certificate, sslIgnoreErrors", received(c) > 0,
182 "%d bytes received" % received(c))
183
184 c, log = download(bnc, "down_unknown_ca", certs)
185 check("download: unknown CA rejected", served(c) == 0 and "SSL Error" in log
186 and "ignored" not in log, "%d bytes served" % served(c))
187
188 c, log = download(bnc, "down_ca", certs, cadir="cadir")
189 check("download: CA in sslCaCertPath accepted", served(c) > 0 and "Get data" in log,
190 "%d bytes served" % served(c))
191
192 c, log = download(bnc, "down_ignore", certs, ignore=2)
193 check("download: sslIgnoreErrors, errors logged", served(c) > 0 and "Get data" in log
194 and "(ignored as configured)" in log, "%d bytes served" % served(c))
195
196 print("%d check(s) failed, details in %s" % (failed, os.path.relpath(WORK, ROOT)))
197 return 1 if failed else 0
198
199
200if __name__ == "__main__":
201 sys.exit(main())
Note: See TracBrowser for help on using the repository browser.