Revision control
Copy as Markdown
Other Tools
#!/usr/bin/env python3
# (C) 2025 Jack Lloyd
# 2025 René Meusel, Rohde & Schwarz Cybersecurity
#
# Botan is released under the Simplified BSD License (see license.txt)
#
import subprocess
import os
import sys
import argparse
import tempfile
import platform
from textwrap import indent
from enum import Enum
SCRIPT_LOCATION = os.path.dirname(os.path.abspath(__file__))
GDB_EXTENSION = os.path.join(SCRIPT_LOCATION, "gdb", "strubtest.py")
class CPU(Enum):
ANY = "any"
X86_64 = "x86_64"
AARCH64 = "aarch64"
@staticmethod
def current():
cpu = platform.machine().lower()
if cpu in ['x86_64', 'amd64']:
return CPU.X86_64
elif cpu in ['aarch64', 'arm64']:
return CPU.AARCH64
else:
raise ValueError(f"Unsupported or unknown CPU architecture: {cpu}")
class StrubTest:
def __init__(self, symbol, inferior_cmdline, masked_cpuid_bits = None, cpu = CPU.ANY, expect_fail = False):
self.symbol = symbol
self.inferior_cmdline = inferior_cmdline
self.masked_cpuid_bits = masked_cpuid_bits
self.cpu = cpu
self.expect_fail = expect_fail
@property
def gdb_command(self):
return ["gdb", "-x", GDB_EXTENSION,
"-ex", f"strubtest {self.symbol}",
"-ex", "run",
"--batch",
"--args", *self.inferior_cmdline]
@property
def rendered_gdb_command(self):
return " ".join([token if " " not in token else f"'{token}'" for token in self.gdb_command])
@property
def environment(self):
return {"BOTAN_CLEAR_CPUID": ",".join(self.masked_cpuid_bits)} if self.masked_cpuid_bits else {}
@property
def rendered_environment(self):
full_env = os.environ.copy()
full_env.update(self.environment)
return full_env
@property
def runnable(self):
return True if self.cpu == CPU.ANY else self.cpu == CPU.current()
def run(self):
print(f"Checking {self.symbol}... ", end="")
if not self.runnable:
return self._skip("incompatible platform")
try:
proc = subprocess.run(self.gdb_command, capture_output=True, check=False, env=self.rendered_environment)
except subprocess.SubprocessError as ex:
return self._fail("subprocess failure", exception=ex)
if proc.returncode != 0:
return self._fail("nonzero error code", proc_result=proc)
if "Error: " in proc.stderr.decode("utf-8"):
return self._fail("fail", proc_result=proc, may_be_expected=True)
if "Success: " in proc.stdout.decode("utf-8"):
return self._succeed(proc_result=proc)
else:
return self._fail("never invoked", proc_result=proc)
def _print_debug_report(self, proc_result = None, exception = None):
print(f" ran: {self.rendered_gdb_command}")
if self.environment:
print(" Environment:")
print(indent("\n".join([f"{k}={v}" for (k,v) in self.environment.items()]), " " * 6))
if exception:
print(" Exception:")
print(indent(str(exception), " " * 6))
if proc_result:
if proc_result.stdout:
print(" stdout:")
print(indent(proc_result.stdout.decode("utf-8"), " " * 6))
if proc_result.stderr:
print(" stderr:")
print(indent(proc_result.stderr.decode("utf-8"), " " * 6))
def _fail(self, errmsg, proc_result = None, exception = None, may_be_expected = False):
if may_be_expected and self.expect_fail:
print(f"{errmsg} (expected)")
return True
else:
print(errmsg)
self._print_debug_report(proc_result=proc_result, exception=exception)
return False
def _succeed(self, proc_result = None):
if not self.expect_fail:
print("ok")
else:
print("ok (unexpected)")
self._print_debug_report(proc_result=proc_result)
return not self.expect_fail
def _skip(self, reason):
print(f"skipped ({reason})")
return True
def dummy_file(size = 1024):
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
temp_file.write(os.urandom(size))
return temp_file.name
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--botan-cli', required=True)
args = parser.parse_args()
cli = args.botan_cli
myfile = dummy_file()
tests = [
# This is a self-test, it is expected to fail because the version
# information is naturally not annotated for stack scrubbing.
StrubTest("Botan::version_string", [cli, "version", "--full"], expect_fail=True),
# Below is a list of all strub-annotated symbols. Each of these
# symbols is tested by running the Botan CLI with GDB and checking
# that the stack scrubbing was performed correctly.
StrubTest("Botan::SHA_256::compress_digest", [cli, "hash", "--algo=SHA-256", myfile]),
StrubTest("Botan::SHA_256::compress_digest_x86", [cli, "hash", "--algo=SHA-256", myfile], cpu=CPU.X86_64),
StrubTest("Botan::SHA_256::compress_digest_armv8", [cli, "hash", "--algo=SHA-256", myfile], cpu=CPU.AARCH64),
StrubTest("Botan::SHA_256::compress_digest_x86_avx2", [cli, "hash", "--algo=SHA-256", myfile], cpu=CPU.X86_64, masked_cpuid_bits=["intel_sha"]),
StrubTest("Botan::SHA_256::compress_digest_x86_simd", [cli, "hash", "--algo=SHA-256", myfile], cpu=CPU.X86_64, masked_cpuid_bits=["intel_sha", "avx2"]),
]
results = [test.run() for test in tests]
os.remove(myfile)
return 1 if any(not ok for ok in results) else 0
if __name__ == '__main__':
sys.exit(main())