1#!/usr/bin/python
2
3"""
4(C) 2018,2019 Jack Lloyd
5
6Botan is released under the Simplified BSD License (see license.txt)
7"""
8
9import subprocess
10import sys
11import os
12import logging
13import optparse # pylint: disable=deprecated-module
14import time
15import shutil
16import tempfile
17import re
18import random
19import json
20import binascii
21import multiprocessing
22from multiprocessing.pool import ThreadPool
23
24# pylint: disable=global-statement,unused-argument
25
26CLI_PATH = None
27TESTS_RUN = 0
28TESTS_FAILED = 0
29
30class TestLogHandler(logging.StreamHandler, object):
31    def emit(self, record):
32        # Do the default stuff first
33        super(TestLogHandler, self).emit(record)
34        if record.levelno >= logging.ERROR:
35            global TESTS_FAILED
36            TESTS_FAILED += 1
37
38def setup_logging(options):
39    if options.verbose:
40        log_level = logging.DEBUG
41    elif options.quiet:
42        log_level = logging.WARNING
43    else:
44        log_level = logging.INFO
45
46    lh = TestLogHandler(sys.stdout)
47    lh.setFormatter(logging.Formatter('%(levelname) 7s: %(message)s'))
48    logging.getLogger().addHandler(lh)
49    logging.getLogger().setLevel(log_level)
50
51def random_port_number():
52    return random.randint(1024, 65535)
53
54def test_cli(cmd, cmd_options, expected_output=None, cmd_input=None, expected_stderr=None, use_drbg=True):
55    global TESTS_RUN
56
57    TESTS_RUN += 1
58
59    opt_list = []
60
61    if isinstance(cmd_options, str):
62        opt_list = cmd_options.split(' ')
63    elif isinstance(cmd_options, list):
64        opt_list = cmd_options
65
66    if use_drbg:
67        fixed_drbg_seed = "802" * 32
68        drbg_options = ['--rng-type=drbg', '--drbg-seed=' + fixed_drbg_seed]
69    else:
70        drbg_options = []
71
72    cmdline = [CLI_PATH, cmd] + drbg_options + opt_list
73
74    logging.debug("Executing '%s'" % (' '.join([CLI_PATH, cmd] + opt_list)))
75
76    stdout = None
77    stderr = None
78
79    if cmd_input is None:
80        proc = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
81        (stdout, stderr) = proc.communicate()
82    else:
83        proc = subprocess.Popen(cmdline, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
84        (stdout, stderr) = proc.communicate(cmd_input.encode())
85
86    if stderr:
87        if expected_stderr is None:
88            logging.error("Got output on stderr %s (stdout was %s)", stderr, stdout)
89        else:
90            if stderr != expected_stderr:
91                logging.error("Got output on stderr %s which did not match expected value %s", stderr, expected_stderr)
92    else:
93        if expected_stderr is not None:
94            logging.error('Expected output on stderr but got nothing')
95
96    output = stdout.decode('ascii').strip()
97
98    if expected_output is not None:
99        if output != expected_output:
100            logging.error("Got unexpected output running cmd %s %s", cmd, cmd_options)
101            logging.info("Output lengths %d vs expected %d", len(output), len(expected_output))
102            logging.info("Got %s", output)
103            logging.info("Exp %s", expected_output)
104
105    return output
106
107def check_for_command(cmd):
108    cmdline = [CLI_PATH, 'has_command', cmd]
109    proc = subprocess.Popen(cmdline)
110    proc.communicate()
111
112    return proc.returncode == 0
113
114def cli_config_tests(_tmp_dir):
115    prefix = test_cli("config", "prefix")
116    cflags = test_cli("config", "cflags")
117    ldflags = test_cli("config", "ldflags")
118    libs = test_cli("config", "libs")
119
120    if len(prefix) < 4 or prefix[0] != '/':
121        logging.error("Bad prefix %s" % (prefix))
122    if ("-I%s/include/botan-2" % (prefix)) not in cflags:
123        logging.error("Bad cflags %s" % (cflags))
124    if not ldflags.endswith(("-L%s/lib" % (prefix))):
125        logging.error("Bad ldflags %s" % (ldflags))
126    if "-lbotan-2" not in libs:
127        logging.error("Bad libs %s" % (libs))
128
129def cli_help_tests(_tmp_dir):
130    output = test_cli("help", None, None)
131
132    # Maybe test format somehow??
133    if len(output) < 500:
134        logging.error("Help output seems very short")
135
136def cli_version_tests(_tmp_dir):
137    output = test_cli("version", None, None)
138
139    version_re = re.compile(r'[0-9]\.[0-9]+\.[0-9]')
140    if not version_re.match(output):
141        logging.error("Unexpected version output %s" % (output))
142
143    output = test_cli("version", ["--full"], None, None)
144    version_full_re = re.compile(r'Botan [0-9]\.[0-9]+\.[0-9] \(.* revision .*, distribution .*\)$')
145    if not version_full_re.match(output):
146        logging.error("Unexpected version output %s" % (output))
147
148def cli_is_prime_tests(_tmp_dir):
149    test_cli("is_prime", "5", "5 is probably prime")
150    test_cli("is_prime", "9", "9 is composite")
151    test_cli("is_prime", "548950623407687320763", "548950623407687320763 is probably prime")
152
153def cli_gen_prime_tests(_tmp_dir):
154    test_cli("gen_prime", "64", "15568813029901363163")
155    test_cli("gen_prime", "128", "287193909494025008847286845478788766073")
156
157def cli_cycle_counter(_tmp_dir):
158    output = test_cli("cpu_clock", None, None)
159
160    if output.startswith('No CPU cycle counter on this machine'):
161        return
162
163    have_clock_re = re.compile(r'Estimated CPU clock [0-9\.]+ (M|G)Hz')
164
165    if have_clock_re.match(output):
166        return
167
168    logging.error('Unexpected output from cpu_clock: %s', output)
169
170def cli_entropy_tests(_tmp_dir):
171    output = test_cli("entropy", ["all"], None)
172
173    status_re = re.compile('Polling [a-z0-9_]+ gathered [0-9]+ bytes in [0-9]+ outputs with estimated entropy [0-9]+')
174    unavail_re = re.compile('Source [a-z0-9_]+ is unavailable')
175    comp_re = re.compile('Sample from [a-z0-9_]+ was .* compressed from [0-9]+ bytes to [0-9]+ bytes')
176    output_re = re.compile(r'[A-F0-9]+(...)?')
177
178    status_next = True
179
180    for line in output.split('\n'):
181        if comp_re.match(line):
182            continue
183
184        if status_next:
185            if status_re.match(line) is not None:
186                status_next = False
187            elif unavail_re.match(line) is not None:
188                pass
189            else:
190                logging.error('Unexpected status line %s', line)
191                status_next = False
192        else:
193            if output_re.match(line) is None:
194                logging.error('Unexpected sample line %s', line)
195            status_next = True
196
197def cli_factor_tests(_tmp_dir):
198    test_cli("factor", "97", "97: 97")
199    test_cli("factor", "9753893489562389", "9753893489562389: 21433 455087644733")
200    test_cli("factor", "12019502040659149507", "12019502040659149507: 3298628633 3643787579")
201
202def cli_mod_inverse_tests(_tmp_dir):
203    test_cli("mod_inverse", "97 802", "339")
204    test_cli("mod_inverse", "98 802", "0")
205
206def cli_base64_tests(_tmp_dir):
207    test_cli("base64_enc", "-", "YmVlcyE=", "bees!")
208    test_cli("base64_dec", "-", "bees!", "YmVlcyE=")
209
210def cli_base32_tests(_tmp_dir):
211    test_cli("base32_enc", "-", "MJSWK4ZB", "bees!")
212    test_cli("base32_dec", "-", "bees!", "MJSWK4ZB")
213
214def cli_base58_tests(_tmp_dir):
215    test_cli("base58_enc", "-", "C6sRAr4", "bees!")
216    test_cli("base58_dec", "-", "bees!", "C6sRAr4")
217
218    test_cli("base58_enc", ["--check", "-"], "Cjv15cdjaBc", "F00F")
219    test_cli("base58_dec", ["--check", "-"], "F00F", "Cjv15cdjaBc")
220
221def cli_hex_tests(_tmp_dir):
222    test_cli("hex_enc", "-", "6265657321", "bees!")
223    test_cli("hex_dec", "-", "bees!", "6265657321")
224
225def cli_hash_tests(_tmp_dir):
226    test_cli("hash", "--algo=SHA-256",
227             "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 -", "")
228
229    test_cli("hash", "--algo=SHA-256",
230             "BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD -", "abc")
231
232    test_cli("hash", ["--algo=SHA-256", "--format=base64"],
233             "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0= -", "abc")
234
235    test_cli("hash", ["--algo=SHA-224", "--format=base58", "--no-fsname"],
236             "MuGc8HkSVyJjfMjPM5UQikPToBTzNucEghcGLe", "abc")
237
238    test_cli("hash", ["--algo=SHA-224", "--format=base58check", "--no-fsname"],
239             "3MmfMqgrhemdVa9bDAGfooukbviWtKMBx2xauL2RsyAe", "abc")
240
241def cli_hmac_tests(tmp_dir):
242    key_file = os.path.join(tmp_dir, 'hmac.key')
243
244    test_cli("rng", ["64", "--output=%s" % (key_file)], "")
245
246    test_cli("hmac", ["--no-fsname", "--hash=SHA-384", key_file, key_file],
247             "E3A8529377030B28A7DBDFC50DDEC8E4ECEFB6EA850D95EB785938CD3E3AFEF9EF8B08AF219C1496633193468AB755CB")
248
249def cli_bcrypt_tests(_tmp_dir):
250    test_cli("gen_bcrypt", "--work-factor=4 s3kr1t",
251             "$2a$04$0.8G7o08XYwvBBWA3l0WUujtwoGZgGDzVSN8fNkNqXikcK4A3lHPS")
252
253    test_cli("check_bcrypt", "s3kr1t $2a$04$gHX4Qg7pDSJuXiPXnmt8leyb.FFzX1Bv4rXwIj2cPSakJ8zNnhIka",
254             "Password is valid")
255
256    test_cli("check_bcrypt", "santa $2a$04$gHX4Qg7pDSJuXiPXnmt8leyb.FFzX1Bv4rXwIj2cPSakJ8zNnhIka",
257             "Password is NOT valid")
258
259def cli_argon2_tests(_tmp_dir):
260    password = "s3kr1t"
261    expected = "$argon2id$v=19$m=8,t=1,p=1$2A+I9q2+ZayxDDYC5n2YWw$/Lhx+Jbtlpw+Kxpskfv7+AKhBL/5ebalTJkVC1O5+1E"
262    test_cli("gen_argon2", ['--mem=8', password], expected)
263    test_cli("gen_argon2", ['--mem=8', '--t=1', password], expected)
264    test_cli("gen_argon2", ['--mem=8', '--t=1', '--p=1', password], expected)
265
266    test_cli("check_argon2", [password, expected], "Password is valid")
267    test_cli("check_argon2", ["guessing", expected], "Password is NOT valid")
268
269def cli_gen_dl_group_tests(_tmp_dir):
270
271    pem = """-----BEGIN X9.42 DH PARAMETERS-----
272MIIBJAKBgwTw7LQiLkXJsrgMVQxTPlWaQlYz/raZ+5RtIZe4YluQgRQGPFADLZ/t
273TOYzuIzZJFOcdKtEtrVkxZRGSkjZwKFKLUD6fzSjoC2M2EHktK/y5HsvxBxL4tKr
274q1ffbyPQi+iBLYTZAXygvxj2vWyrvA+/w4nbt1fStCHTDhWjLWqFpV9nAoGDAKzA
275HUu/IRl7OiUtW/dz36gzEJnaYtz4ZtJl0FG8RJiOe02lD8myqW2sVzYqMvKD0LGx
276x9fdSKC1G+aZ/NWtqrQjb66Daf7b0ddDx+bfWTWJ2dOtZd8IL2rmQQJm+JogDi9i
277huVYFicDNQGzi+nEKAzrZ1L/VxtiSiw/qw0IyOuVtz8CFjgPiPatvmWssQw2AuZ9
278mFvAZ/8wal0=
279-----END X9.42 DH PARAMETERS-----"""
280
281    test_cli("gen_dl_group", "--pbits=1043", pem)
282
283    dsa_grp = """-----BEGIN X9.42 DH PARAMETERS-----
284MIIBHgKBgQCyP1vosC/axliM2hmJ9EOSdd1zBkuzMP25CYD8PFkRVrPLr1ClSUtn
285eXTIsHToJ7d7sRwtidQGW9BrvUEyiAWE06W/wnLPxB3/g2/l/P2EhbNmNHAO7rV7
286ZVz/uKR4Xcvzxg9uk5MpT1VsxA8H6VEwzefNF1Rya92rqGgBTNT3/wKBgC7HLL8A
287Gu3tqJxTk1iNgojjOiSreLn6ihA8R8kQnRXDTNtDKz996KHGInfMBurUI1zPM3xq
288bHc0CvU1Nf87enhPIretzJcFgiCWrNFUIC25zPEjp0s3/ERHT4Bi1TABZ3j6YUEQ
289fnnj+9XriKKHf2WtX0T4FXorvnKq30m934rzAhUAvwhWDK3yZEmphc7dwl4/J3Zp
290+MU=
291-----END X9.42 DH PARAMETERS-----"""
292
293    test_cli("gen_dl_group", ["--type=dsa", "--pbits=1024"], dsa_grp)
294
295
296def cli_key_tests(tmp_dir):
297
298    pem = """-----BEGIN PRIVATE KEY-----
299MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQg2A+I9q2+ZayxDDYC5n2Y
300W8Bn/zBm4D3mwS5qMwADRDehRANCAATwnDFqsjXL9SD/Rr1Vy4pb79PswXdQNZBN
301mlLtJ5JvZ0/p6zP3x+Y9yPIrAR8L/acG5ItSrAKXzzuqQQZMv4aN
302-----END PRIVATE KEY-----"""
303
304    priv_key = os.path.join(tmp_dir, 'priv.pem')
305    pub_key = os.path.join(tmp_dir, 'pub.pem')
306    pub_der_key = os.path.join(tmp_dir, 'pub.der')
307    enc_pem = os.path.join(tmp_dir, 'priv_enc.pem')
308    enc_der = os.path.join(tmp_dir, 'priv_enc.der')
309    ca_cert = os.path.join(tmp_dir, 'ca.crt')
310    crt_req = os.path.join(tmp_dir, 'crt.req')
311    user_cert = os.path.join(tmp_dir, 'user.crt')
312
313    test_cli("keygen", ["--algo=ECDSA", "--params=secp256k1"], pem)
314
315    test_cli("keygen", ["--algo=ECDSA", "--params=secp256r1", "--output=" + priv_key], "")
316
317    test_cli("pkcs8", "--pub-out --output=%s %s" % (pub_key, priv_key), "")
318    test_cli("pkcs8", "--pub-out --der-out --output=%s %s" % (pub_der_key, priv_key), "")
319
320    test_cli("pkcs8", "--pass-out=foof --der-out --output=%s %s" % (enc_der, priv_key), "")
321    test_cli("pkcs8", "--pass-out=foof --output=%s %s" % (enc_pem, priv_key), "")
322
323    dec_pem = test_cli("pkcs8", ["--pass-in=foof", enc_pem], None)
324    dec_der = test_cli("pkcs8", ["--pass-in=foof", enc_der], None)
325
326    if dec_pem != dec_der:
327        logging.error("Problem decrypting PKCS8 key")
328
329    test_cli("fingerprint", ['--no-fsname', pub_key],
330             "83:FC:67:87:30:C7:0C:9C:54:9A:E7:A1:FA:25:83:4C:77:A4:43:16:33:6D:47:3C:CE:4B:91:62:30:97:62:D4")
331
332    test_cli("fingerprint", ['--no-fsname', pub_der_key],
333             "83:FC:67:87:30:C7:0C:9C:54:9A:E7:A1:FA:25:83:4C:77:A4:43:16:33:6D:47:3C:CE:4B:91:62:30:97:62:D4")
334
335    test_cli("fingerprint", ['--no-fsname', pub_key, pub_der_key],
336             "83:FC:67:87:30:C7:0C:9C:54:9A:E7:A1:FA:25:83:4C:77:A4:43:16:33:6D:47:3C:CE:4B:91:62:30:97:62:D4\n"
337             "83:FC:67:87:30:C7:0C:9C:54:9A:E7:A1:FA:25:83:4C:77:A4:43:16:33:6D:47:3C:CE:4B:91:62:30:97:62:D4")
338
339    test_cli("fingerprint", [pub_der_key],
340             pub_der_key +
341             ": 83:FC:67:87:30:C7:0C:9C:54:9A:E7:A1:FA:25:83:4C:77:A4:43:16:33:6D:47:3C:CE:4B:91:62:30:97:62:D4")
342
343    test_cli("fingerprint", ['-'],
344             "83:FC:67:87:30:C7:0C:9C:54:9A:E7:A1:FA:25:83:4C:77:A4:43:16:33:6D:47:3C:CE:4B:91:62:30:97:62:D4",
345             open(pub_key, 'rb').read().decode())
346
347    valid_sig = "nI4mI1ec14Y7nYUWs2edysAVvkob0TWpmGh5rrYWDA+/W9Fj0ZM21qJw8qa3/avAOIVBO6hoMEVmfJYXlS+ReA=="
348
349    test_cli("sign", "--provider=base %s %s" % (priv_key, pub_key), valid_sig)
350
351    test_cli("verify", [pub_key, pub_key, '-'],
352             "Signature is valid", valid_sig)
353
354    test_cli("verify", [pub_key, pub_key, '-'],
355             "Signature is invalid",
356             valid_sig.replace("G", "H"))
357
358    test_cli("gen_self_signed",
359             [priv_key, "CA", "--ca", "--country=VT",
360              "--dns=ca.example", "--hash=SHA-384", "--output="+ca_cert],
361             "")
362
363    test_cli("cert_verify", ca_cert, "Certificate did not validate - Cannot establish trust")
364
365    cert_info = test_cli("cert_info", ['--fingerprint', ca_cert], None)
366
367    if cert_info.find('Subject: CN="CA",C="VT"') < 0:
368        logging.error('Unexpected output for cert_info command %s', cert_info)
369    if cert_info.find('Subject keyid: 69DD911C9EEE3400C67CBC3F3056CBE711BD56AF9495013F') < 0:
370        logging.error('Unexpected output for cert_info command %s', cert_info)
371
372    test_cli("gen_pkcs10", "%s User --output=%s" % (priv_key, crt_req))
373
374    test_cli("sign_cert", "%s %s %s --output=%s" % (ca_cert, priv_key, crt_req, user_cert))
375
376    test_cli("cert_verify", [user_cert, ca_cert],
377             "Certificate passes validation checks")
378
379    test_cli("cert_verify", user_cert,
380             "Certificate did not validate - Certificate issuer not found")
381
382def cli_xmss_sign_tests(tmp_dir):
383    priv_key = os.path.join(tmp_dir, 'priv.pem')
384    pub_key = os.path.join(tmp_dir, 'pub.pem')
385    pub_key2 = os.path.join(tmp_dir, 'pub2.pem')
386    msg = os.path.join(tmp_dir, 'input')
387    sig1 = os.path.join(tmp_dir, 'sig1')
388    sig2 = os.path.join(tmp_dir, 'sig2')
389
390    test_cli("rng", ['--output=%s' % (msg)], "")
391    test_cli("hash", ["--no-fsname", msg], "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855")
392
393    test_cli("keygen", ["--algo=XMSS", "--output=%s" % (priv_key)], "")
394    test_cli("hash", ["--no-fsname", priv_key], "5B38F737BA41BE7F40433DB30EAEF7C41ABB0F7D9E7A09DEB5FDCE7B6811693F")
395
396    test_cli("pkcs8", "--pub-out --output=%s %s" % (pub_key, priv_key), "")
397    test_cli("fingerprint", ['--no-fsname', pub_key],
398             "B0:F4:98:6E:D8:4E:05:63:A1:D8:4B:37:61:5A:A0:41:78:7E:DE:0E:72:46:E0:A8:D6:CF:09:54:08:DA:A4:22")
399
400    # verify the key is updated after each signature:
401    test_cli("sign", [priv_key, msg, "--output=%s" % (sig1)], "")
402    test_cli("verify", [pub_key, msg, sig1], "Signature is valid")
403    test_cli("hash", ["--no-fsname", sig1], "04AF45451C7A9AF2D828E1AD6EC262E012436F4087C5DA6F32C689D781E597D0")
404    test_cli("hash", ["--no-fsname", priv_key], "67929FAEC636E43DE828C1CD7E2D11CE7C3388CE90DD0A0F687C6627FFA850CD")
405
406    test_cli("sign", [priv_key, msg, "--output=%s" % (sig2)], "")
407    test_cli("verify", [pub_key, msg, sig2], "Signature is valid")
408    test_cli("hash", ["--no-fsname", sig2], "0785A6AD54CC7D01F2BE2BC6463A3EAA1159792E52210ED754992C5068E8F24F")
409    test_cli("hash", ["--no-fsname", priv_key], "1940945D68B1CF54D79E05DD7913A4D0B4959183F1E12B81A4E43EF4E63FBD20")
410
411    # private key updates, public key is unchanged:
412    test_cli("pkcs8", "--pub-out --output=%s %s" % (pub_key2, priv_key), "")
413    test_cli("fingerprint", ['--no-fsname', pub_key2],
414             "B0:F4:98:6E:D8:4E:05:63:A1:D8:4B:37:61:5A:A0:41:78:7E:DE:0E:72:46:E0:A8:D6:CF:09:54:08:DA:A4:22")
415
416def cli_pbkdf_tune_tests(_tmp_dir):
417    if not check_for_command("pbkdf_tune"):
418        return
419
420    expected = re.compile(r'For (default|[1-9][0-9]*) ms selected Scrypt\([0-9]+,[0-9]+,[0-9]+\) using [0-9]+ MiB')
421
422    output = test_cli("pbkdf_tune", ["--check", "1", "10", "50", "default"], None).split('\n')
423
424    for line in output:
425        if expected.match(line) is None:
426            logging.error("Unexpected line '%s'" % (line))
427
428    expected_pbkdf2 = re.compile(r'For (default|[1-9][0-9]*) ms selected PBKDF2\(HMAC\(SHA-256\),[0-9]+\)')
429
430    output = test_cli("pbkdf_tune", ["--algo=PBKDF2(SHA-256)", "--check", "1", "10", "50", "default"], None).split('\n')
431
432    for line in output:
433        if expected_pbkdf2.match(line) is None:
434            logging.error("Unexpected line '%s'" % (line))
435
436    expected_argon2 = re.compile(r'For (default|[1-9][0-9]*) ms selected Argon2id\([0-9]+,[0-9]+,[0-9]+\)')
437
438    output = test_cli("pbkdf_tune", ["--algo=Argon2id", "--check", "1", "10", "50", "default"], None).split('\n')
439
440    for line in output:
441        if expected_argon2.match(line) is None:
442            logging.error("Unexpected line '%s'" % (line))
443
444def cli_psk_db_tests(tmp_dir):
445    if not check_for_command("psk_get"):
446        return
447
448    psk_db = os.path.join(tmp_dir, 'psk.db')
449    db_key1 = "909"*32
450    db_key2 = "451"*32
451
452    test_cli("psk_set", [psk_db, db_key1, "name", "F00FEE"], "")
453    test_cli("psk_set", [psk_db, db_key2, "name", "C00FEE11"], "")
454    test_cli("psk_set", [psk_db, db_key1, "name2", "50051029"], "")
455
456    test_cli("psk_get", [psk_db, db_key1, "name"], "F00FEE")
457    test_cli("psk_get", [psk_db, db_key2, "name"], "C00FEE11")
458
459    test_cli("psk_list", [psk_db, db_key1], "name\nname2")
460    test_cli("psk_list", [psk_db, db_key2], "name")
461
462def cli_compress_tests(tmp_dir):
463
464    if not check_for_command("compress"):
465        return
466
467    input_file = os.path.join(tmp_dir, 'input.txt')
468    output_file = os.path.join(tmp_dir, 'input.txt.gz')
469
470    with open(input_file, 'w') as f:
471        f.write("hi there")
472        f.close()
473
474    test_cli("compress", input_file)
475
476    if not os.access(output_file, os.R_OK):
477        logging.error("Compression did not created expected output file")
478
479    is_py3 = sys.version_info[0] == 3
480
481    output_hdr = open(output_file, 'rb').read(2)
482
483    if is_py3:
484        if output_hdr[0] != 0x1F or output_hdr[1] != 0x8B:
485            logging.error("Did not see expected gzip header")
486    else:
487        if ord(output_hdr[0]) != 0x1F or ord(output_hdr[1]) != 0x8B:
488            logging.error("Did not see expected gzip header")
489
490    os.unlink(input_file)
491
492    test_cli("decompress", output_file)
493
494    if not os.access(input_file, os.R_OK):
495        logging.error("Decompression did not created expected output file")
496
497    recovered = open(input_file).read()
498    if recovered != "hi there":
499        logging.error("Decompression did not recover original input")
500
501def cli_rng_tests(_tmp_dir):
502    test_cli("rng", "10", "D80F88F6ADBE65ACB10C")
503    test_cli("rng", "16", "D80F88F6ADBE65ACB10C3602E67D985B")
504    test_cli("rng", "10 6", "D80F88F6ADBE65ACB10C\n1B119CC068AF")
505
506    test_cli("rng", ['--format=base64', '10'], "2A+I9q2+ZayxDA==")
507    test_cli("rng", ['--format=base58', '10'], "D93XRyVfxqs7oR")
508    test_cli("rng", ['--format=base58check', '10'], "2NS1jYUq92TyGFVnhVLa")
509
510    hex_10 = re.compile('[A-F0-9]{20}')
511
512    for rng in ['system', 'auto', 'entropy']:
513        output = test_cli("rng", ["10", '--%s' % (rng)], use_drbg=False)
514        if output == "D80F88F6ADBE65ACB10C":
515            logging.error('RNG produced DRBG output')
516        if hex_10.match(output) is None:
517            logging.error('Unexpected RNG output %s' % (output))
518
519    has_rdrand = test_cli("cpuid", []).find(' rdrand ') > 0
520
521    if has_rdrand:
522        output = test_cli("rng", ["10", '--rdrand'], use_drbg=False)
523
524        if output == "D80F88F6ADBE65ACB10C":
525            logging.error('RDRAND produced DRBG output')
526        if hex_10.match(output) is None:
527            logging.error('Unexpected RNG output %s' % (output))
528
529def cli_roughtime_check_tests(tmp_dir):
530    # pylint: disable=line-too-long
531    if not check_for_command("roughtime_check"):
532        return
533    chain = os.path.join(tmp_dir, 'roughtime-chain')
534
535    with open(chain, 'w') as f:
536        f.write("""\
537ed25519 bbT+RPS7zKX6w71ssPibzmwWqU9ffRV5oj2OresSmhE= eu9yhsJfVfguVSqGZdE8WKIxaBBM0ZG3Vmuc+IyZmG2YVmrIktUByDdwIFw6F4rZqmSFsBO85ljoVPz5bVPCOw== BQAAAEAAAABAAAAApAAAADwBAABTSUcAUEFUSFNSRVBDRVJUSU5EWBnGOEajOwPA6G7oL47seBP4C7eEpr57H43C2/fK/kMA0UGZVUdf4KNX8oxOK6JIcsbVk8qhghTwA70qtwpYmQkDAAAABAAAAAwAAABSQURJTUlEUFJPT1RAQg8AJrA8tEqPBQAqisiuAxgy2Pj7UJAiWbCdzGz1xcCnja3T+AqhC8fwpeIwW4GPy/vEb/awXW2DgSLKJfzWIAz+2lsR7t4UjNPvAgAAAEAAAABTSUcAREVMRes9Ch4X0HIw5KdOTB8xK4VDFSJBD/G9t7Et/CU7UW61OiTBXYYQTG2JekWZmGa0OHX1JPGG+APkpbsNw0BKUgYDAAAAIAAAACgAAABQVUJLTUlOVE1BWFR/9BWjpsWTQ1f6iUJea3EfZ1MkX3ftJiV3ABqNLpncFwAAAAAAAAAA//////////8AAAAA
538ed25519 gD63hSj3ScS+wuOeGrubXlq35N1c5Lby/S+T7MNTjxo= uLeTON9D+2HqJMzK6sYWLNDEdtBl9t/9yw1cVAOm0/sONH5Oqdq9dVPkC9syjuWbglCiCPVF+FbOtcxCkrgMmA== BQAAAEAAAABAAAAApAAAADwBAABTSUcAUEFUSFNSRVBDRVJUSU5EWOw1jl0uSiBEH9HE8/6r7zxoSc01f48vw+UzH8+VJoPelnvVJBj4lnH8uRLh5Aw0i4Du7XM1dp2u0r/I5PzhMQoDAAAABAAAAAwAAABSQURJTUlEUFJPT1RAQg8AUBo+tEqPBQC47l77to7ESFTVhlw1SC74P5ssx6gpuJ6eP+1916GuUiySGE/x3Fp0c3otUGAdsRQou5p9PDTeane/YEeVq4/8AgAAAEAAAABTSUcAREVMRe5T1ml8wHyWAcEtHP/U5Rg/jFXTEXOSglngSa4aI/CECVdy4ZNWeP6vv+2//ZW7lQsrWo7ZkXpvm9BdBONRSQIDAAAAIAAAACgAAABQVUJLTUlOVE1BWFQpXlenV0OfVisvp9jDHXLw8vymZVK9Pgw9k6Edf8ZEhUgSGEc5jwUASHLvZE2PBQAAAAAA
539ed25519 etPaaIxcBMY1oUeGpwvPMCJMwlRVNxv51KK/tktoJTQ= U53wX99JzZwy4BXa9C6R04bPu4yqFB5w5/wTgG8Mw5wm+VLrY70ECxJ9ZHnpdHVHaLEU3aeLnQFZyZPRAEOCyw== BQAAAEAAAABAAAAApAAAADwBAABTSUcAUEFUSFNSRVBDRVJUSU5EWMh3mPWCCbOlX8xDWbU9qdfKoReJX/XLsivom8bJJYmcC7T03tyXrtWUheEJweHtg4qMgSyifQS1MjHJSy1jPAsDAAAABAAAAAwAAABSQURJTUlEUFJPT1RAQg8Akxw/tEqPBQBfOsOuciR7jiAW5itQ39y8yVr/ZJmgMwvTjqaU4/wA05ZqG4RqoLdvDXh5bCNySL6LrrnBNSAHwn5COt0CItNuAgAAAEAAAABTSUcAREVMRVP3BIOzsZmuxqMi+ScIBPyKtzFfK7ZlPFNP0JrNwln2QYtAcQFIKywDdNAAL+n8i3dz1p99K50FJjCkCl2J6AMDAAAAIAAAACgAAABQVUJLTUlOVE1BWFQKC/kZVdjiNT2NCSGfnpot4eqipyMFsyMjiIQmqqqXqQCAa245jwUAAGCgA56PBQAAAAAA
540ed25519 AW5uAoTSTDfG5NfY1bTh08GUnOqlRb+HVhbJ3ODJvsE= IcZcXFuaLKYYhWcK3sT/6PrVeXMmabCRbf9hvVfkMkqEW1PFL++ZnHJ1/m+G8azITxvktwsfP1YAOOxWdbf9XQ== BQAAAEAAAABAAAAApAAAADwBAABTSUcAUEFUSFNSRVBDRVJUSU5EWL5DAl8GPNUQ/mSXl0tI4N9yZAO+PiXTodJOTDL+WU/x26iqgyyQRikSSocRMzAEVLDGasdyW19mVC6H/6vfXggDAAAABAAAAAwAAABSQURJTUlEUFJPT1RAQg8Av/JAtEqPBQBIP346SHhCdDfughzeH+uYSbxngDYxqHzBDtZt0obUKrzxfRWzD1oR61B1reLvoPVCKSfzEngi/g1NSQjTrzNMAgAAAEAAAABTSUcAREVMRTQLLplQv0rN4p77Bo59qT8bbquV6MKSwILI/Tw2LLGo9noaZegUFmM+rNu1d1AVOEVQ01j6/2xDmBvp0d6MZgEDAAAAIAAAACgAAABQVUJLTUlOVE1BWFS4a1dYoIB5u/zkbR3sIteuhVrQkszzj+Gng9ywo6O9VgAAAAAAAAAA//////////8AAAAA
541ed25519 cj8GsiNlRkqiDElAeNMSBBMwrAl15hYPgX50+GWX/lA= Tsy82BBU2xxVqNe1ip11OyEGoKWhKoSggWjBmDTSBmKbTs7bPPCEidYc5TQ23sQUWe62G35fQOVU28q+Eq5uhQ== BQAAAEAAAABAAAAApAAAADwBAABTSUcAUEFUSFNSRVBDRVJUSU5EWDAmi7zgXAqLgQXVfbjeqnUZRiXCZI64QIoAKFL83CQHbyXgB4cNwHfQ9mSg0hYxTp1M8QxOuzusnUpk05DIRwwDAAAABAAAAAwAAABSQURJTUlEUFJPT1RAQg8AcOBCtEqPBQBhsr1mKOxxCf4VDFzAtYB4Nhs332AN1LrJU/8+VqktzfPd2R7awJHEVEWugvSvOrr+9d332mQObAkYfKfDtbSFAgAAAEAAAABTSUcAREVMRUjnhDvkIjFzTEYtgHOfMpRHtnNZj4P31RFtapkwzGjOtc93pYDd7zqQCw2AVcfbSnPqa8k26z96Q9fVRzq0pw8DAAAAIAAAACgAAABQVUJLTUlOVE1BWFR7qp2oerjpbN8Y23nUGARIlsgkodW4owH29ZKhxDMn8AAAAAAAAAAA//////////8AAAAA
542""")
543
544    test_cli("roughtime_check", chain, """\
5451: UTC 2019-08-04T13:38:17 (+-1000000us)
546  2: UTC 2019-08-04T13:38:17 (+-1000000us)
547  3: UTC 2019-08-04T13:38:17 (+-1000000us)
548  4: UTC 2019-08-04T13:38:18 (+-1000000us)
549  5: UTC 2019-08-04T13:38:18 (+-1000000us)""")
550
551    with open(chain, 'w') as f:
552        f.write("ed25519 bbT+RPS7zKX6w71ssPibzmwWqU9ffRV5oj2OresSmhE= eu9yhsJfVfguVSqGZdE8WKIxaBBM0ZG3Vmuc+IyZmG2YVmrIktUByDdwIFw6F4rZqmSFsBO85ljoVPz5bVPCOw== BQAAAEAAAABAAAAApAAAADwBAABTSUcAUEFUSFNSRVBDRVJUSU5EWBnGOEajOwPA6G7oL47seBP4C7eEpr57H43C2/fK/kMA0UGZVUdf4KNX8oxOK6JIcsbVk8qhghTwA70qtwpYmQkDAAAABAAAAAwAAABSQURJTUlEUFJPT1RAQg8AJrA8tEqPBQAqisiuAxgy2Pj7UJAiWbCdzGz1xcCnja3T+AqhC8fwpeIwW4GPy/vEb/awXW2DgSLKJfzWIAz+2lsR7t4UjNPvAgAAAEAAAABTSUcAREVMRes9Ch4X0HIw5KdOTB8xK4VDFSJBD/G9t7Et/CU7UW61OiTBXYYQTG2JekWZmGa0OHX1JPGG+APkpbsNw0BKUgYDAAAAIAAAACgAAABQVUJLTUlOVE1BWFR/9BWjpsWTQ1f6iUJea3EfZ1MkX3ftJiV3ABqNLpncFwAAAAAAAAAA//////////8AAAAA")
553    test_cli("roughtime_check", [chain, "--raw-time"], "1: UTC 1564925897781286 (+-1000000us)")
554
555    with open(chain, 'w') as f:
556        f.write("ed25519 cbT+RPS7zKX6w71ssPibzmwWqU9ffRV5oj2OresSmhE= eu9yhsJfVfguVSqGZdE8WKIxaBBM0ZG3Vmuc+IyZmG2YVmrIktUByDdwIFw6F4rZqmSFsBO85ljoVPz5bVPCOw== BQAAAEAAAABAAAAApAAAADwBAABTSUcAUEFUSFNSRVBDRVJUSU5EWBnGOEajOwPA6G7oL47seBP4C7eEpr57H43C2/fK/kMA0UGZVUdf4KNX8oxOK6JIcsbVk8qhghTwA70qtwpYmQkDAAAABAAAAAwAAABSQURJTUlEUFJPT1RAQg8AJrA8tEqPBQAqisiuAxgy2Pj7UJAiWbCdzGz1xcCnja3T+AqhC8fwpeIwW4GPy/vEb/awXW2DgSLKJfzWIAz+2lsR7t4UjNPvAgAAAEAAAABTSUcAREVMRes9Ch4X0HIw5KdOTB8xK4VDFSJBD/G9t7Et/CU7UW61OiTBXYYQTG2JekWZmGa0OHX1JPGG+APkpbsNw0BKUgYDAAAAIAAAACgAAABQVUJLTUlOVE1BWFR/9BWjpsWTQ1f6iUJea3EfZ1MkX3ftJiV3ABqNLpncFwAAAAAAAAAA//////////8AAAAA")
557    test_cli("roughtime_check", chain, expected_stderr=b'Error: Roughtime Invalid signature or public key\n')
558
559def cli_roughtime_tests(tmp_dir):
560    # pylint: disable=line-too-long
561    # pylint: disable=too-many-locals
562    import socket
563    import base64
564    import threading
565
566    if not check_for_command("roughtime"):
567        return
568
569    server_port = random_port_number()
570    chain_file = os.path.join(tmp_dir, 'roughtime-chain')
571    ecosystem = os.path.join(tmp_dir, 'ecosystem')
572
573    def run_udp_server():
574        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
575        server_address = ('127.0.0.1', server_port)
576        sock.bind(server_address)
577
578        while True:
579            data, address = sock.recvfrom(4096)
580
581            if data:
582                if data != base64.b64decode(server_request):
583                    logging.error("unexpected request")
584
585                sock.sendto(base64.b64decode(server_response), address)
586
587    udp_thread = threading.Thread(target=run_udp_server)
588    udp_thread.daemon = True
589    udp_thread.start()
590
591    chain = [
592        """\
593ed25519 gD63hSj3ScS+wuOeGrubXlq35N1c5Lby/S+T7MNTjxo= 2A+I9q2+ZayxDDYC5n2YW8Bn/zBm4D3mwS5qMwADRDcbFpBcf3yPOyeZiqpLBTkxo8GT8zMQFeApv4ScffjC8A== BQAAAEAAAABAAAAApAAAADwBAABTSUcAUEFUSFNSRVBDRVJUSU5EWDwlo/AkUnTrecAW4Ci5Tkh3KOqs6R7KLTsFtq16RXN5F7G5ckGv11UtzHoZTbKbEk03a6ogAOK54Q2CI/7XGA8DAAAABAAAAAwAAABSQURJTUlEUFJPT1RAQg8AWDLihlaSBQAoq/5gEjRCrhfH16X2GYjQJSG/CgSuGhYeCsrw7XkphLI3cxw2unJRDW8DAJrYqEGaW0NPKZk7bbpPjU/Q6Es1AgAAAEAAAABTSUcAREVMRUJbs67Sb5Wx/jzWyT1PhWR0c4kg59tjSGofo8R3eHzcA9CGwavuRdxOArhVWWODG99gYgfmjcRLgt9/jH+99w4DAAAAIAAAACgAAABQVUJLTUlOVE1BWFRXRfQ1RHLWGOgqABUTYfVBDZrv3OL2nPLYve9ldfNVLOjdPVFFkgUA6D0Vb1mSBQAAAAAA
594""",
595        """\
596ed25519 gD63hSj3ScS+wuOeGrubXlq35N1c5Lby/S+T7MNTjxo= 2A+I9q2+ZayxDDYC5n2YW8Bn/zBm4D3mwS5qMwADRDcbFpBcf3yPOyeZiqpLBTkxo8GT8zMQFeApv4ScffjC8A== BQAAAEAAAABAAAAApAAAADwBAABTSUcAUEFUSFNSRVBDRVJUSU5EWDwlo/AkUnTrecAW4Ci5Tkh3KOqs6R7KLTsFtq16RXN5F7G5ckGv11UtzHoZTbKbEk03a6ogAOK54Q2CI/7XGA8DAAAABAAAAAwAAABSQURJTUlEUFJPT1RAQg8AWDLihlaSBQAoq/5gEjRCrhfH16X2GYjQJSG/CgSuGhYeCsrw7XkphLI3cxw2unJRDW8DAJrYqEGaW0NPKZk7bbpPjU/Q6Es1AgAAAEAAAABTSUcAREVMRUJbs67Sb5Wx/jzWyT1PhWR0c4kg59tjSGofo8R3eHzcA9CGwavuRdxOArhVWWODG99gYgfmjcRLgt9/jH+99w4DAAAAIAAAACgAAABQVUJLTUlOVE1BWFRXRfQ1RHLWGOgqABUTYfVBDZrv3OL2nPLYve9ldfNVLOjdPVFFkgUA6D0Vb1mSBQAAAAAA
597ed25519 gD63hSj3ScS+wuOeGrubXlq35N1c5Lby/S+T7MNTjxo= 2A+I9q2+ZayxDDYC5n2YW8Bn/zBm4D3mwS5qMwADRDcbFpBcf3yPOyeZiqpLBTkxo8GT8zMQFeApv4ScffjC8A== BQAAAEAAAABAAAAApAAAADwBAABTSUcAUEFUSFNSRVBDRVJUSU5EWHH5Ofs4HciIFXjE9egjDbistJptoMXIC7ugCgHhI4NPJqfYY256NpULXKc9c30ul7oHXQyKLfGd84mIAxC3UwQDAAAABAAAAAwAAABSQURJTUlEUFJPT1RAQg8AuOoUh1aSBQANeC4gGGG3a23PpmF+y6CrUS9VWjyj0Ydpl2tMVDLaK2vd5QtYKKJ3UOyprGKk0D/aPn4E3Bk2rE3BKBZRXM1AAgAAAEAAAABTSUcAREVMRci9uvioJssgd8txxFlqz9RqPx+YLVMkHmm24fMUtYGWF/nhkoEYVGT7O+tXSfHHY/KHcUZjVaZpEt/tmXlXBAUDAAAAIAAAACgAAABQVUJLTUlOVE1BWFSxhKhavdriTvCAtNVcK5yr0cAbsWp2MsrwUV5YTc+7V0CsaLZSkgUAQAxA1GaSBQAAAAAA
598""",
599        """\
600ed25519 gD63hSj3ScS+wuOeGrubXlq35N1c5Lby/S+T7MNTjxo= SbWKPilWYrt+1vgFU3jlxGNOH6I/1npX8wl+KoraN3S6VDsyM6EfCV+JPEK8BsNoM2VIpMcSdjcVna/GwXwZkg== BQAAAEAAAABAAAAApAAAADwBAABTSUcAUEFUSFNSRVBDRVJUSU5EWHH5Ofs4HciIFXjE9egjDbistJptoMXIC7ugCgHhI4NPJqfYY256NpULXKc9c30ul7oHXQyKLfGd84mIAxC3UwQDAAAABAAAAAwAAABSQURJTUlEUFJPT1RAQg8AuOoUh1aSBQANeC4gGGG3a23PpmF+y6CrUS9VWjyj0Ydpl2tMVDLaK2vd5QtYKKJ3UOyprGKk0D/aPn4E3Bk2rE3BKBZRXM1AAgAAAEAAAABTSUcAREVMRci9uvioJssgd8txxFlqz9RqPx+YLVMkHmm24fMUtYGWF/nhkoEYVGT7O+tXSfHHY/KHcUZjVaZpEt/tmXlXBAUDAAAAIAAAACgAAABQVUJLTUlOVE1BWFSxhKhavdriTvCAtNVcK5yr0cAbsWp2MsrwUV5YTc+7V0CsaLZSkgUAQAxA1GaSBQAAAAAA
601ed25519 gD63hSj3ScS+wuOeGrubXlq35N1c5Lby/S+T7MNTjxo= 2A+I9q2+ZayxDDYC5n2YW8Bn/zBm4D3mwS5qMwADRDcbFpBcf3yPOyeZiqpLBTkxo8GT8zMQFeApv4ScffjC8A== BQAAAEAAAABAAAAApAAAADwBAABTSUcAUEFUSFNSRVBDRVJUSU5EWN5Y0b2irPS1JgqJFQMciPg4aWd9qj1ZqcJc5bGXe1m4ZdAXa5OIhXa0+680MgpyhEHhqYJDIwH1XRa1OZx5YAUDAAAABAAAAAwAAABSQURJTUlEUFJPT1RAQg8AgBW3iFaSBQD9WI+Qr6NOZsDmP0PsnCo66mstM3ac5ZON+I+ZeEK8lZWBASvsD2JIfq3v4d1QH5g4STs3wOazQPc25Puy659ZAgAAAEAAAABTSUcAREVMRUJbs67Sb5Wx/jzWyT1PhWR0c4kg59tjSGofo8R3eHzcA9CGwavuRdxOArhVWWODG99gYgfmjcRLgt9/jH+99w4DAAAAIAAAACgAAABQVUJLTUlOVE1BWFRXRfQ1RHLWGOgqABUTYfVBDZrv3OL2nPLYve9ldfNVLOjdPVFFkgUA6D0Vb1mSBQAAAAAA
602""",
603    ]
604    request = [
605        "AgAAAEAAAABOT05DUEFE/9gPiPatvmWssQw2AuZ9mFvAZ/8wZuA95sEuajMAA0Q3GxaQXH98jzsnmYqqSwU5MaPBk/MzEBXgKb+EnH34wvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
606        "AgAAAEAAAABOT05DUEFE/0m1ij4pVmK7ftb4BVN45cRjTh+iP9Z6V/MJfiqK2jd0ulQ7MjOhHwlfiTxCvAbDaDNlSKTHEnY3FZ2vxsF8GZIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
607        "AgAAAEAAAABOT05DUEFE/0AcDP0F/L7NTiOCQlHovyMlovVtG4lBRqAgydNYk9WOoanOwclZuV8z2b/SCHj5thxbSNxuLNZoDQ2b6TWgPfsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
608    ]
609    response = [
610        "BQAAAEAAAABAAAAApAAAADwBAABTSUcAUEFUSFNSRVBDRVJUSU5EWDwlo/AkUnTrecAW4Ci5Tkh3KOqs6R7KLTsFtq16RXN5F7G5ckGv11UtzHoZTbKbEk03a6ogAOK54Q2CI/7XGA8DAAAABAAAAAwAAABSQURJTUlEUFJPT1RAQg8AWDLihlaSBQAoq/5gEjRCrhfH16X2GYjQJSG/CgSuGhYeCsrw7XkphLI3cxw2unJRDW8DAJrYqEGaW0NPKZk7bbpPjU/Q6Es1AgAAAEAAAABTSUcAREVMRUJbs67Sb5Wx/jzWyT1PhWR0c4kg59tjSGofo8R3eHzcA9CGwavuRdxOArhVWWODG99gYgfmjcRLgt9/jH+99w4DAAAAIAAAACgAAABQVUJLTUlOVE1BWFRXRfQ1RHLWGOgqABUTYfVBDZrv3OL2nPLYve9ldfNVLOjdPVFFkgUA6D0Vb1mSBQAAAAAA",
611        "BQAAAEAAAABAAAAApAAAADwBAABTSUcAUEFUSFNSRVBDRVJUSU5EWHH5Ofs4HciIFXjE9egjDbistJptoMXIC7ugCgHhI4NPJqfYY256NpULXKc9c30ul7oHXQyKLfGd84mIAxC3UwQDAAAABAAAAAwAAABSQURJTUlEUFJPT1RAQg8AuOoUh1aSBQANeC4gGGG3a23PpmF+y6CrUS9VWjyj0Ydpl2tMVDLaK2vd5QtYKKJ3UOyprGKk0D/aPn4E3Bk2rE3BKBZRXM1AAgAAAEAAAABTSUcAREVMRci9uvioJssgd8txxFlqz9RqPx+YLVMkHmm24fMUtYGWF/nhkoEYVGT7O+tXSfHHY/KHcUZjVaZpEt/tmXlXBAUDAAAAIAAAACgAAABQVUJLTUlOVE1BWFSxhKhavdriTvCAtNVcK5yr0cAbsWp2MsrwUV5YTc+7V0CsaLZSkgUAQAxA1GaSBQAAAAAA",
612        "BQAAAEAAAABAAAAApAAAADwBAABTSUcAUEFUSFNSRVBDRVJUSU5EWN5Y0b2irPS1JgqJFQMciPg4aWd9qj1ZqcJc5bGXe1m4ZdAXa5OIhXa0+680MgpyhEHhqYJDIwH1XRa1OZx5YAUDAAAABAAAAAwAAABSQURJTUlEUFJPT1RAQg8AgBW3iFaSBQD9WI+Qr6NOZsDmP0PsnCo66mstM3ac5ZON+I+ZeEK8lZWBASvsD2JIfq3v4d1QH5g4STs3wOazQPc25Puy659ZAgAAAEAAAABTSUcAREVMRUJbs67Sb5Wx/jzWyT1PhWR0c4kg59tjSGofo8R3eHzcA9CGwavuRdxOArhVWWODG99gYgfmjcRLgt9/jH+99w4DAAAAIAAAACgAAABQVUJLTUlOVE1BWFRXRfQ1RHLWGOgqABUTYfVBDZrv3OL2nPLYve9ldfNVLOjdPVFFkgUA6D0Vb1mSBQAAAAAA",
613    ]
614
615    server_request = request[0]
616    server_response = response[0]
617    test_cli("roughtime", [], expected_stderr=b'Please specify either --servers-file or --host and --pubkey\n')
618
619    with open(ecosystem, 'w') as f:
620        f.write("Cloudflare-Roughtime ed25519 gD63hSj4ScS+wuOeGrubXlq35N1c5Lby/S+T7MNTjxo= udp 127.0.0.1:" + str(server_port))
621
622    test_cli("roughtime", [
623        "--check-local-clock=0",
624        "--chain-file=",
625        "--servers-file=" + ecosystem]
626             , expected_stderr=b'ERROR: Public key does not match!\n')
627
628    with open(ecosystem, 'w') as f:
629        f.write("Cloudflare-Roughtime ed25519 gD63hSj3ScS+wuOeGrubXlq35N1c5Lby/S+T7MNTjxo= udp 127.0.0.1:" + str(server_port))
630
631    test_cli("roughtime", [
632        "--chain-file=",
633        "--servers-file=" + ecosystem]
634             , expected_stderr=b'ERROR: Local clock mismatch\n')
635
636    test_cli("roughtime", [
637        "--check-local-clock=0",
638        "--chain-file=" + chain_file,
639        "--servers-file=" + ecosystem]
640             , "Cloudflare-Roughtime     : UTC 2019-09-12T08:00:11 (+-1000000us)")
641
642    with open(chain_file, 'r') as f:
643        read_data = f.read()
644    if read_data != chain[0]:
645        logging.error("unexpected chain")
646
647    server_request = request[1]
648    server_response = response[1]
649    test_cli("roughtime", [
650        "--check-local-clock=0",
651        "--chain-file=" + chain_file,
652        "--host=127.0.0.1:" + str(server_port),
653        "--pubkey=gD63hSj3ScS+wuOeGrubXlq35N1c5Lby/S+T7MNTjxo=",
654        "--raw-time"]
655             , "UTC 1568275214691000 (+-1000000us)")
656
657    with open(chain_file, 'r') as f:
658        read_data = f.read()
659    if read_data != chain[1]:
660        logging.error("unexpected chain")
661
662    server_request = request[2]
663    server_response = response[2]
664    test_cli("roughtime", [
665        "--check-local-clock=0",
666        "--chain-file=" + chain_file,
667        "--host=127.0.0.1:" + str(server_port),
668        "--pubkey=gD63hSj3ScS+wuOeGrubXlq35N1c5Lby/S+T7MNTjxo=",
669        "--max-chain-size=2"]
670             , "UTC 2019-09-12T08:00:42 (+-1000000us)")
671
672    with open(chain_file, 'r') as f:
673        read_data = f.read()
674    if read_data != chain[2]:
675        logging.error("unexpected chain")
676
677def cli_pk_workfactor_tests(_tmp_dir):
678    test_cli("pk_workfactor", "1024", "80")
679    test_cli("pk_workfactor", "2048", "111")
680    test_cli("pk_workfactor", ["--type=rsa", "512"], "58")
681    test_cli("pk_workfactor", ["--type=dl", "512"], "58")
682    test_cli("pk_workfactor", ["--type=dl_exp", "512"], "128")
683
684def cli_dl_group_info_tests(_tmp_dir):
685
686    dl_output = re.compile('(P|G) = [A-F0-9]+')
687
688    for bits in [1024, 1536, 2048, 3072, 4096, 6144, 8192]:
689        output = test_cli("dl_group_info", "modp/ietf/%d" % (bits))
690        lines = output.split('\n')
691
692        if len(lines) != 2:
693            logging.error('Unexpected output from dl_group_info')
694
695        for l in lines:
696            if not dl_output.match(l):
697                logging.error('Unexpected output from dl_group_info')
698
699
700
701def cli_ec_group_info_tests(_tmp_dir):
702
703    # pylint: disable=line-too-long
704    secp256r1_info = """P = FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
705A = FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
706B = 5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
707N = FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
708G = 6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296,4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5"""
709
710    secp256r1_pem = """-----BEGIN EC PARAMETERS-----
711MIHgAgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP//////////
712/////zBEBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6
713k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsEQQRrF9Hy4SxCR/i85uVjpEDydwN9
714gS3rM6D0oTlF2JjClk/jQuL+Gn+bjufrSnwPnhYrzjNXazFezsu2QGg3v1H1AiEA
715/////wAAAAD//////////7zm+q2nF56E87nKwvxjJVECAQE=
716-----END EC PARAMETERS-----"""
717
718    test_cli("ec_group_info", "secp256r1", secp256r1_info)
719    test_cli("ec_group_info", "--pem secp256r1", secp256r1_pem)
720
721def cli_cpuid_tests(_tmp_dir):
722    cpuid_output = test_cli("cpuid", [])
723
724    if not cpuid_output.startswith('CPUID flags:'):
725        logging.error('Unexpected cpuid output "%s"' % (cpuid_output))
726
727    flag_re = re.compile('[a-z0-9_]+')
728    flags = cpuid_output[13:].split(' ')
729    for flag in flags:
730        if flag != '' and flag_re.match(flag) is None:
731            logging.error('Unexpected CPUID flag name "%s"' % (flag))
732
733def cli_cc_enc_tests(_tmp_dir):
734    test_cli("cc_encrypt", ["8028028028028029", "pass"], "4308989841607208")
735    test_cli("cc_decrypt", ["4308989841607208", "pass"], "8028028028028027")
736
737def cli_cert_issuance_tests(tmp_dir):
738    root_key = os.path.join(tmp_dir, 'root.key')
739    root_crt = os.path.join(tmp_dir, 'root.crt')
740    int_key = os.path.join(tmp_dir, 'int.key')
741    int_crt = os.path.join(tmp_dir, 'int.crt')
742    int_csr = os.path.join(tmp_dir, 'int.csr')
743    leaf_key = os.path.join(tmp_dir, 'leaf.key')
744    leaf_crt = os.path.join(tmp_dir, 'leaf.crt')
745    leaf_csr = os.path.join(tmp_dir, 'leaf.csr')
746
747    test_cli("keygen", ["--params=2048", "--output=" + root_key], "")
748    test_cli("keygen", ["--params=2048", "--output=" + int_key], "")
749    test_cli("keygen", ["--params=2048", "--output=" + leaf_key], "")
750
751    test_cli("gen_self_signed",
752             [root_key, "Root", "--ca", "--path-limit=2", "--output="+root_crt], "")
753
754    test_cli("gen_pkcs10", "%s Intermediate --ca --output=%s" % (int_key, int_csr))
755    test_cli("sign_cert", "%s %s %s --output=%s" % (root_crt, root_key, int_csr, int_crt))
756
757    test_cli("gen_pkcs10", "%s Leaf --output=%s" % (leaf_key, leaf_csr))
758    test_cli("sign_cert", "%s %s %s --output=%s" % (int_crt, int_key, leaf_csr, leaf_crt))
759
760    test_cli("cert_verify" "%s %s %s" % (leaf_crt, int_crt, root_crt), "Certificate passes validation checks")
761
762def cli_timing_test_tests(_tmp_dir):
763
764    timing_tests = ["bleichenbacher", "manger",
765                    "ecdsa", "ecc_mul", "inverse_mod", "pow_mod",
766                    "lucky13sec3", "lucky13sec4sha1",
767                    "lucky13sec4sha256", "lucky13sec4sha384"]
768
769    output_re = re.compile('[0-9]+;[0-9];[0-9]+')
770
771    for suite in timing_tests:
772        output = test_cli("timing_test", [suite, "--measurement-runs=16", "--warmup-runs=3"], None).split('\n')
773
774        for line in output:
775            if output_re.match(line) is None:
776                logging.error("Unexpected output in timing_test %s: %s", suite, line)
777
778def cli_tls_ciphersuite_tests(_tmp_dir):
779    policies = ['default', 'suiteb_128', 'suiteb_192', 'strict', 'all']
780
781    versions = ['tls1.0', 'tls1.1', 'tls1.2']
782
783    ciphersuite_re = re.compile('^[A-Z0-9_]+$')
784
785    for policy in policies:
786        for version in versions:
787
788            if version != 'tls1.2' and policy != 'all':
789                continue
790
791            output = test_cli("tls_ciphers", ["--version=" + version, "--policy=" + policy], None).split('\n')
792
793            for line in output:
794                if ciphersuite_re.match(line) is None:
795                    logging.error("Unexpected ciphersuite line %s", line)
796
797def cli_asn1_tests(_tmp_dir):
798    input_pem = """-----BEGIN BLOB-----
799MCACAQUTBnN0cmluZzEGAQH/AgFjBAUAAAAAAAMEAP///w==
800-----END BLOB------
801"""
802
803    expected = """d= 0, l=  32: SEQUENCE
804  d= 1, l=   1:  INTEGER                                    05
805  d= 1, l=   6:  PRINTABLE STRING                           string
806  d= 1, l=   6:  SET
807  d= 2, l=   1:   BOOLEAN                                   true
808  d= 2, l=   1:   INTEGER                                   63
809  d= 1, l=   5:  OCTET STRING                               0000000000
810  d= 1, l=   4:  BIT STRING                                 FFFFFF"""
811
812    test_cli("asn1print", "--pem -", expected, input_pem)
813
814def cli_tls_socket_tests(tmp_dir):
815    client_msg = b'Client message %d\n' % (random.randint(0, 2**128))
816    server_port = random_port_number()
817
818    priv_key = os.path.join(tmp_dir, 'priv.pem')
819    ca_cert = os.path.join(tmp_dir, 'ca.crt')
820    crt_req = os.path.join(tmp_dir, 'crt.req')
821    server_cert = os.path.join(tmp_dir, 'server.crt')
822
823    test_cli("keygen", ["--algo=ECDSA", "--params=secp256r1", "--output=" + priv_key], "")
824
825    test_cli("gen_self_signed",
826             [priv_key, "CA", "--ca", "--country=VT",
827              "--dns=ca.example", "--hash=SHA-384", "--output="+ca_cert],
828             "")
829
830    test_cli("cert_verify", ca_cert, "Certificate did not validate - Cannot establish trust")
831
832    test_cli("gen_pkcs10", "%s localhost --output=%s" % (priv_key, crt_req))
833
834    test_cli("sign_cert", "%s %s %s --output=%s" % (ca_cert, priv_key, crt_req, server_cert))
835
836    tls_server = subprocess.Popen([CLI_PATH, 'tls_server', '--max-clients=1',
837                                   '--port=%d' % (server_port), server_cert, priv_key],
838                                  stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
839
840    wait_time = 1.0
841
842    time.sleep(wait_time)
843
844    tls_client = subprocess.Popen([CLI_PATH, 'tls_client', 'localhost',
845                                   '--port=%d' % (server_port), '--trusted-cas=%s' % (ca_cert)],
846                                  stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
847
848    time.sleep(wait_time)
849
850    tls_client.stdin.write(client_msg)
851    tls_client.stdin.flush()
852
853    time.sleep(wait_time)
854
855    (stdout, stderr) = tls_client.communicate()
856
857    if stderr:
858        logging.error("Got unexpected stderr output %s" % (stderr))
859
860    if b'Handshake complete' not in stdout:
861        logging.error('Failed to complete handshake: %s' % (stdout))
862
863    if client_msg not in stdout:
864        logging.error("Missing client message from stdout %s" % (stdout))
865
866    tls_server.communicate()
867
868def cli_tls_http_server_tests(tmp_dir):
869    if not check_for_command("tls_http_server"):
870        return
871
872    try:
873        from http.client import HTTPSConnection
874    except ImportError:
875        try:
876            from httplib import HTTPSConnection
877        except ImportError:
878            return
879    import ssl
880
881    server_port = random_port_number()
882
883    priv_key = os.path.join(tmp_dir, 'priv.pem')
884    ca_cert = os.path.join(tmp_dir, 'ca.crt')
885    crt_req = os.path.join(tmp_dir, 'crt.req')
886    server_cert = os.path.join(tmp_dir, 'server.crt')
887
888    test_cli("keygen", ["--algo=ECDSA", "--params=secp384r1", "--output=" + priv_key], "")
889
890    test_cli("gen_self_signed",
891             [priv_key, "CA", "--ca", "--country=VT",
892              "--dns=ca.example", "--hash=SHA-384", "--output="+ca_cert],
893             "")
894
895    test_cli("gen_pkcs10", "%s localhost --output=%s" % (priv_key, crt_req))
896
897    test_cli("sign_cert", "%s %s %s --output=%s" % (ca_cert, priv_key, crt_req, server_cert))
898
899    tls_server = subprocess.Popen([CLI_PATH, 'tls_http_server', '--max-clients=2',
900                                   '--port=%d' % (server_port), server_cert, priv_key],
901                                  stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
902
903    wait_time = 1.0
904    time.sleep(wait_time)
905
906    context = ssl.create_default_context(cafile=ca_cert)
907    conn = HTTPSConnection('localhost', port=server_port, context=context)
908    conn.request("GET", "/")
909    resp = conn.getresponse()
910
911    if resp.status != 200:
912        logging.error('Unexpected response status %d' % (resp.status))
913
914    body = str(resp.read())
915
916    if body.find('TLS negotiation with Botan 2.') < 0:
917        logging.error('Unexpected response body')
918
919    conn.request("POST", "/logout")
920    resp = conn.getresponse()
921
922    if resp.status != 405:
923        logging.error('Unexpected response status %d' % (resp.status))
924
925    if sys.version_info.major >= 3:
926        rc = tls_server.wait(5) # pylint: disable=too-many-function-args
927    else:
928        rc = tls_server.wait()
929
930    if rc != 0:
931        logging.error("Unexpected return code from https_server %d", rc)
932
933def cli_tls_proxy_tests(tmp_dir):
934    # pylint: disable=too-many-locals,too-many-statements
935    if not check_for_command("tls_proxy"):
936        return
937
938    try:
939        from http.client import HTTPSConnection
940    except ImportError:
941        try:
942            from httplib import HTTPSConnection
943        except ImportError:
944            return
945
946    try:
947        from http.server import HTTPServer, BaseHTTPRequestHandler
948    except ImportError:
949        try:
950            from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
951        except ImportError:
952            return
953
954    import ssl
955    import threading
956
957    server_port = random_port_number()
958    proxy_port = random_port_number()
959
960    while server_port == proxy_port:
961        proxy_port = random_port_number()
962
963    priv_key = os.path.join(tmp_dir, 'priv.pem')
964    ca_cert = os.path.join(tmp_dir, 'ca.crt')
965    crt_req = os.path.join(tmp_dir, 'crt.req')
966    server_cert = os.path.join(tmp_dir, 'server.crt')
967
968    test_cli("keygen", ["--algo=ECDSA", "--params=secp384r1", "--output=" + priv_key], "")
969
970    test_cli("gen_self_signed",
971             [priv_key, "CA", "--ca", "--country=VT",
972              "--dns=ca.example", "--hash=SHA-384", "--output="+ca_cert],
973             "")
974
975    test_cli("gen_pkcs10", "%s localhost --output=%s" % (priv_key, crt_req))
976
977    test_cli("sign_cert", "%s %s %s --output=%s" % (ca_cert, priv_key, crt_req, server_cert))
978
979    tls_proxy = subprocess.Popen([CLI_PATH, 'tls_proxy', str(proxy_port), '127.0.0.1', str(server_port),
980                                  server_cert, priv_key, '--output=/tmp/proxy.err', '--max-clients=2'],
981                                 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
982
983    wait_time = 1.0
984
985    time.sleep(wait_time)
986
987    server_response = binascii.hexlify(os.urandom(32))
988
989    def run_http_server():
990        class Handler(BaseHTTPRequestHandler):
991
992            def do_GET(self): # pylint: disable=invalid-name
993                self.send_response(200)
994                self.end_headers()
995                self.wfile.write(server_response)
996
997        httpd = HTTPServer(('', server_port), Handler)
998        httpd.serve_forever()
999
1000    http_thread = threading.Thread(target=run_http_server)
1001    http_thread.daemon = True
1002    http_thread.start()
1003
1004    time.sleep(wait_time)
1005
1006    context = ssl.create_default_context(cafile=ca_cert)
1007
1008    for _i in range(2):
1009        conn = HTTPSConnection('localhost', port=proxy_port, context=context)
1010        conn.request("GET", "/")
1011        resp = conn.getresponse()
1012
1013        if resp.status != 200:
1014            logging.error('Unexpected response status %d' % (resp.status))
1015
1016        body = resp.read()
1017
1018        if body != server_response:
1019            logging.error('Unexpected response from server %s' % (body))
1020
1021    if sys.version_info.major >= 3:
1022        rc = tls_proxy.wait(5) # pylint: disable=too-many-function-args
1023    else:
1024        rc = tls_proxy.wait()
1025
1026    if rc != 0:
1027        logging.error('Unexpected return code %d', rc)
1028
1029def cli_trust_root_tests(tmp_dir):
1030    pem_file = os.path.join(tmp_dir, 'pems')
1031    dn_file = os.path.join(tmp_dir, 'dns')
1032
1033    test_cli("trust_roots", ['--dn-only', '--output=%s' % (dn_file)], "")
1034
1035    dn_re = re.compile('(.+=\".+\")(,.+=\".+\")')
1036    for line in open(dn_file):
1037        if dn_re.match(line) is None:
1038            logging.error("Unexpected DN line %s", line)
1039
1040    test_cli("trust_roots", ['--output=%s' % (pem_file)], "")
1041
1042def cli_tss_tests(tmp_dir):
1043    data_file = os.path.join(tmp_dir, 'data')
1044
1045    exp_hash = "53B3C59276AE30EA7FD882268E80FD96AD80CC9FEB15F9FB940E7C4B5CF80B9E"
1046
1047    test_cli("rng", ["32", "--output=%s" % (data_file)], "")
1048    test_cli("hash", ["--no-fsname", data_file], exp_hash)
1049
1050    m = 3
1051    n = 5
1052
1053    test_cli("tss_split", [str(m), str(n), data_file, "--share-prefix=%s/split" % (tmp_dir)], "")
1054
1055    share_files = []
1056
1057    for i in range(1, n+1):
1058        share = os.path.join(tmp_dir, "split%d.tss" % (i))
1059        if not os.access(share, os.R_OK):
1060            logging.error("Failed to create expected split file %s", share)
1061        share_files.append(share)
1062
1063    rec5 = os.path.join(tmp_dir, "recovered_5")
1064    test_cli("tss_recover", share_files + ["--output=%s" % (rec5)], "")
1065    test_cli("hash", ["--no-fsname", rec5], exp_hash)
1066
1067    rec4 = os.path.join(tmp_dir, "recovered_4")
1068    test_cli("tss_recover", share_files[1:] + ["--output=%s" % (rec4)], "")
1069    test_cli("hash", ["--no-fsname", rec4], exp_hash)
1070
1071    rec3 = os.path.join(tmp_dir, "recovered_3")
1072    test_cli("tss_recover", share_files[2:] + ["--output=%s" % (rec3)], "")
1073    test_cli("hash", ["--no-fsname", rec3], exp_hash)
1074
1075    rec2 = os.path.join(tmp_dir, "recovered_2")
1076    test_cli("tss_recover", share_files[3:] + ["--output=%s" % (rec2)], "", None,
1077             b'Error: Insufficient shares to do TSS reconstruction\n')
1078
1079
1080def cli_pk_encrypt_tests(tmp_dir):
1081    input_file = os.path.join(tmp_dir, 'input')
1082    ctext_file = os.path.join(tmp_dir, 'ctext')
1083    recovered_file = os.path.join(tmp_dir, 'recovered')
1084    rsa_priv_key = os.path.join(tmp_dir, 'rsa.priv')
1085    rsa_pub_key = os.path.join(tmp_dir, 'rsa.pub')
1086
1087    test_cli("keygen", ["--algo=RSA", "--provider=base", "--params=2048", "--output=%s" % (rsa_priv_key)], "")
1088
1089    key_hash = "72AF3227EF57A728E894D54623EB8E2C0CD11A4A98BF2DF32DB052BF60897873"
1090    test_cli("hash", ["--no-fsname", "--algo=SHA-256", rsa_priv_key], key_hash)
1091
1092    test_cli("pkcs8", ["--pub-out", "%s/rsa.priv" % (tmp_dir), "--output=%s" % (rsa_pub_key)], "")
1093
1094    # Generate a random input file
1095    test_cli("rng", ["10", "16", "32", "--output=%s" % (input_file)], "")
1096
1097    # Because we used a fixed DRBG for each invocation the same ctext is generated each time
1098    rng_output_hash = "32F5E7B61357DE8397EFDA1E598379DFD5EE21767BDF4E2A435F05117B836AC6"
1099    ctext_hash = "FF1F0EEC2C42DD61D78505C5DF624A19AE6FE2BAB0B8F7D878C7655D54C68FE0"
1100
1101    test_cli("hash", ["--no-fsname", "--algo=SHA-256", input_file], rng_output_hash)
1102
1103    # Encrypt and verify ciphertext is the expected value
1104    test_cli("pk_encrypt", [rsa_pub_key, input_file, "--output=%s" % (ctext_file)], "")
1105    test_cli("hash", ["--no-fsname", "--algo=SHA-256", ctext_file], ctext_hash)
1106
1107    # Decrypt and verify plaintext is recovered
1108    test_cli("pk_decrypt", [rsa_priv_key, ctext_file, "--output=%s" % (recovered_file)], "")
1109    test_cli("hash", ["--no-fsname", "--algo=SHA-256", recovered_file], rng_output_hash)
1110
1111def cli_uuid_tests(_tmp_dir):
1112    test_cli("uuid", [], "D80F88F6-ADBE-45AC-B10C-3602E67D985B")
1113
1114    uuid_re = re.compile(r'[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}')
1115
1116    output = test_cli("uuid", [])
1117
1118    if uuid_re.match(output) is None:
1119        logging.error('Bad uuid output %s' % (output))
1120
1121def cli_tls_client_hello_tests(_tmp_dir):
1122
1123    # pylint: disable=line-too-long
1124    chello = "16030100cf010000cb03035b3cf2457b864d7bef2a4b1f84fc3ced2b68d9551f3455ffdd305af277a91bb200003a16b816b716ba16b9cca9cca8c02cc030c02bc02fc0adc0acc024c00ac028c014c023c009c027c013ccaa009f009ec09fc09e006b003900670033010000680000000e000c000009676d61696c2e636f6d000500050100000000000a001a0018001d0017001a0018001b0019001c01000101010201030104000b00020100000d00140012080508040806050106010401050306030403001600000017000000230000ff01000100"
1125
1126    output = test_cli("tls_client_hello", ["--hex", "-"], None, chello)
1127
1128    output_hash = "8EBFC3205ACFA98461128FE5D081D19254237AF84F7DAF000A3C992C3CF6DE44"
1129    test_cli("hash", ["--no-fsname", "--algo=SHA-256", "-"], output_hash, output)
1130
1131def cli_speed_pk_tests(_tmp_dir):
1132    msec = 1
1133
1134    pk_algos = ["ECDSA", "ECDH", "SM2", "ECKCDSA", "ECGDSA", "GOST-34.10",
1135                "DH", "DSA", "ElGamal", "Ed25519", "Curve25519", "NEWHOPE", "McEliece",
1136                "RSA", "RSA_keygen", "XMSS"]
1137
1138    output = test_cli("speed", ["--msec=%d" % (msec)] + pk_algos, None).split('\n')
1139
1140    # ECDSA-secp256r1 106 keygen/sec; 9.35 ms/op 37489733 cycles/op (1 op in 9 ms)
1141    format_re = re.compile(r'^.* [0-9]+ ([A-Za-z ]+)/sec; [0-9]+\.[0-9]+ ms/op .*\([0-9]+ (op|ops) in [0-9\.]+ ms\)')
1142    for line in output:
1143        if format_re.match(line) is None:
1144            logging.error("Unexpected line %s", line)
1145
1146def cli_speed_pbkdf_tests(_tmp_dir):
1147    msec = 1
1148    pbkdf_ops = ['bcrypt', 'passhash9', 'argon2']
1149
1150    format_re = re.compile(r'^.* [0-9]+ /sec; [0-9]+\.[0-9]+ ms/op .*\([0-9]+ (op|ops) in [0-9]+(\.[0-9]+)? ms\)')
1151    for op in pbkdf_ops:
1152        output = test_cli("speed", ["--msec=%d" % (msec), op], None).split('\n')
1153        for line in output:
1154            if format_re.match(line) is None:
1155                logging.error("Unexpected line %s", line)
1156
1157def cli_speed_table_tests(_tmp_dir):
1158    msec = 1
1159
1160    version_re = re.compile(r'^Botan 2\.[0-9]+\.[0-9] \(.*, revision .*, distribution .*\)')
1161    cpuid_re = re.compile(r'^CPUID: [a-z_0-9 ]*$')
1162    format_re = re.compile(r'^AES-128 .* buffer size [0-9]+ bytes: [0-9]+\.[0-9]+ MiB\/sec .*\([0-9]+\.[0-9]+ MiB in [0-9]+\.[0-9]+ ms\)')
1163    tbl_hdr_re = re.compile(r'^algo +operation +1024 bytes$')
1164    tbl_val_re = re.compile(r'^AES-128 +(encrypt|decrypt) +[0-9]+(\.[0-9]{2})$')
1165
1166    output = test_cli("speed", ["--format=table", "--provider=base", "--msec=%d" % (msec), "AES-128"], None).split('\n')
1167
1168    if len(output) != 11:
1169        logging.error('Unexpected number of lines from table output')
1170
1171    if version_re.match(output[0]) is None:
1172        logging.error("Unexpected version line %s", output[0])
1173
1174    if output[1] != '':
1175        if cpuid_re.match(output[1]) is None:
1176            logging.error("Unexpected cpuid line %s", output[1])
1177    elif output[2] != '':
1178        logging.error("Expected newline got %s", output[2])
1179
1180    if format_re.match(output[3]) is None:
1181        logging.error("Unexpected line %s", output[3])
1182    if format_re.match(output[4]) is None:
1183        logging.error("Unexpected line %s", output[4])
1184    if output[5] != '':
1185        logging.error("Expected newline got %s", output[5])
1186
1187    if tbl_hdr_re.match(output[6]) is None:
1188        logging.error("Unexpected table header %s", output[6])
1189    if tbl_val_re.match(output[7]) is None:
1190        logging.error("Unexpected table header %s", output[7])
1191    if tbl_val_re.match(output[8]) is None:
1192        logging.error("Unexpected table header %s", output[8])
1193    if output[9] != '':
1194        logging.error("Expected newline got %s", output[9])
1195    if output[10].find('results are the number of 1000s bytes processed per second') < 0:
1196        logging.error("Unexpected trailing message got %s", output[10])
1197
1198def cli_speed_invalid_option_tests(_tmp_dir):
1199    speed_usage = b"Usage: speed --msec=500 --format=default --ecc-groups= --provider= --buf-size=1024 --clear-cpuid= --cpu-clock-speed=0 --cpu-clock-ratio=1.0 *algos\n"
1200
1201    test_cli("speed", ["--buf-size=0", "--msec=1", "AES-128"],
1202             expected_stderr=b"Usage error: Cannot have a zero-sized buffer\n%s" % (speed_usage))
1203
1204    test_cli("speed", ["--buf-size=F00F", "--msec=1", "AES-128"],
1205             expected_stderr=b"Usage error: Invalid integer value 'F00F' for option buf-size\n%s" % (speed_usage))
1206
1207    test_cli("speed", ["--buf-size=90000000", "--msec=1", "AES-128"],
1208             expected_stderr=b"Usage error: Specified buffer size is too large\n%s" % (speed_usage))
1209
1210    test_cli("speed", ["--clear-cpuid=goku", "--msec=1", "AES-128"],
1211             expected_stderr=b"Warning don't know CPUID flag 'goku'\n")
1212
1213def cli_speed_math_tests(_tmp_dir):
1214    msec = 1
1215    # these all have a common output format
1216    math_ops = ['mp_mul', 'mp_div', 'mp_div10', 'modexp', 'random_prime', 'inverse_mod',
1217                'rfc3394', 'fpe_fe1', 'ecdsa_recovery', 'ecc_init', 'poly_dbl',
1218                'bn_redc', 'nistp_redc', 'ecc_mult', 'ecc_ops', 'os2ecp', 'primality_test']
1219
1220    format_re = re.compile(r'^.* [0-9]+ /sec; [0-9]+\.[0-9]+ ms/op .*\([0-9]+ (op|ops) in [0-9]+(\.[0-9]+)? ms\)')
1221    for op in math_ops:
1222        output = test_cli("speed", ["--msec=%d" % (msec), op], None).split('\n')
1223        for line in output:
1224            if format_re.match(line) is None:
1225                logging.error("Unexpected line %s", line)
1226
1227def cli_speed_tests(_tmp_dir):
1228    # pylint: disable=too-many-branches
1229
1230    msec = 1
1231
1232    output = test_cli("speed", ["--msec=%d" % (msec), "--buf-size=64,512", "AES-128"], None).split('\n')
1233
1234    if len(output) % 4 != 0:
1235        logging.error("Unexpected number of lines for AES-128 speed test")
1236
1237    # pylint: disable=line-too-long
1238    format_re = re.compile(r'^AES-128 .* buffer size [0-9]+ bytes: [0-9]+\.[0-9]+ MiB\/sec .*\([0-9]+\.[0-9]+ MiB in [0-9]+\.[0-9]+ ms\)')
1239    for line in output:
1240        if format_re.match(line) is None:
1241            logging.error("Unexpected line %s", line)
1242
1243    output = test_cli("speed", ["--msec=%d" % (msec), "ChaCha20", "SHA-256", "HMAC(SHA-256)"], None).split('\n')
1244
1245    # pylint: disable=line-too-long
1246    format_re = re.compile(r'^.* buffer size [0-9]+ bytes: [0-9]+\.[0-9]+ MiB\/sec .*\([0-9]+\.[0-9]+ MiB in [0-9]+\.[0-9]+ ms\)')
1247    for line in output:
1248        if format_re.match(line) is None:
1249            logging.error("Unexpected line %s", line)
1250
1251    output = test_cli("speed", ["--msec=%d" % (msec), "AES-128/GCM"], None).split('\n')
1252    format_re_ks = re.compile(r'^AES-128/GCM\(16\).* [0-9]+ key schedule/sec; [0-9]+\.[0-9]+ ms/op .*\([0-9]+ (op|ops) in [0-9\.]+ ms\)')
1253    format_re_cipher = re.compile(r'^AES-128/GCM\(16\) .* buffer size [0-9]+ bytes: [0-9]+\.[0-9]+ MiB\/sec .*\([0-9]+\.[0-9]+ MiB in [0-9]+\.[0-9]+ ms\)')
1254    for line in output:
1255        if format_re_ks.match(line) is None:
1256            if format_re_cipher.match(line) is None:
1257                logging.error('Unexpected line %s', line)
1258
1259    output = test_cli("speed", ["--msec=%d" % (msec), "scrypt"], None).split('\n')
1260
1261    format_re = re.compile(r'^scrypt-[0-9]+-[0-9]+-[0-9]+ \([0-9]+ MiB\) [0-9]+ /sec; [0-9]+\.[0-9]+ ms/op .*\([0-9]+ (op|ops) in [0-9\.]+ ms\)')
1262
1263    for line in output:
1264        if format_re.match(line) is None:
1265            logging.error("Unexpected line %s", line)
1266
1267    output = test_cli("speed", ["--msec=%d" % (msec), "RNG"], None).split('\n')
1268
1269    # ChaCha_RNG generate buffer size 1024 bytes: 954.431 MiB/sec 4.01 cycles/byte (477.22 MiB in 500.00 ms)
1270    format_re = re.compile(r'^.* generate buffer size [0-9]+ bytes: [0-9]+\.[0-9]+ MiB/sec .*\([0-9]+\.[0-9]+ MiB in [0-9]+\.[0-9]+ ms')
1271    for line in output:
1272        if format_re.match(line) is None:
1273            logging.error("Unexpected line %s", line)
1274
1275    # Entropy source rdseed output 128 bytes estimated entropy 0 in 0.02168 ms total samples 32
1276    output = test_cli("speed", ["--msec=%d" % (msec), "entropy"], None).split('\n')
1277    format_re = re.compile(r'^Entropy source [_a-z0-9]+ output [0-9]+ bytes estimated entropy [0-9]+ in [0-9]+\.[0-9]+ ms .*total samples [0-9]+')
1278    for line in output:
1279        if format_re.match(line) is None:
1280            logging.error("Unexpected line %s", line)
1281
1282    output = test_cli("speed", ["--msec=%d" % (msec), "--format=json", "AES-128"], None)
1283
1284    json_blob = json.loads(output)
1285    if len(json_blob) < 2:
1286        logging.error("Unexpected size for JSON output")
1287
1288    for b in json_blob:
1289        for field in ['algo', 'op', 'events', 'bps', 'buf_size', 'nanos']:
1290            if field not in b:
1291                logging.error('Missing field %s in JSON record %s' % (field, b))
1292
1293def run_test(fn_name, fn):
1294    start = time.time()
1295    tmp_dir = tempfile.mkdtemp(prefix='botan_cli_')
1296    try:
1297        fn(tmp_dir)
1298    except Exception as e: # pylint: disable=broad-except
1299        logging.error("Test %s threw exception: %s", fn_name, e)
1300
1301    shutil.rmtree(tmp_dir)
1302    end = time.time()
1303    logging.info("Ran %s in %.02f sec", fn_name, end-start)
1304
1305def main(args=None):
1306    # pylint: disable=too-many-branches,too-many-locals
1307    if args is None:
1308        args = sys.argv
1309
1310    parser = optparse.OptionParser(
1311        formatter=optparse.IndentedHelpFormatter(max_help_position=50))
1312
1313    parser.add_option('--verbose', action='store_true', default=False)
1314    parser.add_option('--quiet', action='store_true', default=False)
1315    parser.add_option('--threads', action='store', type='int', default=0)
1316
1317    (options, args) = parser.parse_args(args)
1318
1319    setup_logging(options)
1320
1321    if len(args) < 2:
1322        logging.error("Usage: %s path_to_botan_cli [test_regex]", args[0])
1323        return 1
1324
1325    if not os.access(args[1], os.X_OK):
1326        logging.error("Could not access/execute %s", args[1])
1327        return 2
1328
1329    threads = options.threads
1330    if threads == 0:
1331        threads = multiprocessing.cpu_count()
1332
1333    global CLI_PATH
1334    CLI_PATH = args[1]
1335
1336    test_regex = None
1337    if len(args) == 3:
1338        try:
1339            test_regex = re.compile(args[2])
1340        except re.error as e:
1341            logging.error("Invalid regex: %s", str(e))
1342            return 1
1343
1344    # some of the slowest tests are grouped up front
1345    test_fns = [
1346        cli_speed_tests,
1347        cli_speed_pk_tests,
1348        cli_speed_math_tests,
1349        cli_speed_pbkdf_tests,
1350        cli_speed_table_tests,
1351        cli_speed_invalid_option_tests,
1352        cli_xmss_sign_tests,
1353
1354        cli_argon2_tests,
1355        cli_asn1_tests,
1356        cli_base32_tests,
1357        cli_base58_tests,
1358        cli_base64_tests,
1359        cli_bcrypt_tests,
1360        cli_cc_enc_tests,
1361        cli_cycle_counter,
1362        cli_cert_issuance_tests,
1363        cli_compress_tests,
1364        cli_config_tests,
1365        cli_cpuid_tests,
1366        cli_dl_group_info_tests,
1367        cli_ec_group_info_tests,
1368        cli_entropy_tests,
1369        cli_factor_tests,
1370        cli_gen_dl_group_tests,
1371        cli_gen_prime_tests,
1372        cli_hash_tests,
1373        cli_help_tests,
1374        cli_hex_tests,
1375        cli_hmac_tests,
1376        cli_is_prime_tests,
1377        cli_key_tests,
1378        cli_mod_inverse_tests,
1379        cli_pbkdf_tune_tests,
1380        cli_pk_encrypt_tests,
1381        cli_pk_workfactor_tests,
1382        cli_psk_db_tests,
1383        cli_rng_tests,
1384        cli_roughtime_check_tests,
1385        cli_roughtime_tests,
1386        cli_timing_test_tests,
1387        cli_tls_ciphersuite_tests,
1388        cli_tls_client_hello_tests,
1389        cli_tls_http_server_tests,
1390        cli_tls_proxy_tests,
1391        cli_tls_socket_tests,
1392        cli_trust_root_tests,
1393        cli_tss_tests,
1394        cli_uuid_tests,
1395        cli_version_tests,
1396        ]
1397
1398    tests_to_run = []
1399    for fn in test_fns:
1400        fn_name = fn.__name__
1401
1402        if test_regex is None or test_regex.search(fn_name) is not None:
1403            tests_to_run.append((fn_name, fn))
1404
1405    start_time = time.time()
1406
1407    if threads > 1:
1408        pool = ThreadPool(processes=threads)
1409        results = []
1410        for test in tests_to_run:
1411            results.append(pool.apply_async(run_test, test))
1412
1413        for result in results:
1414            result.get()
1415    else:
1416        for test in tests_to_run:
1417            run_test(test[0], test[1])
1418
1419    end_time = time.time()
1420
1421    print("Ran %d tests with %d failures in %.02f seconds" % (
1422        TESTS_RUN, TESTS_FAILED, end_time - start_time))
1423
1424    if TESTS_FAILED > 0:
1425        return 1
1426    return 0
1427
1428if __name__ == '__main__':
1429    sys.exit(main())
1430