Merge 49e1934eee9cd11330e3102f9e23284c3167b2bf into 3bb373867917b674265067cbd38b9d252c43d014

This commit is contained in:
Gilles Peskine 2026-07-20 21:12:14 +01:00 committed by GitHub
commit d2501d4145
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 252 additions and 0 deletions

191
tests/scripts/check_ssl_opt.py Executable file
View File

@ -0,0 +1,191 @@
#!/usr/bin/env python3
"""
Check some requires_xxx in ssl-opt.sh.
Usage: check_ssl_opt.py [PATH/TO/ssl-opt.sh]
"""
import os
import re
import subprocess
import sys
import tempfile
from typing import Callable, Optional, Pattern, Iterator, TypeVar
T = TypeVar('T') #pylint: disable=invalid-name
SOURCE_BLOCKS = (
(re.compile(r'run_tests?_\w+ *\(\)'), re.compile(r'}')),
(re.compile(r'(requires|run)_\w+ (?! *\()'), re.compile(r'\n'))
)
def transform_file(inp, out) -> None:
"""Transform ssl-opt.sh into a script that just dumps data about test cases. """
copy_until = None #type: Optional[Pattern]
for line in inp:
if not copy_until:
for (from_re, until_re) in SOURCE_BLOCKS:
if from_re.match(line):
copy_until = until_re
break
if copy_until:
line = re.sub(r'\A( *)requires_', r'\1requires ', line)
out.write(line)
if copy_until.match(line):
copy_until = None
else:
out.write('\n')
def run_adapted_file(filename: str) -> str:
cmd = [os.path.join(os.path.dirname(__file__),
'run-ssl-opt-for-check.sh'),
filename]
#pylint: disable=try-except-raise
try:
return subprocess.check_output(cmd).decode('ascii')
except subprocess.CalledProcessError:
#import pdb; pdb.set_trace()
raise
class Test:
"""Information about one test case of ssl-opt.sh."""
def __init__(self, data: str) -> None:
parts = data.split('\036')
(self.description, requirements,
self.server, self.proxy, self.client, self.ret,
self.options) = parts
self.requirements = frozenset(requirements.split(';'))
def requires_option(self, symbol: str) -> bool:
return 'config_enabled ' + symbol in self.requirements
@staticmethod
def is_mbedtls(cmd: str) -> bool:
return cmd.startswith('ssl_client2') or cmd.startswith('ssl_server2')
def get_mbedtls_runtime_option(self, cmd: str, name: str) -> Optional[str]:
"""Look for an option passed on an mbedtls utility command line."""
if not self.is_mbedtls(cmd):
return None
m = re.search(r' ' + name + '=([^ ]*)', cmd)
if m:
return m.group(1)
else:
return None
def enables_option(self, name: str) -> bool:
"""True if the client or server is Mbed TLS and has name=... with a nonzero value."""
for cmd in (self.client, self.server):
value = self.get_mbedtls_runtime_option(cmd, name)
if value is not None and value != '0':
return True
return False
def max_option(self,
name: str,
func: Callable[[Optional[str]], T]) -> T:
"""Maximum value of the specified option between the client and the server.
Call ``func(value)`` on each option value. ``value`` is ``None`` if
the option was not passed.
"""
return max(func(self.get_mbedtls_runtime_option(cmd, name))
for cmd in (self.client, self.server))
def match_options(self, symbol: str, *opts: str) -> Iterator[str]:
"""Match compile-time configuration with command line
Check that the compile-time configuration symbol is required iff
the command-line option opt is used with a nonzero value.
"""
requires = self.requires_option(symbol)
enables = any(self.enables_option(opt) for opt in opts)
if requires and not enables:
yield 'Configuration {} is required but not used'.format(symbol)
if not requires and enables:
yield 'Configuration {} is used but not required'.format(symbol)
# This list is deliberately partial and the lengths are not exact.
# The objective is to match the highly simplified declared requirements
# in ssl-opt.sh rather than the truth.
MAX_CONTENT_LEN_FOR_CRT_FILE = {
'data_files/server7_int-ca.crt': 2048,
}
def max_content_len_for_crt_file(self, filename: Optional[str]) -> int:
if filename is None:
return 0
return self.MAX_CONTENT_LEN_FOR_CRT_FILE.get(filename, 0)
def max_content_len(self) -> Iterator[str]:
"""Check the plausibility of requires_max_content_len (or lack thereof)."""
max_frag_len = self.max_option('max_frag_len',
lambda x: 0 if x is None else int(x))
mtu = self.max_option('mtu', lambda x: 0 if x is None else int(x))
cert = self.max_option('crt_file', self.max_content_len_for_crt_file)
required = 0
for req in self.requirements:
m = re.match(r'max_content_len (\w+)', req)
if m:
required = max(required, int(m.group(1)))
if not required:
if max_frag_len:
yield ('max_frag_len={} but no requires_max_content_len'
.format(max_frag_len))
if mtu:
pass #yield 'mtu={} but no requires_max_content_len'.format(mtu)
if cert:
pass #yield 'cert length up to {} but no requires_max_content_len'.format(cert)
return
if not (max_frag_len or mtu or cert):
yield ('requires_max_content_len {} for no discernible reason'
.format(required))
if max(max_frag_len, mtu, cert) not in (required, required - 1):
yield ('requires_max_content_len {} but max_frag_len={}, mtu={}'
.format(required, max_frag_len, mtu))
def problems(self) -> Iterator[str]:
"""Perform all checks. Iterate over the problems encountered."""
yield from self.match_options('MBEDTLS_SSL_DTLS_CONNECTION_ID',
'cid', 'cid_renego')
yield from self.match_options('MBEDTLS_SSL_CONTEXT_SERIALIZATION',
'serialize')
yield from self.match_options('MBEDTLS_X509_TRUSTED_CERTIFICATE_CALLBACK',
'ca_callback')
yield from self.max_content_len()
def analyze_output(output: str) -> bool:
"""Analyze the output of run-ssl-opt-for-check.
Return False if there are problems.
"""
ok = True
for part in output.split('\035'):
if not part:
continue
test = Test(part)
for problem in test.problems():
ok = False
print(test.description + ';' + problem)
return ok
def check_ssl_opt(original_file_name: str) -> bool:
"""Analyze the test cases in ssl-opt.sh for missing or spurious requirements."""
dirname = os.path.dirname(os.path.abspath(original_file_name))
basename = os.path.basename(original_file_name)
with tempfile.NamedTemporaryFile(
prefix=os.path.join(dirname, basename + '-check-'), suffix='.sh',
mode='tw', newline='\n'
) as transformed_file:
with open(original_file_name) as original_file:
transform_file(original_file, transformed_file)
transformed_file.flush()
output = run_adapted_file(transformed_file.name)
return analyze_output(output)
if __name__ == '__main__':
sys.exit(not check_ssl_opt(
sys.argv[1] if len(sys.argv) > 1 else
os.path.join(os.path.dirname(__file__), os.path.pardir, 'ssl-opt.sh')
))

View File

@ -0,0 +1,61 @@
#!/bin/sh
# Auxiliary script for check_ssl_opt.py. This script runs an adapted version
# of ssl-opt.sh with some variables and functions defined for the sake of
# the checks. See check_ssl_opt.py for details.
set -e -u
G_CLI="gnutls-cli"
G_NEXT_CLI="next/gnutls-cli"
G_NEXT_SRV="next/gnutls-serv"
G_SRV="gnutls-serv"
MAX_CONTENT_LEN=-111111
MAX_IM_CA=8
O_CLI="openssl s_client"
O_SRV="openssl s_server"
P_PXY="udp_proxy"
P_CLI="ssl_client2"
P_SRV="ssl_server2"
SESSION="session.tmp"
SKIP_NEXT=
SRV_PORT=4433
TCP_CLIENT=":"
client_needs_more_time () {
:
}
fragments_for_write () {
echo ...
}
requires () {
requirements="$requirements;$*"
}
# run_test DESCRIPTION [-p PROXY] SERVER CLIENT RET ...
run_test () {
description=$1; shift
if [ "$1" = "-p" ]; then
proxy=$2
shift 2
else
proxy=
fi
server=$1
client=$2
ret=$3
shift 3
printf '%s\036%s\036%s\036%s\036%s\036%s\036%s\035' \
"$description" \
"${requirements%;}" \
"$server " "$proxy " "$client " "$ret" "$*"
requirements=
}
requirements=
. "$1"