Reference implementations of PQC
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

298 rivejä
10 KiB

  1. import atexit
  2. import functools
  3. import logging
  4. import os
  5. import secrets
  6. import shutil
  7. import string
  8. import subprocess
  9. import sys
  10. import unittest
  11. from functools import lru_cache
  12. import pqclean
  13. @atexit.register
  14. def cleanup_testcases():
  15. """Clean up any remaining isolated test dirs"""
  16. print("Cleaning up testcases directory",
  17. file=sys.stderr)
  18. for dir_ in TEST_TEMPDIRS:
  19. shutil.rmtree(dir_, ignore_errors=True)
  20. TEST_TEMPDIRS = []
  21. ALPHABET = string.ascii_letters + string.digits + '_'
  22. def mktmpdir(parent, prefix):
  23. """Returns a unique directory name"""
  24. uniq = ''.join(secrets.choice(ALPHABET) for i in range(8))
  25. return os.path.join(parent, "{}_{}".format(prefix, uniq))
  26. def isolate_test_files(impl_path, test_prefix,
  27. dir=os.path.join('..', 'testcases')):
  28. """Isolates the test files in a separate directory, to help parallelise.
  29. Especially Windows is problematic and needs isolation of all test files:
  30. its build process will create .obj files EVERYWHERE.
  31. """
  32. try:
  33. os.mkdir(dir)
  34. except FileExistsError:
  35. pass
  36. test_dir = mktmpdir(dir, test_prefix)
  37. test_dir = os.path.abspath(test_dir)
  38. TEST_TEMPDIRS.append(test_dir)
  39. # the implementation will go here.
  40. scheme_dir = os.path.join(test_dir, 'crypto_bla', 'scheme')
  41. new_impl_dir = os.path.abspath(os.path.join(scheme_dir, 'impl'))
  42. def initializer():
  43. """Isolate the files to be tested"""
  44. # Create layers in folder structure
  45. os.makedirs(scheme_dir)
  46. # Create test dependencies structure
  47. os.mkdir(os.path.join(test_dir, 'test'))
  48. # Copy common files (randombytes.c, aes.c, ...)
  49. shutil.copytree(
  50. os.path.join('..', 'common'), os.path.join(test_dir, 'common'))
  51. # Copy makefiles
  52. shutil.copy(os.path.join('..', 'test', 'Makefile'),
  53. os.path.join(test_dir, 'test', 'Makefile'))
  54. shutil.copy(os.path.join('..', 'test', 'Makefile.Microsoft_nmake'),
  55. os.path.join(test_dir, 'test', 'Makefile.Microsoft_nmake'))
  56. # Copy directories with support files
  57. for d in ['common', 'test_common', 'crypto_sign', 'crypto_kem']:
  58. shutil.copytree(
  59. os.path.join('..', 'test', d),
  60. os.path.join(test_dir, 'test', d)
  61. )
  62. shutil.copytree(impl_path, new_impl_dir)
  63. def destructor():
  64. """Clean up the isolated files"""
  65. shutil.rmtree(test_dir, ignore_errors=True)
  66. return (test_dir, new_impl_dir, initializer, destructor)
  67. def run_subprocess(command, working_dir='.', env=None, expected_returncode=0,
  68. print_output=True):
  69. """
  70. Helper function to run a shell command and report success/failure
  71. depending on the exit status of the shell command.
  72. """
  73. if env is not None:
  74. env_ = os.environ.copy()
  75. env_.update(env)
  76. env = env_
  77. # Note we need to capture stdout/stderr from the subprocess,
  78. # then print it, which the unittest will then capture and
  79. # buffer appropriately
  80. print(working_dir + " > " + " ".join(command))
  81. result = subprocess.run(
  82. command,
  83. stdout=subprocess.PIPE,
  84. stderr=subprocess.STDOUT,
  85. cwd=working_dir,
  86. env=env,
  87. )
  88. if print_output:
  89. print(result.stdout.decode('utf-8'))
  90. if expected_returncode is not None:
  91. assert result.returncode == expected_returncode, \
  92. "Got unexpected return code {}".format(result.returncode)
  93. else:
  94. return (result.returncode, result.stdout.decode('utf-8'))
  95. return result.stdout.decode('utf-8')
  96. def make(*args, working_dir='.', env=None, expected_returncode=0, **kwargs):
  97. """
  98. Runs a make target in the specified working directory
  99. Usage:
  100. make('clean', 'targetb', SCHEME='bla')
  101. """
  102. if os.name == 'nt':
  103. make_command = ['nmake', '/f', 'Makefile.Microsoft_nmake',
  104. '/NOLOGO', '/E']
  105. # we need SCHEME_UPPERCASE and IMPLEMENTATION_UPPERCASE with nmake
  106. for envvar in ['IMPLEMENTATION', 'SCHEME']:
  107. if envvar in kwargs:
  108. kwargs['{}_UPPERCASE'.format(envvar)] = (
  109. kwargs[envvar].upper().replace('-', ''))
  110. else:
  111. make_command = ['make']
  112. return run_subprocess(
  113. [
  114. *make_command,
  115. *['{}={}'.format(k, v) for k, v in kwargs.items()],
  116. *args,
  117. ],
  118. working_dir=working_dir,
  119. env=env,
  120. expected_returncode=expected_returncode,
  121. )
  122. def skip_windows(message="This test is not supported on Windows"):
  123. def wrapper(f):
  124. @functools.wraps(f)
  125. def skip_windows(*args, **kwargs):
  126. raise unittest.SkipTest(message)
  127. if os.name == 'nt':
  128. return skip_windows
  129. else:
  130. return f
  131. return wrapper
  132. @lru_cache(maxsize=None)
  133. def ensure_available(executable):
  134. """
  135. Checks if a command is available.
  136. If a command MUST be available, because we are in a CI environment,
  137. raises an AssertionError.
  138. In the docker containers, on Travis and on Windows, CI=true is set.
  139. """
  140. path = shutil.which(executable)
  141. if path:
  142. return path
  143. # Installing clang-tidy on LLVM will be too much of a mess.
  144. if ((executable == 'clang-tidy' and sys.platform == 'darwin')
  145. or 'CI' not in os.environ):
  146. raise unittest.SkipTest(
  147. "{} is not available on PATH. Install it to run this test.{}"
  148. .format(executable, "" if not os.name == 'nt'
  149. else "On Windows, make sure to add it to PATH")
  150. )
  151. raise AssertionError("{} not available on CI".format(executable))
  152. def permit_test(testname, *args, **kwargs):
  153. if len(args) == 0:
  154. thing = list(kwargs.values())[0]
  155. else:
  156. thing = args[0]
  157. if 'PQCLEAN_ONLY_TESTS' in os.environ:
  158. if not(testname.lower() in os.environ['PQCLEAN_ONLY_TESTS'].lower().split(',')):
  159. return False
  160. if 'PQCLEAN_SKIP_TESTS' in os.environ:
  161. if testname.lower() in os.environ['PQCLEAN_SKIP_TESTS'].lower().split(','):
  162. return False
  163. if isinstance(thing, pqclean.Implementation):
  164. scheme = thing.scheme
  165. elif isinstance(thing, pqclean.Scheme):
  166. scheme = thing
  167. else:
  168. return True
  169. if 'PQCLEAN_ONLY_TYPES' in os.environ:
  170. if not(scheme.type.lower() in os.environ['PQCLEAN_ONLY_TYPES'].lower().split(',')):
  171. return False
  172. if 'PQCLEAN_SKIP_TYPES' in os.environ:
  173. if scheme.type.lower() in os.environ['PQCLEAN_SKIP_TYPES'].lower().split(','):
  174. return False
  175. if 'PQCLEAN_ONLY_SCHEMES' in os.environ:
  176. if not(scheme.name.lower() in os.environ['PQCLEAN_ONLY_SCHEMES'].lower().split(',')):
  177. return False
  178. if 'PQCLEAN_SKIP_SCHEMES' in os.environ:
  179. if scheme.name.lower() in os.environ['PQCLEAN_SKIP_SCHEMES'].lower().split(','):
  180. return False
  181. if 'PQCLEAN_ONLY_DIFF' in os.environ:
  182. if shutil.which('git') is not None:
  183. # if we're on a non-master branch, and the only changes are in schemes,
  184. # only run tests on those schemes
  185. branch_result = subprocess.run(
  186. ['git', 'status', '--porcelain=2', '--branch'],
  187. stdout=subprocess.PIPE,
  188. stderr=subprocess.STDOUT,
  189. cwd="..",
  190. )
  191. # ensure we're in a working directory
  192. if branch_result.returncode != 0:
  193. return True
  194. # ensure we're not on master branch
  195. for branch_line in branch_result.stdout.decode('utf-8').splitlines():
  196. tokens = branch_line.split(' ')
  197. if tokens[0] == '#' and tokens[1] == 'branch.head':
  198. if tokens[2] == 'master':
  199. return True
  200. # where are there changes?
  201. diff_result = subprocess.run(
  202. ['git', 'diff', '--name-only', 'origin/master'],
  203. stdout=subprocess.PIPE,
  204. stderr=subprocess.STDOUT
  205. )
  206. assert diff_result.returncode == 0, \
  207. "Got unexpected return code {}".format(diff_result.returncode)
  208. for diff_line in diff_result.stdout.decode('utf-8').splitlines():
  209. # Git still returns UNIX-style paths on Windows, normalize
  210. diff_line = os.path.normpath(diff_line)
  211. # don't skip test if there are any changes outside schemes
  212. if (not diff_line.startswith('crypto_kem') and
  213. not diff_line.startswith('crypto_sign') and
  214. not diff_line.startswith(os.path.join('test', 'duplicate_consistency'))):
  215. logging.info("Running all tests as there are changes "
  216. "outside of schemes")
  217. return True
  218. # do test if the scheme in question has been changed
  219. if diff_line.startswith(thing.path(base='')):
  220. return True
  221. # do test if the scheme's duplicate_consistency files have been changed
  222. if diff_line.startswith(os.path.join('test', 'duplicate_consistency', scheme.name.lower())):
  223. return True
  224. # there were no changes outside schemes, and the scheme in question had no diffs
  225. return False
  226. return True
  227. def filtered_test(func):
  228. funcname = func.__name__[len("test_"):]
  229. @functools.wraps(func)
  230. def wrapper(*args, **kwargs):
  231. if permit_test(funcname, *args, **kwargs):
  232. return func(*args, **kwargs)
  233. else:
  234. raise unittest.SkipTest("Test disabled by filter")
  235. return wrapper
  236. @lru_cache(maxsize=1)
  237. def get_cpu_info():
  238. the_info = None
  239. while the_info is None or 'flags' not in the_info:
  240. import cpuinfo
  241. the_info = cpuinfo.get_cpu_info()
  242. # CPUINFO is unreliable on Travis CI Macs
  243. if 'CI' in os.environ and sys.platform == 'darwin':
  244. the_info['flags'] = [
  245. 'aes', 'apic', 'avx1.0', 'clfsh', 'cmov', 'cx16', 'cx8', 'de',
  246. 'em64t', 'erms', 'f16c', 'fpu', 'fxsr', 'lahf', 'mca', 'mce',
  247. 'mmx', 'mon', 'msr', 'mtrr', 'osxsave', 'pae', 'pat', 'pcid',
  248. 'pclmulqdq', 'pge', 'popcnt', 'pse', 'pse36', 'rdrand',
  249. 'rdtscp', 'rdwrfsgs', 'sep', 'smep', 'ss', 'sse', 'sse2',
  250. 'sse3', 'sse4.1', 'sse4.2', 'ssse3', 'syscall', 'tsc',
  251. 'tsc_thread_offset', 'tsci', 'tsctmr', 'vme', 'vmm', 'x2apic',
  252. 'xd', 'xsave']
  253. return the_info