- Add fail-closed wolfSSL small-math policy and vectors - Backport DHCP, EMS, and X.509 allocation fixes - Extend source override validation and operational documentation
165 lines
6.6 KiB
Python
165 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Offline compile-profile regression; never invokes PlatformIO or a device."""
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
import tempfile
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
HERE = Path(__file__).resolve().parent
|
|
GUARD = ROOT / 'cmake/wolf_crypto_policy.h'
|
|
VENDOR = ROOT / 'managed_components/wolfssl__wolfssl'
|
|
ENV = {**os.environ, 'CCACHE_DISABLE': '1'}
|
|
SMALL = ('CURVE25519_SMALL', 'ED25519_SMALL')
|
|
|
|
|
|
def run(args, cwd=ROOT, **kw):
|
|
return subprocess.run(args, cwd=cwd, env=ENV, timeout=60,
|
|
capture_output=True, text=True, **kw)
|
|
|
|
|
|
def require(result):
|
|
if result.returncode:
|
|
raise RuntimeError(result.stderr)
|
|
return result.stdout
|
|
|
|
|
|
def clean(entry):
|
|
args = entry.get('arguments') or shlex.split(entry['command'])
|
|
result = []
|
|
skip = False
|
|
for arg in args:
|
|
if skip:
|
|
skip = False
|
|
elif arg in ('-o', '-MF', '-MT', '-MQ'):
|
|
skip = True
|
|
elif arg not in ('-c', '-MD', '-MMD', '-MP'):
|
|
result.append(arg)
|
|
return result
|
|
|
|
|
|
def matrix():
|
|
with tempfile.TemporaryDirectory(prefix='wolf-policy-') as tmp:
|
|
tmp = Path(tmp)
|
|
settings = tmp / 'wolfssl/wolfcrypt/settings.h'
|
|
settings.parent.mkdir(parents=True)
|
|
settings.write_text('/* Resolved settings supplied by matrix. */\n')
|
|
base = ['HAVE_CURVE25519', 'HAVE_ED25519', *SMALL]
|
|
cases = [('valid', base, True)]
|
|
cases += [(f'missing {m}', [x for x in base if x != m], False) for m in base]
|
|
cases += [(m, base + [m], False) for m in
|
|
('WOLFSSL_CURVE25519_BLINDING', 'HAVE_CURVE448', 'HAVE_ED448')]
|
|
for label, defines, good in cases:
|
|
p = run(['cc', '-x', 'c', '-fsyntax-only', '-I' + str(tmp),
|
|
'-include', str(GUARD), *['-D' + x for x in defines], '-'], input='')
|
|
if (p.returncode == 0) != good or (not good and 'wolf crypto policy:' not in p.stderr):
|
|
raise RuntimeError(f'guard matrix failed: {label}: {p.stderr}')
|
|
print(f'PASS: {len(cases)} fail-closed guard cases')
|
|
|
|
|
|
def profiles(database, candidate):
|
|
entries = json.loads(database.read_text())
|
|
suffixes = ('wolfcrypt/src/curve25519.c', 'wolfcrypt/src/ed25519.c',
|
|
'wolfcrypt/src/fe_operations.c', 'wolfcrypt/src/ge_operations.c',
|
|
'wolfcrypt/src/fe_low_mem.c', 'wolfcrypt/src/ge_low_mem.c',
|
|
'wolfssl__wolfssh/src/ssh.c',
|
|
'security_overrides/wolfssh_internal/internal.c',
|
|
'src/ssh_transport.c', 'src/ssh_security.c')
|
|
for suffix in suffixes:
|
|
matches = [e for e in entries if e['file'].endswith('/' + suffix)]
|
|
if len(matches) != 1:
|
|
raise RuntimeError(f'expected one compile entry for {suffix}: {len(matches)}')
|
|
entry = matches[0]
|
|
command = clean(entry)
|
|
if candidate:
|
|
command += ['-D' + x for x in SMALL] + ['-include', str(GUARD)]
|
|
elif not any('wolf_crypto_policy.h' in x for x in command):
|
|
raise RuntimeError(f'{suffix}: missing production guard; parent must reconfigure/build')
|
|
text = require(run(command + ['-E', '-dM'], cwd=entry['directory']))
|
|
macros = dict(re.findall(r'^#define (\w+)(?: (.*))?$', text, re.M))
|
|
for name in (*SMALL, 'HAVE_CURVE25519', 'HAVE_ED25519',
|
|
'WC_RNG_SEED_CB', 'WOLFSSL_ED25519_STREAMING_VERIFY',
|
|
'NO_WOLFSSL_ESP32_CRYPT_AES', 'NO_WOLFSSL_ESP32_CRYPT_HASH'):
|
|
if name not in macros:
|
|
raise RuntimeError(f'{suffix}: missing resolved {name}')
|
|
if not any(x in macros for x in ('__XTENSA__', '__xtensa__')):
|
|
raise RuntimeError('expected actual Xtensa compiler')
|
|
for name in ('WOLFSSL_CURVE25519_BLINDING', 'HAVE_CURVE448', 'HAVE_ED448'):
|
|
if name in macros:
|
|
raise RuntimeError(f'{suffix}: unexpected {name}')
|
|
require(run(command + ['-fsyntax-only'], cwd=entry['directory']))
|
|
print(f'PASS: {"CANDIDATE replay" if candidate else "production"} macros + syntax: {suffix}')
|
|
# Exercise failures with the real installed settings, not only fake headers.
|
|
entry = matches[0]
|
|
base = [x for x in clean(entry) if x not in ['-D' + m for m in SMALL]
|
|
and 'wolf_crypto_policy.h' not in x]
|
|
for missing in SMALL:
|
|
command = base + ['-D' + x for x in SMALL if x != missing]
|
|
command += ['-U' + missing, '-include', str(GUARD), '-E']
|
|
p = run(command, cwd=entry['directory'])
|
|
if p.returncode == 0 or 'wolf crypto policy:' not in p.stderr:
|
|
raise RuntimeError(f'real settings accepted missing {missing}: {p.stderr}')
|
|
print('PASS: real target settings reject either missing small flag')
|
|
|
|
|
|
def vectors():
|
|
with tempfile.TemporaryDirectory(prefix='wolf-vectors-') as tmp:
|
|
tmp = Path(tmp)
|
|
(tmp / 'user_settings.h').write_text('''
|
|
#define WOLFCRYPT_ONLY
|
|
#define NO_ASN
|
|
#define NO_RSA
|
|
#define NO_DH
|
|
#define NO_DSA
|
|
#define NO_AES
|
|
#define NO_DES3
|
|
#define NO_RC4
|
|
#define NO_MD4
|
|
#define NO_MD5
|
|
#define NO_SHA
|
|
|
|
#define NO_HMAC
|
|
#define WC_NO_RNG
|
|
#define NO_FILESYSTEM
|
|
#define NO_WRITEV
|
|
#define NO_DEV_RANDOM
|
|
#define NO_MAIN_DRIVER
|
|
#define HAVE_CURVE25519
|
|
#define HAVE_ED25519
|
|
#define CURVE25519_SMALL
|
|
#define ED25519_SMALL
|
|
#define WOLFSSL_SHA512
|
|
#define WOLFSSL_ED25519_STREAMING_VERIFY
|
|
''')
|
|
sources = ['curve25519.c', 'ed25519.c', 'fe_operations.c',
|
|
'ge_operations.c', 'fe_low_mem.c', 'ge_low_mem.c', 'sha512.c']
|
|
require(run(['cc', '-std=c99', '-O2', '-DWOLFSSL_USER_SETTINGS',
|
|
'-I' + str(tmp), '-I' + str(VENDOR), '-include', str(GUARD),
|
|
'-ffunction-sections', '-fdata-sections',
|
|
str(HERE / 'vectors.c'),
|
|
*[str(VENDOR / 'wolfcrypt/src' / s) for s in sources],
|
|
'-Wl,--gc-sections', '-o', str(tmp / 'vectors')]))
|
|
print(require(run([str(tmp / 'vectors')])).strip())
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--compile-commands', type=Path,
|
|
default=ROOT / '.pio/build/esp32-s3-devkitc-1-n16r8/compile_commands.json')
|
|
parser.add_argument('--candidate', action='store_true',
|
|
help='inject proposed flags/guard into old commands; NOT production evidence')
|
|
parser.add_argument('--host-only', action='store_true')
|
|
args = parser.parse_args()
|
|
matrix()
|
|
vectors()
|
|
if not args.host_only:
|
|
profiles(args.compile_commands, args.candidate)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|