#!/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()