Skip to content

Commit bb72042

Browse files
committed
CI: Add ACVP tests in CI
This commit adds a portable acvp_client.py that runs all the ACVP tests in parallel. This way we do not have to rely on parallel to be installed. It also adds these ACVP tests to CI. Resolves #4 Signed-off-by: Matthias J. Kannwischer <[email protected]>
1 parent 6079933 commit bb72042

File tree

7 files changed

+285
-340
lines changed

7 files changed

+285
-340
lines changed

.github/workflows/all.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ on:
1313
branches: ["main"]
1414
types: [ "opened", "synchronize" ]
1515
jobs:
16+
base:
17+
name: Base
18+
permissions:
19+
contents: 'read'
20+
id-token: 'write'
21+
uses: ./.github/workflows/base.yml
22+
secrets: inherit
1623
nix:
1724
name: Nix
1825
permissions:
@@ -26,7 +33,6 @@ jobs:
2633
permissions:
2734
contents: 'read'
2835
id-token: 'write'
29-
needs: [ nix ]
36+
needs: [ base, nix ]
3037
uses: ./.github/workflows/cbmc.yml
3138
secrets: inherit
32-

.github/workflows/base.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright (c) The mlkem-native project authors
2+
# Copyright (c) The slhdsa-c project authors
3+
# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT
4+
5+
name: Base
6+
permissions:
7+
contents: read
8+
on:
9+
workflow_call:
10+
workflow_dispatch:
11+
12+
jobs:
13+
quickcheck:
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
external:
18+
- ${{ github.repository_owner != 'pq-code-package' }}
19+
target:
20+
- runner: pqcp-arm64
21+
name: 'aarch64'
22+
- runner: ubuntu-latest
23+
name: 'x86_64'
24+
- runner: macos-latest
25+
name: 'macos (aarch64)'
26+
- runner: macos-13
27+
name: 'macos (x86_64)'
28+
exclude:
29+
- {external: true,
30+
target: {
31+
runner: pqcp-arm64,
32+
name: 'aarch64'
33+
}}
34+
name: Quickcheck (${{ matrix.target.name }})
35+
runs-on: ${{ matrix.target.runner }}
36+
steps:
37+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
38+
with:
39+
submodules: true
40+
- name: make test
41+
run: |
42+
make test

Makefile

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,8 @@ $(XTEST): $(OBJS)
3434
%.o: %.[cS]
3535
$(CC) $(CFLAGS) -c $^ -o $@
3636

37-
# without gnu parallel: bash test/acvp_cases.sh | tee test.log
38-
test: $(XTEST) test/acvp_cases.sh
39-
cat test/acvp_cases.sh | parallel --pipe bash | tee test.log
40-
@echo "=== test summary ==="
41-
@echo "PASS:" `grep -c PASS test.log`
42-
@echo "SKIP:" `grep -c SKIP test.log`
43-
@echo "FAIL:" `grep -c FAIL test.log`
44-
45-
test/acvp_cases.sh:
46-
cd test && $(MAKE) acvp_cases.sh
37+
test: $(XTEST)
38+
python3 test/acvp_client.py
4739

4840
clean:
4941
$(RM) -rf $(XTEST) $(OBJS) *.rsp *.req *.log

cbmc.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* Copyright (c) The mlkem-native project authors
3-
* Copyright (c) The slhdsa-native project authors
3+
* Copyright (c) The slhdsa-c project authors
44
* SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT
55
*/
66

test/Makefile

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ $(XCOUNT): $(OBJS) xcount.c my_dbg.c
2121
%.o: %.[cS]
2222
$(CC) $(CFLAGS) -c $^ -o $@
2323

24-
acvp_cases.sh: ACVP-Server/gen-val/json-files
25-
python3 test_slhdsa.py > $@
26-
27-
new_param.csv: $(XCOUNT) test_param.py new_param.txt acvp_cases.sh
24+
new_param.csv: $(XCOUNT) test_param.py new_param.txt
2825
echo "alg_id, pk, sk, sig, keygen, sign, vfy_ok, vfy_fail"> $@
2926
./$(XCOUNT) | tee /dev/tty | sort >> $@
3027
python3 test_param.py | parallel | tee /dev/tty | sort >> $@
@@ -35,6 +32,5 @@ ACVP-Server/gen-val/json-files:
3532
clean:
3633
$(RM) -rf $(XCOUNT) $(OBJS) *.log
3734
$(RM) -f *.pyc *.cprof */*.pyc *.rsp *.log
38-
$(RM) -f acvp_cases.sh
3935
$(RM) -rf __pycache__ */__pycache__
4036

test/acvp_client.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) The slhdsa-c project authors
3+
# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT
4+
5+
"""SLH-DSA ACVP client."""
6+
7+
import argparse
8+
import json
9+
import os
10+
import subprocess
11+
import sys
12+
from concurrent.futures import ThreadPoolExecutor, as_completed
13+
14+
# === JSON parsing functions ===
15+
16+
def slhdsa_load_keygen(req_fn, res_fn):
17+
with open(req_fn) as f:
18+
keygen_req = json.load(f)
19+
with open(res_fn) as f:
20+
keygen_res = json.load(f)
21+
22+
keygen_kat = []
23+
for qtg in keygen_req['testGroups']:
24+
alg = qtg['parameterSet']
25+
tgid = qtg['tgId']
26+
27+
rtg = None
28+
for tg in keygen_res['testGroups']:
29+
if tg['tgId'] == tgid:
30+
rtg = tg['tests']
31+
break
32+
33+
for qt in qtg['tests']:
34+
tcid = qt['tcId']
35+
for t in rtg:
36+
if t['tcId'] == tcid:
37+
qt.update(t)
38+
qt['parameterSet'] = alg
39+
keygen_kat += [qt]
40+
return keygen_kat
41+
42+
def slhdsa_load_siggen(req_fn, res_fn):
43+
with open(req_fn) as f:
44+
siggen_req = json.load(f)
45+
with open(res_fn) as f:
46+
siggen_res = json.load(f)
47+
48+
siggen_kat = []
49+
for qtg in siggen_req['testGroups']:
50+
alg = qtg['parameterSet']
51+
det = qtg['deterministic']
52+
pre = False
53+
if 'preHash' in qtg and qtg['preHash'] == 'preHash':
54+
pre = True
55+
ifc = None
56+
if 'signatureInterface' in qtg:
57+
ifc = qtg['signatureInterface']
58+
tgid = qtg['tgId']
59+
60+
rtg = None
61+
for tg in siggen_res['testGroups']:
62+
if tg['tgId'] == tgid:
63+
rtg = tg['tests']
64+
break
65+
66+
for qt in qtg['tests']:
67+
tcid = qt['tcId']
68+
for t in rtg:
69+
if t['tcId'] == tcid:
70+
qt.update(t)
71+
qt['parameterSet'] = alg
72+
qt['deterministic'] = det
73+
if 'preHash' not in qt:
74+
qt['preHash'] = pre
75+
if 'context' not in qt:
76+
qt['context'] = ''
77+
qt['signatureInterface'] = ifc
78+
siggen_kat += [qt]
79+
return siggen_kat
80+
81+
def slhdsa_load_sigver(req_fn, res_fn, int_fn):
82+
with open(req_fn) as f:
83+
sigver_req = json.load(f)
84+
with open(res_fn) as f:
85+
sigver_res = json.load(f)
86+
with open(int_fn) as f:
87+
sigver_int = json.load(f)
88+
89+
sigver_kat = []
90+
for qtg in sigver_req['testGroups']:
91+
alg = qtg['parameterSet']
92+
tgid = qtg['tgId']
93+
pre = False
94+
if 'preHash' in qtg and qtg['preHash'] == 'preHash':
95+
pre = True
96+
ifc = None
97+
if 'signatureInterface' in qtg:
98+
ifc = qtg['signatureInterface']
99+
100+
rtg = None
101+
for tg in sigver_res['testGroups']:
102+
if tg['tgId'] == tgid:
103+
rtg = tg['tests']
104+
break
105+
106+
itg = None
107+
for tg in sigver_int['testGroups']:
108+
if tg['tgId'] == tgid:
109+
itg = tg['tests']
110+
break
111+
112+
for qt in qtg['tests']:
113+
pk = qt['pk']
114+
tcid = qt['tcId']
115+
for t in rtg:
116+
if t['tcId'] == tcid:
117+
qt.update(t)
118+
# message, signature in this file overrides prompts
119+
for t in itg:
120+
if t['tcId'] == tcid:
121+
qt.update(t)
122+
qt['parameterSet'] = alg
123+
qt['pk'] = pk
124+
if 'preHash' not in qt:
125+
qt['preHash'] = pre
126+
qt['signatureInterface'] = ifc
127+
sigver_kat += [qt]
128+
return sigver_kat
129+
130+
# === Test execution ===
131+
132+
def run_command(cmd):
133+
"""Run a single test command and return result."""
134+
try:
135+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
136+
return (result.returncode, result.stdout, result.stderr)
137+
except Exception as e:
138+
return (-1, "", str(e))
139+
140+
def run_test_kat(test_type, kat, jobs, xbin='./xfips205'):
141+
"""Run test KAT in parallel."""
142+
passed = 0
143+
failed = 0
144+
145+
def build_command(x):
146+
s = xbin
147+
for t in x:
148+
if x[t] != "":
149+
s += f' -{t} "{x[t]}"'
150+
s += f' {test_type}'
151+
return s
152+
153+
# Run tests in parallel
154+
with ThreadPoolExecutor(max_workers=jobs) as executor:
155+
futures = [executor.submit(run_command, build_command(x)) for x in kat]
156+
157+
for future in as_completed(futures):
158+
returncode, stdout, stderr = future.result()
159+
160+
if returncode == 0:
161+
passed += 1
162+
if "PASS" in stdout:
163+
print(stdout.strip())
164+
else:
165+
failed += 1
166+
if stderr:
167+
print(f"Error: {stderr}")
168+
169+
return passed, failed
170+
171+
def main():
172+
parser = argparse.ArgumentParser(description="SLH-DSA ACVP test runner")
173+
parser.add_argument("--jobs", "-j", type=int, default=os.cpu_count() or 4,
174+
help="Number of parallel jobs (default: auto-detect CPU cores)")
175+
176+
args = parser.parse_args()
177+
178+
print("Generating test commands from ACVP JSON files...", file=sys.stderr)
179+
180+
try:
181+
json_path = 'test/ACVP-Server/gen-val/json-files/'
182+
183+
keygen_kat = slhdsa_load_keygen(
184+
json_path + 'SLH-DSA-keyGen-FIPS205/prompt.json',
185+
json_path + 'SLH-DSA-keyGen-FIPS205/expectedResults.json')
186+
187+
siggen_kat = slhdsa_load_siggen(
188+
json_path + 'SLH-DSA-sigGen-FIPS205/prompt.json',
189+
json_path + 'SLH-DSA-sigGen-FIPS205/expectedResults.json')
190+
191+
sigver_kat = slhdsa_load_sigver(
192+
json_path + 'SLH-DSA-sigVer-FIPS205/prompt.json',
193+
json_path + 'SLH-DSA-sigVer-FIPS205/expectedResults.json',
194+
json_path + 'SLH-DSA-sigVer-FIPS205/internalProjection.json')
195+
196+
except FileNotFoundError as e:
197+
print(f"Error: Could not find ACVP JSON files. Make sure submodule is initialized.", file=sys.stderr)
198+
print(f"Run: git submodule update --init --recursive", file=sys.stderr)
199+
return 1
200+
201+
total_tests = len(keygen_kat) + len(siggen_kat) + len(sigver_kat)
202+
print(f"Running {total_tests} tests with {args.jobs} parallel jobs", file=sys.stderr)
203+
204+
total_passed = 0
205+
total_failed = 0
206+
207+
# Run each test type
208+
passed, failed = run_test_kat('keyGen', keygen_kat, args.jobs)
209+
total_passed += passed
210+
total_failed += failed
211+
212+
passed, failed = run_test_kat('sigGen', siggen_kat, args.jobs)
213+
total_passed += passed
214+
total_failed += failed
215+
216+
passed, failed = run_test_kat('sigVer', sigver_kat, args.jobs)
217+
total_passed += passed
218+
total_failed += failed
219+
220+
print(f"\n=== test summary ===")
221+
print(f"PASS: {total_passed}")
222+
print(f"FAIL: {total_failed}")
223+
224+
if total_failed == 0:
225+
print("ALL GOOD!")
226+
return 0
227+
else:
228+
return 1
229+
230+
if __name__ == "__main__":
231+
sys.exit(main())

0 commit comments

Comments
 (0)