summaryrefslogtreecommitdiff
path: root/regress/lib/libssl/tlsfuzzer/tlsfuzzer.py
blob: ef037aec9a8e0a3cc45b7665d623de14cc1c7922 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
#   $OpenBSD: tlsfuzzer.py,v 1.5 2020/06/01 10:46:45 tb Exp $
#
# Copyright (c) 2020 Theo Buehler <tb@openbsd.org>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

import getopt
import os
import subprocess
import sys
from timeit import default_timer as timer

tlsfuzzer_scriptdir = "/usr/local/share/tlsfuzzer/scripts/"

class Test:
    """
    Represents a tlsfuzzer test script.
        name:           the script's name
        args:           arguments to feed to the script
        tls12_args:     override args for a TLSv1.2 server
        tls13_args:     override args for a TLSv1.3 server

    XXX Add client cert support.
    """
    def __init__(self, name="", args=[], tls12_args=[], tls13_args=[]):
        self.name = name
        self.tls12_args = args
        self.tls13_args = args
        if tls12_args:
            self.tls12_args = tls12_args
        if tls13_args:
            self.tls13_args = tls13_args

    def args(self, has_tls1_3: True):
        if has_tls1_3:
            return self.tls13_args
        else:
            return self.tls12_args

    def __repr__(self):
        return "<Test: %s tls12_args: %s tls13_args: %s>" % (
                self.name, self.tls12_args, tls13_args
            )

class TestGroup:
    """ A group of Test objects to be run by TestRunner."""
    def __init__(self, title="Tests", tests=[]):
        self.title = title
        self.tests = tests

    def __iter__(self):
        return iter(self.tests)

# argument to pass to several tests
tls13_unsupported_ciphers = [
    "-e", "TLS 1.3 with ffdhe2048",
    "-e", "TLS 1.3 with ffdhe3072",
    "-e", "TLS 1.3 with secp521r1",   # XXX: why is this curve problematic?
    "-e", "TLS 1.3 with x448",
]

tls13_tests = TestGroup("TLSv1.3 tests", [
    Test("test-tls13-ccs.py"),
    Test("test-tls13-conversation.py"),
    Test("test-tls13-count-tickets.py"),
    Test("test-tls13-empty-alert.py"),
    Test("test-tls13-finished-plaintext.py"),
    Test("test-tls13-hrr.py"),
    Test("test-tls13-legacy-version.py"),
    Test("test-tls13-nociphers.py"),
    Test("test-tls13-record-padding.py"),

    # The skipped tests fail due to a bug in BIO_gets() which masks the retry
    # signalled from an SSL_read() failure. Testing with httpd(8) shows we're
    # handling these corner cases correctly since tls13_record_layer.c -r1.47.
    Test("test-tls13-zero-length-data.py", [
        "-e", "zero-length app data",
        "-e", "zero-length app data with large padding",
        "-e", "zero-length app data with padding",
    ]),
])

# Tests that take a lot of time (> ~30s on an x280)
tls13_slow_tests = TestGroup("slow TLSv1.3 tests", [
    # XXX: Investigate the occasional message
    # "Got shared secret with 1 most significant bytes equal to zero."
    Test("test-tls13-dhe-shared-secret-padding.py", tls13_unsupported_ciphers),

    Test("test-tls13-invalid-ciphers.py"),
    Test("test-tls13-serverhello-random.py", tls13_unsupported_ciphers),
])

tls13_extra_cert_tests = TestGroup("TLSv1.3 certificate tests", [
    # need to set up client certs to run these
    Test("test-tls13-certificate-request.py"),
    Test("test-tls13-certificate-verify.py"),
    Test("test-tls13-ecdsa-in-certificate-verify.py"),

    # Test expects the server to have installed three certificates:
    # with P-256, P-384 and P-521 curve. Also SHA1+ECDSA is verified
    # to not work.
    Test("test-tls13-ecdsa-support.py"),
])

tls13_failing_tests = TestGroup("failing TLSv1.3 tests", [
    # Some tests fail because we fail later than the scripts expect us to.
    # With X25519, we accept weak peer public keys and fail when we actually
    # compute the keyshare.  Other tests seem to indicate that we could be
    # stricter about what keyshares we accept.
    Test("test-tls13-crfg-curves.py"),
    Test("test-tls13-ecdhe-curves.py"),

    # Expects a missing_extensions alert
    # AssertionError: Unexpected message from peer: Handshake(server_hello)
    Test("test-tls13-keyshare-omitted.py"),

    # https://github.com/openssl/openssl/issues/8369
    Test("test-tls13-obsolete-curves.py"),

    # 3 failing rsa_pss_pss tests
    Test("test-tls13-rsa-signatures.py"),

    # AssertionError: Unexpected message from peer: ChangeCipherSpec()
    # Most failing tests expect the CCS right before finished.
    # What's up with that?
    Test("test-tls13-version-negotiation.py"),
])

tls13_slow_failing_tests = TestGroup("slow, failing TLSv1.3 tests", [
    # After adding record overflow alert, 14 tests fail because
    # they expect ExpectNewSessionTicket().
    Test("test-tls13-record-layer-limits.py" ),

    # Other test failures bugs in keyshare/tlsext negotiation?
    Test("test-tls13-shuffled-extentions.py"),    # should reject 2nd CH
    Test("test-tls13-unrecognised-groups.py"),    # unexpected closure

    # 5 failures:
    #   'app data split, conversation with KeyUpdate msg'
    #   'fragmented keyupdate msg'
    #   'multiple KeyUpdate messages'
    #   'post-handshake KeyUpdate msg with update_not_request'
    #   'post-handshake KeyUpdate msg with update_request'
    Test("test-tls13-keyupdate.py"),

    Test("test-tls13-symetric-ciphers.py"),       # unexpected message from peer

    # 70 fail and 644 pass. For some reason the tests expect a decode_error
    # but we send a decrypt_error after the CBS_mem_equal() fails in
    # tls13_server_finished_recv() (which is correct).
    Test("test-tls13-finished.py"),               # decrypt_error -> decode_error?

    # The following two tests fail Test (skip empty extensions for the first one):
    # 'empty unassigned extensions, ids in range from 2 to 4118'
    # 'unassigned extensions with random payload, ids in range from 2 to 1046'
    Test("test-tls13-large-number-of-extensions.py"), # 2 fail: empty/random payload

    # 6 tests fail: 'rsa_pkcs1_{md5,sha{1,224,256,384,512}} signature'
    # We send server hello, but the test expects handshake_failure
    Test("test-tls13-pkcs-signature.py"),
    # 8 tests fail: 'tls13 signature rsa_pss_{pss,rsae}_sha{256,384,512}
    Test("test-tls13-rsapss-signatures.py"),

    # ExpectNewSessionTicket
    Test("test-tls13-session-resumption.py"),
])

tls13_unsupported_tests = TestGroup("TLSv1.3 tests for unsupported features", [
    # Tests for features we don't support
    Test("test-tls13-0rtt-garbage.py"),
    Test("test-tls13-ffdhe-groups.py"),
    Test("test-tls13-ffdhe-sanity.py"),
    Test("test-tls13-psk_dhe_ke.py"),
    Test("test-tls13-psk_ke.py"),

    # need server to react to HTTP GET for /keyupdate
    Test("test-tls13-keyupdate-from-server.py"),

    # Weird test: tests servers that don't support 1.3
    Test("test-tls13-non-support.py"),

    # broken test script
    # UnboundLocalError: local variable 'cert' referenced before assignment
    Test("test-tls13-post-handshake-auth.py"),

    # Server must be configured to support only rsa_pss_rsae_sha512
    Test("test-tls13-signature-algorithms.py"),
])

tls12_exclude_legacy_protocols = [
    # all these have BIO_read timeouts against TLSv1.3
    "-e", "Protocol (3, 0)",
    "-e", "Protocol (3, 0) in SSLv2 compatible ClientHello",
    # the following only fail with TLSv1.3
    "-e", "Protocol (3, 1) in SSLv2 compatible ClientHello",
    "-e", "Protocol (3, 2) in SSLv2 compatible ClientHello",
    "-e", "Protocol (3, 3) in SSLv2 compatible ClientHello",
    "-e", "Protocol (3, 1) with secp521r1 group",   # XXX
    "-e", "Protocol (3, 1) with x448 group",
    "-e", "Protocol (3, 2) with secp521r1 group",   # XXX
    "-e", "Protocol (3, 2) with x448 group",
    "-e", "Protocol (3, 3) with secp521r1 group",   # XXX
    "-e", "Protocol (3, 3) with x448 group",
]

tls12_tests = TestGroup("TLSv1.2 tests", [
    # Tests that pass as they are.
    Test("test-TLSv1_2-rejected-without-TLSv1_2.py"),
    Test("test-aes-gcm-nonces.py"),
    Test("test-chacha20.py"),
    Test("test-conversation.py"),
    Test("test-cve-2016-2107.py"),
    Test("test-dhe-rsa-key-exchange.py"),
    Test("test-dhe-rsa-key-exchange-with-bad-messages.py"),
    Test("test-early-application-data.py"),
    Test("test-empty-extensions.py"),
    Test("test-fuzzed-MAC.py"),
    Test("test-fuzzed-ciphertext.py"),
    Test("test-fuzzed-finished.py"),
    Test("test-fuzzed-padding.py"),
    Test("test-hello-request-by-client.py"),
    Test("test-invalid-cipher-suites.py"),
    Test("test-invalid-content-type.py"),
    Test("test-invalid-session-id.py"),
    Test("test-invalid-version.py"),
    Test("test-message-skipping.py"),
    Test("test-no-heartbeat.py"),
    Test("test-sessionID-resumption.py"),
    Test("test-sslv2-connection.py"),
    Test("test-truncating-of-finished.py"),
    Test("test-truncating-of-kRSA-client-key-exchange.py"),
    Test("test-unsupported-curve-fallback.py"),
    Test("test-version-numbers.py"),
    Test("test-zero-length-data.py"),

    # Tests that need tweaking for unsupported features and ciphers.
    Test(
        "test-atypical-padding.py", [
            "-e", "sanity - encrypt then MAC",
            "-e", "2^14 bytes of AppData with 256 bytes of padding (SHA1 + Encrypt then MAC)",
        ]
    ),
    Test(
        "test-dhe-rsa-key-exchange-signatures.py", [
            "-e", "TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA sha224 signature",
            "-e", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 sha224 signature",
            "-e", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA sha224 signature",
            "-e", "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 sha224 signature",
            "-e", "TLS_DHE_RSA_WITH_AES_256_CBC_SHA sha224 signature",
        ]
    ),
    Test("test-dhe-key-share-random.py", tls12_exclude_legacy_protocols),
    Test("test-export-ciphers-rejected.py", ["--min-ver", "TLSv1.0"]),
    Test(
        "test-downgrade-protection.py",
        tls12_args = ["--server-max-protocol", "TLSv1.2"],
        tls13_args = ["--server-max-protocol", "TLSv1.3"],
    ),
    Test("test-fallback-scsv.py", tls13_args = ["--tls-1.3"] ),
    Test("test-serverhello-random.py", args = tls12_exclude_legacy_protocols),
])

tls12_slow_tests = TestGroup("slow TLSv1.2 tests", [
    Test("test-cve-2016-7054.py"),
    Test("test-dhe-no-shared-secret-padding.py", tls12_exclude_legacy_protocols),
    Test("test-ecdhe-padded-shared-secret.py", tls12_exclude_legacy_protocols),
    Test("test-ecdhe-rsa-key-share-random.py", tls12_exclude_legacy_protocols),
    # This test has some failures once in a while.
    Test("test-fuzzed-plaintext.py"),
])

tls12_failing_tests = TestGroup("failing TLSv1.2 tests", [
    # no shared cipher
    Test("test-aesccm.py"),
    # need server to set up alpn
    Test("test-alpn-negotiation.py"),
    # many tests fail due to unexpected server_name extension
    Test("test-bleichenbacher-workaround.py"),

    # need client key and cert plus extra server setup
    Test("test-certificate-malformed.py"),
    Test("test-certificate-request.py"),
    Test("test-certificate-verify-malformed-sig.py"),
    Test("test-certificate-verify-malformed.py"),
    Test("test-certificate-verify.py"),
    Test("test-ecdsa-in-certificate-verify.py"),
    Test("test-renegotiation-disabled-client-cert.py"),
    Test("test-rsa-pss-sigs-on-certificate-verify.py"),
    Test("test-rsa-sigs-on-certificate-verify.py"),

    # test doesn't expect session ticket
    Test("test-client-compatibility.py"),
    # abrupt closure
    Test("test-client-hello-max-size.py"),
    # unknown signature algorithms
    Test("test-clienthello-md5.py"),
    # abrupt closure
    Test("test-cve-2016-6309.py"),

    # Tests expect an illegal_parameter alert
    Test("test-ecdhe-rsa-key-exchange-with-bad-messages.py"),

    # We send a handshake_failure while the test expects to succeed
    Test("test-ecdhe-rsa-key-exchange.py"),

    # unsupported?
    Test("test-extended-master-secret-extension-with-client-cert.py"),

    # no shared cipher
    Test("test-ecdsa-sig-flexibility.py"),

    # unsupported
    Test("test-encrypt-then-mac-renegotiation.py"),
    Test("test-encrypt-then-mac.py"),
    Test("test-extended-master-secret-extension.py"),
    Test("test-ffdhe-negotiation.py"),
    # unsupported. Expects the server to send the heartbeat extension
    Test("test-heartbeat.py"),

    # 29 succeed, 263 fail:
    #   'n extensions', 'n extensions last empty' n in 4086, 4096, 8192, 16383
    #   'fuzz ext length to n' n in [0..255] with the exception of 41...
    Test("test-extensions.py"),

    # Tests expect SH but we send unexpected_message or handshake_failure
    #   'Application data inside Client Hello'
    #   'Application data inside Client Key Exchange'
    #   'Application data inside Finished'
    Test("test-interleaved-application-data-and-fragmented-handshakes-in-renegotiation.py"),
    # Tests expect SH but we send handshake_failure
    #   'Application data before Change Cipher Spec'
    #   'Application data before Client Key Exchange'
    #   'Application data before Finished'
    Test("test-interleaved-application-data-in-renegotiation.py"),

    # broken test script
    # TypeError: '<' not supported between instances of 'int' and 'NoneType'
    Test("test-invalid-client-hello-w-record-overflow.py"),

    # Lots of failures. abrupt closure
    Test("test-invalid-client-hello.py"),

    # Test expects illegal_parameter, we send decode_error in ssl_srvr.c:1016
    # Need to check that this is correct.
    Test("test-invalid-compression-methods.py"),

    # abrupt closure
    # 'encrypted premaster set to all zero (n)' n in 256 384 512
    Test("test-invalid-rsa-key-exchange-messages.py"),

    # test expects illegal_parameter, we send unrecognized_name (which seems
    # correct according to rfc 6066?)
    Test("test-invalid-server-name-extension-resumption.py"),
    # let through some server names without sending an alert
    # again illegal_parameter vs unrecognized_name
    Test("test-invalid-server-name-extension.py"),

    Test("test-large-hello.py"),

    # 14 pass
    # 7 fail
    # 'n extensions', n in 4095, 4096, 4097, 8191, 8192, 8193, 16383,
    Test("test-large-number-of-extensions.py"),

    # 4 failures:
    #   'insecure (legacy) renegotiation with GET after 2nd handshake'
    #   'insecure (legacy) renegotiation with incomplete GET'
    #   'secure renegotiation with GET after 2nd handshake'
    #   'secure renegotiation with incomplete GET'
    Test("test-legacy-renegotiation.py"),

    # 1 failure (timeout): we don't send the unexpected_message alert
    # 'duplicate change cipher spec after Finished'
    Test("test-message-duplication.py"),

    # server should send status_request
    Test("test-ocsp-stapling.py"),

    # unexpected closure
    Test("test-openssl-3712.py"),

    # 3 failures:
    # 'big, needs fragmentation: max fragment - 16336B extension'
    # 'big, needs fragmentation: max fragment - 32768B extension'
    # 'maximum size: max fragment - 65531B extension'
    Test("test-record-layer-fragmentation.py"),

    # wants --reply-AD-size
    Test("test-record-size-limit.py"),

    # failed: 3 (expect an alert, we send AD)
    # 'try insecure (legacy) renegotiation with incomplete GET'
    # 'try secure renegotiation with GET after 2nd CH'
    # 'try secure renegotiation with incomplete GET'
    Test("test-renegotiation-disabled.py"),

    # 'resumption of safe session with NULL cipher'
    # 'resumption with cipher from old CH but not selected by server'
    Test("test-resumption-with-wrong-ciphers.py"),

    # 5 failures:
    #   'empty sigalgs'
    #   'only undefined sigalgs'
    #   'rsa_pss_pss_sha256 only'
    #   'rsa_pss_pss_sha384 only'
    #   'rsa_pss_pss_sha512 only'
    Test("test-sig-algs.py"),

    # 13 failures:
    #   'duplicated n non-rsa schemes' for n in 202 2342 8119 23741 32744
    #   'empty list of signature methods'
    #   'tolerance n RSA or ECDSA methods' for n in 215 2355 8132 23754
    #   'tolerance 32758 methods with sig_alg_cert'
    #   'tolerance max 32744 number of methods with sig_alg_cert'
    #   'tolerance max (32760) number of methods'
    Test("test-signature-algorithms.py"),

    # times out
    Test("test-ssl-death-alert.py"),

    # 17 pass, 13 fail. padding and truncation
    Test("test-truncating-of-client-hello.py"),

    # x448 tests need disabling plus x25519 corner cases need sorting out
    Test("test-x25519.py"),
])

tls12_unsupported_tests = TestGroup("TLSv1.2 for unsupported features", [
    # protocol_version
    Test("test-SSLv3-padding.py"),
])

# These tests take a ton of time to fail against an 1.3 server,
# so don't run them against 1.3 pending further investigation.
legacy_tests = TestGroup("Legacy protocol tests", [
    Test("test-sslv2-force-cipher-3des.py"),
    Test("test-sslv2-force-cipher-non3des.py"),
    Test("test-sslv2-force-cipher.py"),
    Test("test-sslv2-force-export-cipher.py"),
    Test("test-sslv2hello-protocol.py"),
])

all_groups = [
    tls13_tests,
    tls13_slow_tests,
    tls13_extra_cert_tests,
    tls13_failing_tests,
    tls13_slow_failing_tests,
    tls13_unsupported_tests,
    tls12_tests,
    tls12_slow_tests,
    tls12_failing_tests,
    tls12_unsupported_tests,
    legacy_tests,
]

failing_groups = [
    tls13_failing_tests,
    tls13_slow_failing_tests,
    tls12_failing_tests,
]

class TestRunner:
    """ Runs the given tests troups against a server and displays stats. """

    def __init__(
        self, timing=False, verbose=False, port=4433, use_tls1_3=True,
        dry_run=False, tests=[], scriptdir=tlsfuzzer_scriptdir,
    ):
        self.tests = []

        self.dryrun = dry_run
        self.use_tls1_3 = use_tls1_3
        self.port = str(port)
        self.scriptdir = scriptdir

        self.stats = []
        self.failed = []

        self.timing = timing
        self.verbose = verbose

    def add(self, title="tests", tests=[]):
        # tests.sort(key=lambda test: test.name)
        self.tests.append(TestGroup(title, tests))

    def add_group(self, group):
        self.tests.append(group)

    def run_script(self, test):
        script = test.name
        args = ["-p"] + [self.port] + test.args(self.use_tls1_3)

        if self.dryrun:
            if not self.verbose:
                args = []
            print(script , end=' ' if args else '')
            print(' '.join([f"\"{arg}\"" for arg in args]))
            return

        if self.verbose:
            print(script)
        else:
            print(f"{script[:68]:<72}", end=" ", flush=True)
        start = timer()
        test = subprocess.run(
            ["python3", os.path.join(self.scriptdir, script)] + args,
            capture_output=not self.verbose,
            text=True,
        )
        end = timer()
        self.stats.append((script, end - start))
        if test.returncode == 0:
            print("OK")
            return
        print("FAILED")
        self.failed.append(script)

        if self.verbose:
            return

        print('\n'.join(test.stdout.split("Test end\n", 1)[1:]), end="")

    def run(self):
        for group in self:
            print(f"Running {group.title} ...")
            for test in group:
                self.run_script(test)
        return not self.failed

    def __iter__(self):
        return iter(self.tests)

    def __del__(self):
        if self.timing and self.stats:
            total = 0.0
            for (script, time) in self.stats:
                print(f"{round(time, 2):6.2f} {script}")
                total += time
            print(f"{round(total, 2):6.2f} total")

        if self.failed:
            print("Failed tests:")
            print('\n'.join(self.failed))

class TlsServer:
    """ Spawns an s_server listening on localhost:port if necessary. """

    def __init__(self, port=4433):
        self.spawn = True
        # Check whether a server is already listening on localhost:port
        self.spawn = subprocess.run(
            ["nc", "-c", "-z", "-T", "noverify", "localhost", str(port)],
            stderr=subprocess.DEVNULL,
        ).returncode != 0

        if self.spawn:
            self.server = subprocess.Popen(
                [
                    "openssl",
                    "s_server",
                    "-accept",
                    str(port),
                    "-key",
                    "localhost.key",
                    "-cert",
                    "localhost.crt",
                    "-www",
                ],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.PIPE,
                text=True,
            )

        # Check whether the server talks TLSv1.3
        self.has_tls1_3 = True or subprocess.run(
            [
                "nc",
                "-c",
                "-z",
                "-T",
                "noverify",
                "-T",
                "protocols=TLSv1.3",
                "localhost",
                str(port),
            ],
            stderr=subprocess.DEVNULL,
        ).returncode == 0

        self.check()

    def check(self):
        if self.spawn and self.server.poll() is not None:
            print(self.server.stderr.read())
            raise RuntimeError(
                f"openssl s_server died. Return code: {self.server.returncode}."
            )
        if self.spawn:
            self.server.stderr.detach()

    def __del__(self):
        if self.spawn:
            self.server.terminate()

# Extract the arguments we pass to script
def defaultargs(script, has_tls1_3):
    return next(
        (test for group in all_groups for test in group if test.name == script),
        Test()
    ).args(has_tls1_3)

def list_or_missing(missing=True):
    tests = [test.name for group in all_groups for test in group]

    if missing:
        scripts = {
            f for f in os.listdir(tlsfuzzer_scriptdir) if f != "__pycache__"
        }
        missing = scripts - set(tests)
        if missing:
            print('\n'.join(sorted(missing)))
        exit(0)

    tests.sort()
    print('\n'.join(tests))
    exit(0)

def usage():
    print("Usage: python3 tlsfuzzer.py [-lmnstv] [-p port] [script [test...]]")
    print(" --help      help")
    print(" -f          run failing tests")
    print(" -l          list tests")
    print(" -m          list new tests after package update")
    print(" -n          do not run tests, but list the ones that would be run")
    print(" -p port     connect to this port - defaults to 4433")
    print(" -s          run slow tests")
    print(" -t          show timing stats at end")
    print(" -v          verbose output")
    exit(0)

def main():
    failing = False
    list = False
    missing = False
    dryrun = False
    port = 4433
    slow = False
    timing = False
    verbose = False

    argv = sys.argv[1:]
    opts, args = getopt.getopt(argv, "flmnp:stv", ["help"])
    for opt, arg in opts:
        if opt == '--help':
            usage()
        elif opt == '-f':
            failing = True
        elif opt == '-l':
            list = True
        elif opt == '-m':
            missing = True
        elif opt == '-n':
            dryrun = True
        elif opt == '-p':
            port = int(arg)
        elif opt == '-s':
            slow = True
        elif opt == '-t':
            timing = True
        elif opt == '-v':
            verbose = True
        else:
            raise ValueError(f"Unknown option: {opt}")

    if not os.path.exists(tlsfuzzer_scriptdir):
        print("package py3-tlsfuzzer is required for this regress")
        exit(1)

    if list and failing:
        failing = [test.name for group in failing_groups for test in group]
        failing.sort()
        print('\n'.join(failing))
        exit(0)

    if list or missing:
        list_or_missing(missing)

    tls_server = TlsServer(port)

    tests = TestRunner(timing, verbose, port, tls_server.has_tls1_3, dryrun)

    if args:
        (dir, script) = os.path.split(args[0])
        if dir and not dir == '.':
            tests.scriptdir = dir

        testargs = defaultargs(script, tls_server.has_tls1_3)

        tests.verbose = True
        tests.add("test from command line", [Test(script, testargs + args[1:])])

        exit(not tests.run())

    if failing:
        if tls_server.has_tls1_3:
            tests.add_group(tls13_failing_tests)
            if slow:
                tests.add_group(tls13_slow_failing_tests)
        tests.add_group(tls12_failing_tests)

    if tls_server.has_tls1_3:
        tests.add_group(tls13_tests)
        if slow:
            tests.add_group(tls13_slow_tests)
    else:
        tests.add_group(legacy_tests)

    tests.add_group(tls12_tests)
    if slow:
        tests.add_group(tls12_slow_tests)

    success = tests.run()
    del tests

    if not success:
        print("FAILED")
        exit(1)

if __name__ == "__main__":
    main()