-
Notifications
You must be signed in to change notification settings - Fork 485
/
Copy pathtest_constant_time.py
282 lines (241 loc) · 11.7 KB
/
test_constant_time.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# 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