Files
Gilles Peskine 37e13da3c2 Fix --list-tasks parsing
Don't require the file and task arguments when `--list-tasks` is passed.

Signed-off-by: Gilles Peskine <[email protected]>
2026-04-08 16:51:22 +02:00

154 lines
6.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""Validate logs from ssl_client2 and ssl_server2.
On success, print nothing and return 0.
On a validation failure, print a short error message and return 1.
On a command line or parse error, die with an exception and return 1.
"""
# Copyright The Mbed TLS Contributors
# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
import argparse
import sys
from typing import Callable, Dict, List, Optional
from mbedtls_framework import ssl_log_parser
# None: validation succeeded.
# str: validation failed; the string is a human-readable explanation of the failure.
Validation = Optional[str]
def match_random(client_log: ssl_log_parser.Info,
server_log: ssl_log_parser.Info) -> Validation:
"""Check that both sides have the same idea of client_random and server_random."""
client_client_randoms = client_log.dumps['client hello, random bytes']
client_server_randoms = client_log.dumps['server hello, random bytes']
server_client_randoms = server_log.dumps['client hello, random bytes']
server_server_randoms = server_log.dumps['server hello, random bytes']
if len(client_client_randoms) != len(server_client_randoms):
return ('Client and server disagree on the number of client_random ' +
f'({len(client_client_randoms)} != {len(server_client_randoms)})')
if len(client_server_randoms) != len(server_server_randoms):
return ('Client and server disagree on the number of server_random ' +
f'({len(client_server_randoms)} != {len(server_server_randoms)})')
for n, (c, s) in enumerate(zip(client_client_randoms, server_client_randoms)):
if c != s:
return f'Client and server disagree on client random #{n}'
for n, (c, s) in enumerate(zip(client_server_randoms, server_server_randoms)):
if c != s:
return f'Client and server disagree on server random #{n}'
return None
def distinct_server_ephemeral(client_log: ssl_log_parser.Info,
_server_log: ssl_log_parser.Info) -> Validation:
"""Check that server ephemeral keys as seen from the client are not repeated."""
# The current implementation does not handle cases where the client
# receives and discards a legitimate resend of the ServerKeyExchange
# message in DTLS.
if 'DHM: GY' in client_log.dumps:
values = client_log.dumps['DHM: GY']
elif 'server ephemeral public key' in client_log.dumps:
values = client_log.dumps['server ephemeral public key']
else:
return 'Ephemeral public keys not found in client log'
if len(values) < 2:
return 'Fewer than two server ephemeral public keys found'
seen = {} #type: Dict[str, int]
for n, v in enumerate(values):
if v in seen:
return f'server ephemeral public key #{n} repeats #{seen[v]}'
seen[v] = n
return None
def distinct_server_random(client_log: ssl_log_parser.Info,
_server_log: ssl_log_parser.Info) -> Validation:
"""Check that server randoms as seen from the client are not repeated."""
# The current implementation does not handle cases where the client
# receives and discards a legitimate resend of the server hello in DTLS.
values = client_log.dumps['server hello, random bytes']
if len(values) < 2:
return 'Fewer than two server_random found'
def random_part(hex_data: str) -> str:
# In TLS <=1.2, the first 4 bytes (8 hex digits) are the time,
# and may differ even if the actually random part is repeated.
# The last 8 bytes (16 hex digits) are not random in TLS 1.2 when
# the server also supports 1.3 (they are forced to b'DOWNGR\001').
return hex_data[8:48]
seen = {} #type: Dict[str, int]
for n, v in enumerate(values):
r = random_part(v)
if r in seen:
return f'server_random #{n} repeats #{seen[r]}'
seen[r] = n
return None
Task = Callable[[ssl_log_parser.Info, ssl_log_parser.Info], Validation]
TASKS = {
'distinct_server_ephemeral': distinct_server_ephemeral,
'distinct_server_random': distinct_server_random,
'match_random': match_random,
} #type: Dict[str, Task]
def validate(client_log: ssl_log_parser.Info,
server_log: ssl_log_parser.Info,
tasks: List[str]) -> Validation:
"""Perform validation tasks on a pair of matching logs.
Return None if the validation succeeds, a human-oriented error message
otherwise.
"""
for task_name in tasks:
task = TASKS[task_name]
outcome = task(client_log, server_log)
if outcome is not None:
return outcome
return None
def main() -> int:
"""Command line entry point."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--list-tasks',
action='store_true',
help='List available tasks and exit')
parser.add_argument('client_log', metavar='CLIENT_LOG_FILE',
nargs='?',
help='Client log file ($CLI_OUT or ?-cli-*.log)')
parser.add_argument('server_log', metavar='SERVER_LOG_FILE',
nargs='?',
help='Server log file ($SRV_OUT or ?-srv-*.log)')
parser.add_argument('tasks', metavar='TASK',
nargs='*',
help='Tasks to perform (use --list-tasks to see supported task names)')
args = parser.parse_args()
if args.list_tasks:
for task_name in sorted(TASKS.keys()):
print(task_name)
return 0
if args.client_log is None or args.server_log is None:
parser.error('the following arguments are required: CLIENT_LOG_FILE SERVER_LOG_FILE')
if not args.tasks:
parser.error('at least one TASK is required')
client_log = ssl_log_parser.parse_log_file(args.client_log)
server_log = ssl_log_parser.parse_log_file(args.server_log)
outcome = validate(client_log, server_log, args.tasks)
if outcome is None:
return 0
else:
if outcome and outcome[-1] != '\n':
outcome += '\n'
sys.stderr.write(outcome)
return 1
if __name__ == '__main__':
sys.exit(main())