mirror of
https://github.com/umutcamliyurt/Amnezichat.git
synced 2025-05-07 07:55:23 +01:00
283 lines
12 KiB
Python
283 lines
12 KiB
Python
# SPDX-License-Identifier: MIT
|
|
|
|
""" test_constant_time.py
|
|
|
|
The goal of this script is to ensure that every instance of secret-dependant
|
|
control flow in liboqs is documented. This script does not ensure that all of
|
|
the software in liboqs is constant time. Rather, it is intended to aid auditors
|
|
in their search for non-constant time behaviour.
|
|
|
|
WARNING: This script currently runs test_kem and test_sig on random seeds.
|
|
It is not coverage guided. It will miss instances of non-constant time
|
|
behaviour in code paths that are rarely executed.
|
|
|
|
This script requires Valgrind version >= 3.14.0, and it only gives meaningful
|
|
results if test_kem and test_sig have been compiled with CMAKE_BUILD_TYPE=Debug
|
|
and OQS_ENABLE_TEST_CONSTANT_TIME.
|
|
|
|
|
|
How this script works
|
|
---------------------
|
|
This script runs test_kem (and/or test_sig) through Valgrind's Memcheck tool.
|
|
Valgrind executes the test program and issues an error message if/whenever the
|
|
program's control flow depends on uninitialized data. As observed by Adam
|
|
Langley [1], if we tell Valgrind that secrets are uninitialized, then Valgrind
|
|
will tell us about secret-dependant control flow.
|
|
|
|
Assuming that each scheme in liboqs passes test_kat, our test_kem and test_sig
|
|
programs are structured such that all secret data can be traced back to a call
|
|
to OQS_randombytes. The tests intercept calls to OQS_randombytes and tell
|
|
Valgrind that every random byte is uninitialized. Hence, Valgrind will issue an
|
|
error if (but not only if!) our tests branch on secret data.
|
|
|
|
Since there may be false positives, we say that Valgrind identifies "suspected
|
|
non-constant time behaviour".
|
|
|
|
Again, the purpose of this script is to ensure that suspected non-constant time
|
|
behaviour is documented. This script ships with a collection of Valgrind
|
|
"suppression files". Each suppression file documents one or more instances of
|
|
suspected non-constant time behaviour in liboqs.
|
|
|
|
The suppression files are also used to silence errors from Valgrind. If this
|
|
script runs without error, then all of the suspected non-constant time behaviour
|
|
in liboqs has been documented. If this script fails, then a new suppression
|
|
file should be written.
|
|
|
|
|
|
How to write suppression files
|
|
------------------------------
|
|
Valgrind will output a suppression file template along with its error message.
|
|
It's your job to copy this template to the correct location, edit it, and tell
|
|
this script about the new file.
|
|
|
|
Suppression files for KEMs are stored in
|
|
liboqs/tests/constant_time/kem/{passes,issues}/.
|
|
Suppression files for signature schemes are stored in
|
|
liboqs/tests/constant_time/sig/{passes,issues}/.
|
|
|
|
This script does not differentiate between the passes and issues
|
|
subdirectories. The label is for auditors. We "give a pass" to an error that
|
|
is known not to be a security threat, and we store the corresponding
|
|
suppression file in the "passes" subdirectory. We "raise an issue" about any
|
|
other error, and we store the corresponding suppression file in the "issues"
|
|
subdirectory.
|
|
|
|
If you are unsure where your suppression file belongs, then save it to the
|
|
"issues" subdirectory.
|
|
|
|
Once you've written a suppression file, give it a descriptive file name and
|
|
tell this script about it. There are json files called passes.json and
|
|
issues.json in
|
|
liboqs/tests/constant_time/{kem,sig}/
|
|
These json files contain dictionaries of the form
|
|
{ "Scheme name" : ["list", "of", "suppression", "files"], ... }
|
|
Add the name of your suppression file to the appropriate list to suppress
|
|
the errors that you have documented.
|
|
|
|
|
|
How to write a good suppression file
|
|
------------------------------------
|
|
Here is an example of a suppression file:
|
|
{
|
|
Rejection sampling to produce public "A" matrix
|
|
Memcheck:Cond
|
|
fun:rej_uniform
|
|
fun:PQCLEAN_KYBER*_CLEAN_gen_matrix
|
|
}
|
|
|
|
The brackets wrap a single error that is to be suppressed. Within the brackets,
|
|
the first line is a comment. The remaining lines tell Valgrind to ignore
|
|
any "Memcheck:Cond" errors that occur when a function named rej_uniform is
|
|
called from a function whose name matches the glob pattern
|
|
PQCLEAN_KYBER*_CLEAN_gen_matrix.
|
|
|
|
Before this suppression file was written, a run of this script produced the
|
|
following output.
|
|
|
|
==594== Conditional jump or move depends on uninitialised value(s)
|
|
==594== at 0x22550D: rej_uniform (indcpa.c:133)
|
|
==594== by 0x225654: PQCLEAN_KYBER512_CLEAN_gen_matrix (indcpa.c:177)
|
|
==594== by 0x2257D1: PQCLEAN_KYBER512_CLEAN_indcpa_keypair (indcpa.c:216)
|
|
==594== by 0x1B6C1E: PQCLEAN_KYBER512_CLEAN_crypto_kem_keypair (kem.c:26)
|
|
==594== by 0x1B6B9F: OQS_KEM_kyber_512_keypair (kem_kyber_512.c:56)
|
|
==594== by 0x10D123: OQS_KEM_keypair (kem.c:818)
|
|
==594== by 0x10AD07: kem_test_correctness (test_kem.c:103)
|
|
==594== by 0x10B4E7: test_wrapper (test_kem.c:186)
|
|
==594== by 0x4CDAFA2: start_thread (pthread_create.c:486)
|
|
==594== by 0x4DED4CE: clone (clone.S:95)
|
|
==594==
|
|
{
|
|
<insert_a_suppression_name_here>
|
|
Memcheck:Cond
|
|
fun:rej_uniform
|
|
fun:PQCLEAN_KYBER512_CLEAN_gen_matrix
|
|
fun:PQCLEAN_KYBER512_CLEAN_indcpa_keypair
|
|
fun:PQCLEAN_KYBER512_CLEAN_crypto_kem_keypair
|
|
fun:OQS_KEM_kyber_512_keypair
|
|
fun:OQS_KEM_keypair
|
|
fun:kem_test_correctness
|
|
fun:test_wrapper
|
|
fun:start_thread
|
|
fun:clone
|
|
}
|
|
|
|
The lines beginning with "==" are a Valgrind error message. The bracketed text
|
|
is a suppression file template. To produce the final suppression file we
|
|
added a comment, replaced "512" with a wildcard (since an identical error occurs
|
|
in other Kyber parameter sets), and truncated the backtrace (since the extra lines
|
|
provide no interesting information to auditors).
|
|
|
|
The "fun:rej_uniform" line says to ignore _all_ Memcheck:Cond errors in
|
|
rej_uniform, but Valgrind told us that line 133 was the problem. Any
|
|
"fun:name" line in the backtrace can be replaced by an equivalent
|
|
"src:file:line", so we could have narrowed the scope of our suppression:
|
|
{
|
|
Rejection sampling to produce public "A" matrix
|
|
Memcheck:Cond
|
|
src:indcpa.c:133 # fun:rej_uniform
|
|
fun:PQCLEAN_KYBER*_CLEAN_gen_matrix
|
|
}
|
|
Here "# fun:rej_uniform" is a comment. An update to the Kyber source code might
|
|
break our suppression file by changing the line number, and leaving the function
|
|
name as a comment might help a future reviewer.
|
|
|
|
An ellipsis (...) can serve as a wildcard for a portion of the backtrace.
|
|
We could have written:
|
|
{
|
|
Rejection sampling to produce public "A" matrix
|
|
Memcheck:Cond
|
|
...
|
|
fun:PQCLEAN_KYBER*_CLEAN_gen_matrix
|
|
}
|
|
But this is perhaps too concise. Remember that the goal here is to help auditors.
|
|
|
|
Further information can be found in Valgrind's manual. See
|
|
https://www.valgrind.org/docs/manual/manual-core.html#manual-core.suppress
|
|
and
|
|
https://www.valgrind.org/docs/manual/mc-manual.html#mc-manual.suppfiles
|
|
|
|
Credits
|
|
-------
|
|
The observation that Valgrind can be used to identify non-constant time
|
|
behaviour is due to Adam Langley [1, 2]. Mortiz Neikes' TIMECOP project applies
|
|
Langley's idea to the SUPERCOP benchmarking suite [3]. Versions of SUPERCOP
|
|
starting with 20200816 include TIMECOP and apply Langley's idea to randombytes
|
|
calls in particular [4]. We have borrowed the idea of instrumenting randombytes
|
|
calls from SUPERCOP.
|
|
|
|
[1] https://github.com/agl/ctgrind
|
|
[2] https://boringssl.googlesource.com/boringssl/+/a6a049a6fb51a052347611d41583a0622bc89d60
|
|
[2] https://post-apocalyptic-crypto.org/timecop/index.html
|
|
[3] http://bench.cr.yp.to/tips.html#timecop
|
|
"""
|
|
|
|
|
|
import helpers
|
|
import json
|
|
import os
|
|
import pytest
|
|
import sys
|
|
import re
|
|
|
|
REQ_LIBOQS_BUILD_OPTS = ['OQS_ENABLE_TEST_CONSTANT_TIME',
|
|
'OQS_DEBUG_BUILD']
|
|
|
|
# Error suppression based on file and line number was introduced in
|
|
# Valgrind 3.14.0 (9 October 2018).
|
|
# https://www.valgrind.org/docs/manual/dist.news.html
|
|
MIN_VALGRIND_VERSION = [3, 14, 0]
|
|
|
|
VALGRIND = ['valgrind',
|
|
# '-v', # Turn on -v to see which suppression files are used
|
|
'--tool=memcheck',
|
|
'--gen-suppressions=all',
|
|
'--error-exitcode=1',
|
|
'--max-stackframe=20480000',
|
|
'--num-callers=20',
|
|
]
|
|
|
|
# The following two functions read the json files
|
|
# liboqs/tests/constant_time/{kem,sig}/{passes,issues}.json
|
|
# into python dictionaries `ct_passes' and `ct_issues', which
|
|
# are of the form
|
|
# { 'kem' : { 'Kem Name' : ['list', 'of', 'filenames'], ... },
|
|
# 'sig' : { 'Sig Name' : ['list', 'of', 'filenames'], ... }
|
|
# }
|
|
|
|
ct_passes = {'kem': None, 'sig': None}
|
|
ct_issues = {'kem': None, 'sig': None}
|
|
|
|
def get_ct_passes(t, name):
|
|
ct_t = os.path.join('tests', 'constant_time', t)
|
|
if ct_passes[t] is None:
|
|
with open(os.path.join(ct_t, 'passes.json'), 'r') as fp:
|
|
ct_passes[t] = json.load(fp)
|
|
passes = ct_passes[t].get(name,[])
|
|
return [os.path.join(ct_t, 'passes', f) for f in passes]
|
|
|
|
def get_ct_issues(t, name):
|
|
ct_t = os.path.join('tests', 'constant_time', t)
|
|
if ct_issues[t] is None:
|
|
with open(os.path.join(ct_t, 'issues.json'), 'r') as fp:
|
|
ct_issues[t] = json.load(fp)
|
|
issues = ct_issues[t].get(name,[])
|
|
return [os.path.join(ct_t, 'issues', f) for f in issues]
|
|
|
|
|
|
@helpers.filtered_test
|
|
@helpers.test_requires_build_options(*REQ_LIBOQS_BUILD_OPTS)
|
|
@helpers.test_requires_valgrind_version_at_least(*MIN_VALGRIND_VERSION)
|
|
@pytest.mark.parametrize('kem_name', helpers.available_kems_by_name())
|
|
def test_constant_time_kem(kem_name):
|
|
if not(helpers.is_kem_enabled_by_name(kem_name)): pytest.skip('Not enabled')
|
|
if ('SKIP_ALGS' in os.environ) and len(os.environ['SKIP_ALGS'])>0:
|
|
for algexp in os.environ['SKIP_ALGS'].split(','):
|
|
if len(re.findall(algexp, kem_name))>0:
|
|
pytest.skip("Test disabled by alg filter")
|
|
passes = get_ct_passes('kem', kem_name)
|
|
issues = get_ct_issues('kem', kem_name)
|
|
output = helpers.run_subprocess(
|
|
VALGRIND + [
|
|
*(['--suppressions='+f for f in passes]),
|
|
*(['--suppressions='+f for f in issues]),
|
|
helpers.path_to_executable('test_kem'),
|
|
kem_name
|
|
]
|
|
)
|
|
|
|
@helpers.filtered_test
|
|
@helpers.test_requires_build_options(*REQ_LIBOQS_BUILD_OPTS)
|
|
@helpers.test_requires_valgrind_version_at_least(*MIN_VALGRIND_VERSION)
|
|
@pytest.mark.parametrize('sig_name', helpers.available_sigs_by_name())
|
|
def test_constant_time_sig(sig_name):
|
|
if not(helpers.is_sig_enabled_by_name(sig_name)): pytest.skip('Not enabled')
|
|
if ('SKIP_ALGS' in os.environ) and len(os.environ['SKIP_ALGS'])>0:
|
|
for algexp in os.environ['SKIP_ALGS'].split(','):
|
|
if len(re.findall(algexp, sig_name))>0:
|
|
pytest.skip("Test disabled by alg filter")
|
|
passes = get_ct_passes('sig', sig_name)
|
|
issues = get_ct_issues('sig', sig_name)
|
|
output = helpers.run_subprocess(
|
|
VALGRIND + [
|
|
*(['--suppressions='+f for f in passes]),
|
|
*(['--suppressions='+f for f in issues]),
|
|
helpers.path_to_executable('test_sig'),
|
|
sig_name
|
|
]
|
|
)
|
|
|
|
if __name__ == '__main__':
|
|
pytest.main(sys.argv)
|
|
|
|
# Unused/obsolete suppressions are a burden on reviewers. You can find out which suppressions
|
|
# are used by passing the -v flag to valgrind. To find unused suppressions we have to extract
|
|
# a list of available suppressions first. You can use awk to find lines that contain only a '{'.
|
|
# Increment these line numbers by 1 to match the output of valgrind -v, then compare against
|
|
# the used suppressions.
|
|
#
|
|
# awk '$0 ~ /^{$/{print FILENAME ":" NR+1}' suppression files > /tmp/available_suppressions
|
|
# valgrind -v --suppressions=[...] ./build/tests/test_kem KEM_NAME 2>&1 \
|
|
# | grep used_suppression \
|
|
# | awk '{ print $NF }' > /tmp/used_suppressions
|
|
# cat /tmp/used_suppressions /tmp/available_suppressions | sort | uniq -u
|