Skip to content

Commit f08a717

Browse files
committed
test: Add test_sign_hook exercising the signing hooks
Add a test analogous to test_alloc, driven by a custom config (test_sign_hook_config.h) that enables the three signing-hook options and forward-declares the hook context; the context type and the hook implementations live in the test. The hooks pause signing after a configurable number of attempts and resume from the recorded attempt, letting one context drive: - a known-answer smoke test, reproducing the test-vector signature both one-shot and single-step; - a restartable-signing equivalence test, checking that pausing after a random number of attempts and resuming yields the same signature as an uninterrupted run, over many random signing-randomness draws; - a distribution test, gathering the attempts-per-signature histogram over many one-shot signatures and checking the measured attempts/signature ratio against the expected geometric mean for the parameter set. The test requires the internal signing API, so it compiles to a no-op main under MLD_CONFIG_NO_SIGN_API. Statistics are printed via printf only (no putchar), so freestanding platforms with a minimal stdio shim can run it. Wire it up in the Makefile, the test make fragments and scripts/tests (new --sign-hook subcommand). Mirror alloc's CI plumbing: add a sign_hook input to the functest / multi-functest actions and disable it for the config variations it is incompatible with -- those passing their own MLD_CONFIG_FILE (would clash with the test's fixed config) and those enabling MLD_CONFIG_KEYGEN_PCT (incompatible with SIGN_HOOK_RESUME). The generated configs follow in a separate commit. Signed-off-by: Hanno Becker <beckphan@amazon.co.uk>
1 parent 9a3b120 commit f08a717

16 files changed

Lines changed: 942 additions & 89 deletions

File tree

.github/actions/config-variations/action.yml

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,12 @@ runs:
3333
func: true
3434
kat: true
3535
acvp: true
36+
sign_hook: false # SIGN_HOOK_RESUME is incompatible with KEYGEN_PCT
3637
opt: ${{ inputs.opt }}
3738
examples: true
38-
extra_args: "--exclude-example basic_deterministic"
39+
# basic_deterministic and restartable_sign set MLD_CONFIG_NO_RANDOMIZED_API,
40+
# which is incompatible with MLD_CONFIG_KEYGEN_PCT.
41+
extra_args: "--exclude-example basic_deterministic --exclude-example restartable_sign"
3942
- name: "REDUCE_RAM"
4043
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'reduce-ram') }}
4144
uses: ./.github/actions/multi-functest
@@ -62,9 +65,12 @@ runs:
6265
kat: true
6366
acvp: true
6467
alloc: true
68+
sign_hook: false # SIGN_HOOK_RESUME is incompatible with KEYGEN_PCT
6569
opt: ${{ inputs.opt }}
6670
examples: true
67-
extra_args: "--exclude-example basic_deterministic"
71+
# basic_deterministic and restartable_sign set MLD_CONFIG_NO_RANDOMIZED_API,
72+
# which is incompatible with MLD_CONFIG_KEYGEN_PCT.
73+
extra_args: "--exclude-example basic_deterministic --exclude-example restartable_sign"
6874
- name: "PCT enabled + broken"
6975
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'pct-enabled-broken') }}
7076
shell: bash
@@ -107,6 +113,7 @@ runs:
107113
extra_env: 'ASAN_OPTIONS=detect_leaks=1'
108114
examples: false # Some examples use a custom config themselves
109115
alloc: false # Requires custom config
116+
sign_hook: false # Requires custom config
110117
rng_fail: true
111118
- name: "Custom zeroization (explicit_bzero)"
112119
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'custom-zeroize') }}
@@ -122,6 +129,7 @@ runs:
122129
opt: ${{ inputs.opt }}
123130
examples: false # Some examples use a custom config themselves
124131
alloc: false # Requires custom config
132+
sign_hook: false # Requires custom config
125133
rng_fail: true
126134
- name: "Custom native capability functions (static ON)"
127135
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'native-cap-ON') }}
@@ -137,6 +145,7 @@ runs:
137145
opt: ${{ inputs.opt }}
138146
examples: false # Some examples use a custom config themselves
139147
alloc: false # Requires custom config
148+
sign_hook: false # Requires custom config
140149
rng_fail: true
141150
- name: "Custom native capability functions (static OFF)"
142151
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'native-cap-OFF') }}
@@ -152,6 +161,7 @@ runs:
152161
opt: ${{ inputs.opt }}
153162
examples: false # Some examples use a custom config themselves
154163
alloc: false # Requires custom config
164+
sign_hook: false # Requires custom config
155165
rng_fail: true
156166
- name: "Custom native capability functions (ID_AA64PFR1_EL1 detection)"
157167
if: ${{ (inputs.tests == 'all' || contains(inputs.tests, 'native-cap-ID_AA64PFR1_EL1')) && runner.os == 'Linux' && runner.arch == 'ARM64' }}
@@ -167,6 +177,7 @@ runs:
167177
opt: ${{ inputs.opt }}
168178
examples: false # Some examples use a custom config themselves
169179
alloc: false # Requires custom config
180+
sign_hook: false # Requires custom config
170181
rng_fail: true
171182
- name: "Custom native capability functions (CPUID AVX2 detection)"
172183
if: ${{ (inputs.tests == 'all' || contains(inputs.tests, 'native-cap-CPUID_AVX2')) && runner.os == 'Linux' && runner.arch == 'X64' }}
@@ -182,6 +193,7 @@ runs:
182193
opt: ${{ inputs.opt }}
183194
examples: false # Some examples use a custom config themselves
184195
alloc: false # Requires custom config
196+
sign_hook: false # Requires custom config
185197
rng_fail: true
186198
- name: "No ASM"
187199
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'no-asm') }}
@@ -197,6 +209,7 @@ runs:
197209
opt: ${{ inputs.opt }}
198210
examples: false # Some examples use a custom config themselves
199211
alloc: false # Requires custom config
212+
sign_hook: false # Requires custom config
200213
rng_fail: true
201214
- name: "Serial FIPS202 (no batched Keccak)"
202215
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'serial-fips202') }}
@@ -212,6 +225,7 @@ runs:
212225
opt: ${{ inputs.opt }}
213226
examples: false # Some examples use a custom config themselves
214227
alloc: false # Requires custom config
228+
sign_hook: false # Requires custom config
215229
rng_fail: true
216230
- name: "Custom randombytes"
217231
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'custom-randombytes') }}
@@ -227,6 +241,7 @@ runs:
227241
opt: ${{ inputs.opt }}
228242
examples: false # Some examples use a custom config themselves
229243
alloc: false # Requires custom config
244+
sign_hook: false # Requires custom config
230245
rng_fail: false # Uses its own randombytes implementation
231246
- name: "Custom memcpy"
232247
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'custom-memcpy') }}
@@ -242,6 +257,7 @@ runs:
242257
opt: ${{ inputs.opt }}
243258
examples: false # Some examples use a custom config themselves
244259
alloc: false # Requires custom config
260+
sign_hook: false # Requires custom config
245261
rng_fail: true
246262
- name: "Custom memset"
247263
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'custom-memset') }}
@@ -257,6 +273,7 @@ runs:
257273
opt: ${{ inputs.opt }}
258274
examples: false # Some examples use a custom config themselves
259275
alloc: false # Requires custom config
276+
sign_hook: false # Requires custom config
260277
rng_fail: true
261278
- name: "Custom stdlib (memcpy + memset)"
262279
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'custom-stdlib') }}
@@ -272,6 +289,7 @@ runs:
272289
opt: ${{ inputs.opt }}
273290
examples: false # Some examples use a custom config themselves
274291
alloc: false # Requires custom config
292+
sign_hook: false # Requires custom config
275293
rng_fail: true
276294
- name: "MLD_POLY_UNIFORM_NBLOCKS=1"
277295
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'nblocks-1') }}
@@ -287,6 +305,7 @@ runs:
287305
opt: ${{ inputs.opt }}
288306
examples: false # Some examples use a custom config themselves
289307
alloc: false # Requires custom config
308+
sign_hook: false # Requires custom config
290309
rng_fail: true
291310
- name: "MLD_POLY_UNIFORM_NBLOCKS=4"
292311
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'nblocks-4') }}
@@ -302,6 +321,7 @@ runs:
302321
opt: ${{ inputs.opt }}
303322
examples: false # Some examples use a custom config themselves
304323
alloc: false # Requires custom config
324+
sign_hook: false # Requires custom config
305325
rng_fail: true
306326
- name: "MLD_POLY_UNIFORM_NBLOCKS=6"
307327
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'nblocks-6') }}
@@ -317,6 +337,7 @@ runs:
317337
opt: ${{ inputs.opt }}
318338
examples: false # Some examples use a custom config themselves
319339
alloc: false # Requires custom config
340+
sign_hook: false # Requires custom config
320341
rng_fail: true
321342
- name: "Keygen-only API"
322343
if: ${{ inputs.tests == 'all' || contains(inputs.tests, 'keygen-only') }}
@@ -330,6 +351,7 @@ runs:
330351
kat: true
331352
acvp: true
332353
stack: true
354+
sign_hook: false # Requires custom config
333355
rng_fail: true
334356
opt: ${{ inputs.opt }}
335357
examples: true
@@ -345,6 +367,7 @@ runs:
345367
kat: true
346368
acvp: true
347369
stack: true
370+
sign_hook: false # Requires custom config
348371
rng_fail: true
349372
opt: ${{ inputs.opt }}
350373
examples: true
@@ -357,6 +380,7 @@ runs:
357380
cflags: "${{ inputs.extra_cflags }} -DMLD_CONFIG_NO_KEYPAIR_API -DMLD_CONFIG_NO_SIGN_API -fsanitize=address -fsanitize=undefined -fno-sanitize-recover=all"
358381
ldflags: "-fsanitize=address -fsanitize=undefined -fno-sanitize-recover=all"
359382
func: true
383+
sign_hook: false # Requires custom config
360384
kat: true
361385
acvp: true
362386
stack: true
@@ -375,6 +399,7 @@ runs:
375399
kat: true
376400
acvp: true
377401
stack: true
402+
sign_hook: false # Requires custom config
378403
rng_fail: true
379404
opt: ${{ inputs.opt }}
380405
examples: true
@@ -390,6 +415,7 @@ runs:
390415
kat: true
391416
acvp: true
392417
stack: true
418+
sign_hook: false # Requires custom config
393419
rng_fail: true
394420
opt: ${{ inputs.opt }}
395421
examples: true
@@ -405,6 +431,7 @@ runs:
405431
kat: true
406432
acvp: true
407433
stack: true
434+
sign_hook: false # Requires custom config
408435
rng_fail: true
409436
opt: ${{ inputs.opt }}
410437
examples: true

.github/actions/functest/action.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ inputs:
6363
alloc:
6464
description: Determine whether to run alloc tests or not
6565
default: "true"
66+
sign_hook:
67+
description: Determine whether to run sign hook tests or not
68+
default: "true"
6669
rng_fail:
6770
description: Determine whether to run rng fail tests or not
6871
default: "true"
@@ -88,6 +91,7 @@ runs:
8891
echo EXAMPLES="${{ inputs.examples == 'true' && 'examples' || 'no-examples' }}" >> $GITHUB_ENV
8992
echo STACK="${{ inputs.stack == 'true' && 'stack' || 'no-stack' }}" >> $GITHUB_ENV
9093
echo ALLOC="${{ inputs.alloc == 'true' && 'alloc' || 'no-alloc' }}" >> $GITHUB_ENV
94+
echo SIGN_HOOK="${{ inputs.sign_hook == 'true' && 'sign-hook' || 'no-sign-hook' }}" >> $GITHUB_ENV
9195
echo RNGFAIL="${{ inputs.rng_fail == 'true' && 'rng-fail' || 'no-rng-fail' }}" >> $GITHUB_ENV
9296
- name: Setup nix
9397
uses: ./.github/actions/setup-shell
@@ -119,13 +123,13 @@ runs:
119123
- $(python3 --version)
120124
- $(${{ inputs.cross_prefix }}${CC} --version | grep -m1 "")
121125
EOF
122-
- name: ${{ env.MODE }} ${{ inputs.opt }} tests (${{ env.FUNC }}, ${{ env.KAT }}, ${{ env.EXAMPLES }}, ${{ env.STACK }}, ${{ env.UNIT }}, ${{ env.ALLOC }}, ${{ env.RNGFAIL }})
126+
- name: ${{ env.MODE }} ${{ inputs.opt }} tests (${{ env.FUNC }}, ${{ env.KAT }}, ${{ env.EXAMPLES }}, ${{ env.STACK }}, ${{ env.UNIT }}, ${{ env.ALLOC }}, ${{ env.SIGN_HOOK }}, ${{ env.RNGFAIL }})
123127
shell: ${{ env.SHELL }}
124128
env:
125129
GH_TOKEN: ${{ inputs.gh_token }}
126130
run: |
127131
make clean
128-
${{ inputs.extra_env }} ./scripts/tests all ${{ inputs.check_namespace == 'true' && '--check-namespace' || ''}} --exec-wrapper="${{ inputs.exec_wrapper }}" --cross-prefix="${{ inputs.cross_prefix }}" --cflags="${{ inputs.cflags }}" --ldflags="${{ inputs.ldflags }}" --opt=${{ inputs.opt }} --${{ env.FUNC }} --${{ env.KAT }} --${{ env.ACVP }} --${{ env.WYCHEPROOF }} --${{ env.EXAMPLES }} --${{ env.STACK }} --${{ env.UNIT }} --${{ env.ALLOC }} --${{ env.RNGFAIL }} -v ${{ inputs.extra_args }}
132+
${{ inputs.extra_env }} ./scripts/tests all ${{ inputs.check_namespace == 'true' && '--check-namespace' || ''}} --exec-wrapper="${{ inputs.exec_wrapper }}" --cross-prefix="${{ inputs.cross_prefix }}" --cflags="${{ inputs.cflags }}" --ldflags="${{ inputs.ldflags }}" --opt=${{ inputs.opt }} --${{ env.FUNC }} --${{ env.KAT }} --${{ env.ACVP }} --${{ env.WYCHEPROOF }} --${{ env.EXAMPLES }} --${{ env.STACK }} --${{ env.UNIT }} --${{ env.ALLOC }} --${{ env.SIGN_HOOK }} --${{ env.RNGFAIL }} -v ${{ inputs.extra_args }}
129133
- name: Post ${{ env.MODE }} Tests
130134
shell: ${{ env.SHELL }}
131135
if: success() || failure()

.github/actions/multi-functest/action.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ inputs:
6363
alloc:
6464
description: Determine whether to run alloc tests or not
6565
default: "true"
66+
sign_hook:
67+
description: Determine whether to run sign hook tests or not
68+
default: "true"
6669
rng_fail:
6770
description: Determine whether to run rng fail tests or not
6871
default: "true"
@@ -96,6 +99,7 @@ runs:
9699
check_namespace: ${{ inputs.check_namespace }}
97100
stack: ${{ inputs.stack }}
98101
alloc: ${{ inputs.alloc }}
102+
sign_hook: ${{ inputs.sign_hook }}
99103
rng_fail: ${{ inputs.rng_fail }}
100104
extra_args: ${{ inputs.extra_args }}
101105
extra_env: ${{ inputs.extra_env }}
@@ -122,6 +126,7 @@ runs:
122126
check_namespace: ${{ inputs.check_namespace }}
123127
stack: ${{ inputs.stack }}
124128
alloc: ${{ inputs.alloc }}
129+
sign_hook: ${{ inputs.sign_hook }}
125130
rng_fail: ${{ inputs.rng_fail }}
126131
extra_args: ${{ inputs.extra_args }}
127132
extra_env: ${{ inputs.extra_env }}
@@ -148,6 +153,7 @@ runs:
148153
check_namespace: ${{ inputs.check_namespace }}
149154
stack: ${{ inputs.stack }}
150155
alloc: ${{ inputs.alloc }}
156+
sign_hook: ${{ inputs.sign_hook }}
151157
rng_fail: ${{ inputs.rng_fail }}
152158
extra_args: ${{ inputs.extra_args }}
153159
extra_env: ${{ inputs.extra_env }}
@@ -174,6 +180,7 @@ runs:
174180
check_namespace: ${{ inputs.check_namespace }}
175181
stack: ${{ inputs.stack }}
176182
alloc: ${{ inputs.alloc }}
183+
sign_hook: ${{ inputs.sign_hook }}
177184
rng_fail: ${{ inputs.rng_fail }}
178185
extra_args: ${{ inputs.extra_args }}
179186
extra_env: ${{ inputs.extra_env }}
@@ -200,6 +207,7 @@ runs:
200207
check_namespace: ${{ inputs.check_namespace }}
201208
stack: ${{ inputs.stack }}
202209
alloc: ${{ inputs.alloc }}
210+
sign_hook: ${{ inputs.sign_hook }}
203211
rng_fail: ${{ inputs.rng_fail }}
204212
extra_args: ${{ inputs.extra_args }}
205213
extra_env: ${{ inputs.extra_env }}
@@ -226,6 +234,7 @@ runs:
226234
check_namespace: ${{ inputs.check_namespace }}
227235
stack: ${{ inputs.stack }}
228236
alloc: ${{ inputs.alloc }}
237+
sign_hook: ${{ inputs.sign_hook }}
229238
rng_fail: ${{ inputs.rng_fail }}
230239
extra_args: ${{ inputs.extra_args }}
231240
extra_env: ${{ inputs.extra_env }}
@@ -252,6 +261,7 @@ runs:
252261
check_namespace: ${{ inputs.check_namespace }}
253262
stack: ${{ inputs.stack }}
254263
alloc: ${{ inputs.alloc }}
264+
sign_hook: ${{ inputs.sign_hook }}
255265
rng_fail: ${{ inputs.rng_fail }}
256266
extra_args: ${{ inputs.extra_args }}
257267
extra_env: ${{ inputs.extra_env }}

.github/workflows/ci.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,11 +435,53 @@ jobs:
435435
stack: false
436436
unit: true
437437
alloc: false
438+
sign_hook: false # Requires custom config
438439
rng_fail: false
439440
check_namespace: false
440441
# Disable AArch64 SHA3 extension: valgrind cannot emulate it
441442
extra_env: "MK_COMPILER_SUPPORTS_SHA3=0"
442443
exec_wrapper: "valgrind --error-exitcode=1"
444+
sign_hook_distribution:
445+
name: "Sign-hook attempt distribution (${{ matrix.target.name }}, ML-DSA-${{ matrix.level }})"
446+
strategy:
447+
fail-fast: false
448+
# One matrix entry per (architecture, parameter set): each level runs on
449+
# its own runner so the three ~9-minute 5M-signature runs proceed in
450+
# parallel rather than sequentially.
451+
matrix:
452+
external:
453+
- ${{ github.repository_owner != 'pq-code-package' }}
454+
target:
455+
- runner: ubuntu-latest
456+
name: x86_64
457+
- runner: ubuntu-24.04-arm
458+
name: aarch64
459+
level: [44, 65, 87]
460+
exclude:
461+
- external: true
462+
runs-on: ${{ matrix.target.runner }}
463+
steps:
464+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
465+
# Fresh random seed each run: the test draws fresh keys and signing
466+
# randomness from it, so this samples the attempts-per-signature
467+
# distribution anew every time. The seed is printed by the test, so a
468+
# failure is reproducible by re-running with the same SIGN_HOOK_SEED.
469+
- name: Generate random seed
470+
run: echo "SIGN_HOOK_SEED=seed=$(openssl rand -hex 32)" >> "$GITHUB_ENV"
471+
- uses: ./.github/actions/setup-shell
472+
with:
473+
gh_token: ${{ secrets.GITHUB_TOKEN }}
474+
nix-shell: 'ci'
475+
# 5,000,000 signatures drive the relative standard error to ~0.04%, so
476+
# the ratio check runs at a +-0.5% (5 per-mille) tolerance: a ~12 sigma
477+
# band (never flaky) that still catches any >=1% bias.
478+
# SIGN_HOOK_SEED (exported to $GITHUB_ENV by the previous step) is a
479+
# job-level env var, so make and the test binary inherit it directly.
480+
# setup-shell runs the script via `eval`, which strips one level of
481+
# quoting; the backslash-escaped quotes keep the space-containing
482+
# CFLAGS value intact as a single make environment variable.
483+
script: |
484+
CFLAGS=\"-DMLD_SIGN_HOOK_DIST_ITERATIONS=5000000 -DMLD_SIGN_HOOK_RATIO_TOL_PERMILLE=5\" make run_sign_hook_${{ matrix.level }} OPT=1 AUTO=1 -j$(nproc)
443485
config_variations:
444486
name: "Config: ${{ matrix.test.name }} (${{ matrix.target.name }})"
445487
strategy:

0 commit comments

Comments
 (0)