CI - dudect Constant-Time Verification #689
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI - dudect Constant-Time Verification | |
| # RULESET / REQUIRED-CHECKS NOTE | |
| # ------------------------------ | |
| # This workflow is path-filtered to PRs that touch C source / dudect | |
| # harness code (see `paths:` blocks below). Audit Issue 11 widened the | |
| # trigger to also include include/** (where ama_dispatch.h declares | |
| # which SIMD slot is taken) and ama_cryptography/** (the Python-side | |
| # nonce state and ctypes shims that select between scalar / SIMD code | |
| # paths). A change in either of those trees can flip which dispatch | |
| # lane the production library hits at runtime without modifying any | |
| # file under src/c/ — and before this widening the dudect gate would | |
| # silently skip such PRs. | |
| # | |
| # PRs that don't touch any of those paths still won't trigger this | |
| # workflow, so its named jobs do NOT emit a status check on such PRs. | |
| # Consequence for branch rulesets: do NOT add these names to the | |
| # `main` branch's required-status-check list. A required check that | |
| # is never reported on path-filtered PRs blocks merge indefinitely | |
| # with 'Expected — Waiting for status to be reported'. Same reasoning | |
| # as `baseline-guard.yml::Enforce baseline.json justification` and the | |
| # wiki-sync job — see PR #289 description ('Deliberately not included') | |
| # for the canonical exclusion list. | |
| # | |
| # The dudect gate still fails closed when it DOES run (no | |
| # `continue-on-error`, no silent skips inside the harness), which is | |
| # the regression case it exists to catch. | |
| # | |
| # The nightly `dudect-simd-sweep` job (added 2026-05 — audit Issue 3) | |
| # runs unconditionally on a cron schedule even when no path-filter | |
| # fires, so the per-PR coverage shrinks to "changed code" and the | |
| # full SIMD dispatch-table sweep happens out of the critical path. | |
| on: | |
| push: | |
| branches: [ main, develop, 'feature/**', 'fix/**' ] | |
| paths: | |
| - 'src/c/**' | |
| - 'include/**' | |
| - 'ama_cryptography/**' | |
| - 'tests/c/test_dudect.c' | |
| - 'tests/c/dudect/**' | |
| - '.github/workflows/dudect.yml' | |
| pull_request: | |
| branches: [ main, develop ] | |
| paths: | |
| - 'src/c/**' | |
| - 'include/**' | |
| - 'ama_cryptography/**' | |
| - 'tests/c/test_dudect.c' | |
| - 'tests/c/dudect/**' | |
| schedule: | |
| # Run weekly on Sundays at 02:00 UTC (existing schedule). | |
| - cron: '0 2 * * 0' | |
| # Nightly SIMD sweep at 03:30 UTC (audit Issue 3) — see | |
| # `dudect-simd-sweep` job below for the slot inventory. | |
| - cron: '30 3 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| measurements: | |
| description: 'Number of measurements per test' | |
| required: false | |
| default: '100000' | |
| permissions: | |
| contents: read | |
| # Collapse overlapping runs on the same ref. Main and the weekly + | |
| # nightly schedules are preserved. | |
| concurrency: | |
| group: dudect-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.event_name != 'schedule' }} | |
| jobs: | |
| dudect-utility: | |
| name: dudect - Utility Functions | |
| # The four legacy dudect lanes (utility / pqc / legacy-harnesses / | |
| # x25519-avx2-batch) only fire on: | |
| # - push / pull_request (path-filtered above) | |
| # - workflow_dispatch (operator-triggered) | |
| # - the WEEKLY schedule '0 2 * * 0' (Sunday 02:00 UTC) | |
| # The new NIGHTLY schedule '30 3 * * *' (audit Issue 3) is for the | |
| # `dudect-simd-sweep` job only. Without this guard, every nightly | |
| # run would also re-execute the four legacy lanes — wasted compute | |
| # the SIMD sweep doesn't need and explicit operational cost the | |
| # weekly schedule already covers. Copilot review #322 follow-up. | |
| if: | | |
| github.event_name != 'schedule' || | |
| github.event.schedule == '0 2 * * 0' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Install dependencies | |
| run: | | |
| sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list /etc/apt/sources.list.d/azure-cli.list || true | |
| sudo apt-get update | |
| sudo apt-get install -y build-essential cmake | |
| - name: Build with dudect enabled | |
| run: | | |
| cmake -B build \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DAMA_ENABLE_DUDECT=ON \ | |
| -DAMA_USE_NATIVE_PQC=ON \ | |
| -DAMA_ENABLE_LTO=OFF \ | |
| -DAMA_BUILD_EXAMPLES=OFF | |
| cmake --build build -j$(nproc) | |
| - name: Run dudect tests (utility functions) | |
| env: | |
| MEASUREMENTS: ${{ github.event.inputs.measurements || '100000' }} | |
| run: | | |
| # Pin to single core; best-effort priority elevation. | |
| # GHA hosted runners lack CAP_SYS_NICE so `nice -n -10` would | |
| # print "Permission denied" but still exec the binary — | |
| # detect that once up front and drop the nice prefix so the | |
| # log stays clean. The harness setup-symmetry fixes | |
| # (v3.2.0) made the lanes noise-tolerant enough that | |
| # taskset-only pinning is the load-bearing CI gate. | |
| NICE_PREFIX="" | |
| if nice -n -10 true 2>/dev/null; then NICE_PREFIX="nice -n -10"; fi | |
| taskset -c 0 $NICE_PREFIX \ | |
| ./build/bin/test_dudect --measurements "$MEASUREMENTS" --timeout 300 | |
| dudect-pqc: | |
| name: dudect - PQC Primitives | |
| # Same legacy-lane schedule guard as dudect-utility — see that | |
| # job's header for rationale. | |
| if: | | |
| github.event_name != 'schedule' || | |
| github.event.schedule == '0 2 * * 0' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Install dependencies | |
| run: | | |
| sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list /etc/apt/sources.list.d/azure-cli.list || true | |
| sudo apt-get update | |
| sudo apt-get install -y build-essential cmake | |
| - name: Build with dudect and PQC enabled | |
| run: | | |
| cmake -B build \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DAMA_ENABLE_DUDECT=ON \ | |
| -DAMA_USE_NATIVE_PQC=ON \ | |
| -DAMA_AES_CONSTTIME=ON \ | |
| -DAMA_ENABLE_LTO=OFF \ | |
| -DAMA_BUILD_EXAMPLES=OFF | |
| cmake --build build -j$(nproc) | |
| - name: Run dudect tests (all functions incl. PQC) | |
| env: | |
| MEASUREMENTS: ${{ github.event.inputs.measurements || '100000' }} | |
| run: | | |
| # Best-effort priority elevation — see `dudect-utility` for rationale. | |
| NICE_PREFIX="" | |
| if nice -n -10 true 2>/dev/null; then NICE_PREFIX="nice -n -10"; fi | |
| taskset -c 0 $NICE_PREFIX \ | |
| ./build/bin/test_dudect --measurements "$MEASUREMENTS" --timeout 600 | |
| dudect-legacy-harnesses: | |
| name: dudect - Legacy Harnesses (tools/constant_time) | |
| # Same legacy-lane schedule guard as dudect-utility. | |
| if: | | |
| github.event_name != 'schedule' || | |
| github.event.schedule == '0 2 * * 0' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Install dependencies | |
| run: | | |
| sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list /etc/apt/sources.list.d/azure-cli.list || true | |
| sudo apt-get update | |
| sudo apt-get install -y build-essential cmake | |
| - name: Build C library | |
| run: | | |
| cmake -B build -DAMA_USE_NATIVE_PQC=ON -DAMA_ENABLE_LTO=OFF | |
| cmake --build build -j$(nproc) | |
| - name: Build and run legacy dudect harnesses | |
| run: | | |
| cd tools/constant_time | |
| make clean && make all | |
| # Best-effort priority elevation — see `dudect-utility` for rationale. | |
| NICE_PREFIX="" | |
| if nice -n -10 true 2>/dev/null; then NICE_PREFIX="nice -n -10"; fi | |
| echo "=== Utility function timing analysis ===" | |
| taskset -c 0 $NICE_PREFIX ./dudect_harness 50000 | |
| echo "=== Crypto primitive timing analysis ===" | |
| taskset -c 0 $NICE_PREFIX ./dudect_crypto 50000 | |
| dudect-x25519-avx2-batch: | |
| # Re-runs the dudect binary with AMA_DISPATCH_USE_X25519_AVX2=1 so | |
| # the `X25519 scalarmult batch×4` lane actually exercises the | |
| # 4-way AVX2 kernel rather than four sequential scalar ladders. | |
| # Without this opt-in, the batch lane measures the scalar path | |
| # (the dispatcher leaves x25519_x4 NULL by default — see | |
| # CHANGELOG [3.0.0] §Performance for the rationale) and the SIMD | |
| # kernel's constant-time signal is never sampled in CI. | |
| name: dudect - X25519 AVX2 4-way (opt-in) | |
| # Same legacy-lane schedule guard as dudect-utility. | |
| if: | | |
| github.event_name != 'schedule' || | |
| github.event.schedule == '0 2 * * 0' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Install dependencies | |
| run: | | |
| sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list /etc/apt/sources.list.d/azure-cli.list || true | |
| sudo apt-get update | |
| sudo apt-get install -y build-essential cmake | |
| - name: Build with dudect enabled | |
| run: | | |
| cmake -B build \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DAMA_ENABLE_DUDECT=ON \ | |
| -DAMA_USE_NATIVE_PQC=ON \ | |
| -DAMA_ENABLE_LTO=OFF \ | |
| -DAMA_BUILD_EXAMPLES=OFF | |
| cmake --build build -j$(nproc) | |
| - name: Run dudect with X25519 AVX2 4-way kernel wired in | |
| env: | |
| MEASUREMENTS: ${{ github.event.inputs.measurements || '100000' }} | |
| AMA_DISPATCH_USE_X25519_AVX2: '1' | |
| run: | | |
| # Best-effort priority elevation — see `dudect-utility` for rationale. | |
| NICE_PREFIX="" | |
| if nice -n -10 true 2>/dev/null; then NICE_PREFIX="nice -n -10"; fi | |
| taskset -c 0 $NICE_PREFIX \ | |
| ./build/bin/test_dudect --measurements "$MEASUREMENTS" --timeout 300 | |
| # =========================================================================== | |
| # SIMD sweep — nightly cron only (audit Issue 3 close-out) | |
| # =========================================================================== | |
| # | |
| # The four legacy jobs above sample the scalar / generic paths and a | |
| # single opt-in AVX2 X25519 lane. This sweep is now PER-SLOT: each | |
| # matrix cell sets `AMA_DISPATCH_ONLY=<slot>` so the dispatcher | |
| # leaves every kernel pointer at its scalar fallback EXCEPT the | |
| # named one. The resulting t-value is attributable to ONE SIMD | |
| # kernel rather than to whichever AVX2 paths happened to fire under | |
| # the same dispatch invocation. See `apply_dispatch_only()` in | |
| # src/c/dispatch/ama_dispatch.c for the per-slot resolution and | |
| # `tests/c/test_dispatch_only_env.c` for the end-to-end contract | |
| # check that pins the env-var → active-slot mapping. | |
| # | |
| # Matrix: | |
| # | |
| # os = ubuntu-latest (x86-64 host) | |
| # ubuntu-24.04-arm (AArch64 / NEON / [SVE2 if available] host) | |
| # | |
| # slot — the full dispatch-table-routable SIMD inventory. The | |
| # `exclude:` entries below remove cells that the runner's CPU | |
| # architecture cannot satisfy (e.g., NEON / SVE2 / aes-gcm-neon | |
| # on an x86-64 host; AVX-* slots on an ARM host). Slots whose | |
| # CPU feature is *not* present at runtime on a host that COULD | |
| # theoretically satisfy them (e.g., ubuntu-24.04-arm without | |
| # SVE2 silicon) self-skip via CTest exit 77 inside test_dudect. | |
| # | |
| # The sweep is scheduled-only (cron) to keep PR latency low. The | |
| # legacy per-PR jobs above remain the gate for changed source code. | |
| # If a sweep lane FAILS its strict t-value threshold, the failure | |
| # is raised as a regular GitHub Actions failure on the schedule | |
| # run — the project owner reviews it and files a follow-up before | |
| # merging any new SIMD work. | |
| dudect-simd-sweep: | |
| name: dudect - SIMD sweep (${{ matrix.os }} / ${{ matrix.slot }}) | |
| # Fires on: | |
| # - the NIGHTLY schedule '30 3 * * *' (audit Issue 3) | |
| # - workflow_dispatch (operator-triggered) | |
| # The WEEKLY '0 2 * * 0' schedule is reserved for the four legacy | |
| # lanes above; running the sweep on both weekly and nightly would | |
| # double-bill compute without adding coverage. | |
| if: | | |
| (github.event_name == 'schedule' && github.event.schedule == '30 3 * * *') || | |
| github.event_name == 'workflow_dispatch' | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 30 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, ubuntu-24.04-arm] | |
| slot: | |
| - sha3-avx512x4 | |
| - kyber-ntt-avx2 | |
| - dilithium-ntt-avx2 | |
| - chacha20-avx2x8 | |
| - argon2-g-avx2 | |
| - aes-gcm-neon | |
| - chacha20-neon | |
| - sha3-neon | |
| - kyber-sve2 | |
| - sha3-sve2 | |
| - x25519-avx2 | |
| exclude: | |
| # x86-64 hosts (ubuntu-latest): exclude NEON + SVE2 slots. | |
| # The kernels are AArch64-only; the dispatcher will report | |
| # the slot unsupported and CTest will treat the cell as a | |
| # silent pass — wasting compute and producing a confusing | |
| # green run. Skip them at the matrix level instead. | |
| - os: ubuntu-latest | |
| slot: aes-gcm-neon | |
| - os: ubuntu-latest | |
| slot: chacha20-neon | |
| - os: ubuntu-latest | |
| slot: sha3-neon | |
| - os: ubuntu-latest | |
| slot: kyber-sve2 | |
| - os: ubuntu-latest | |
| slot: sha3-sve2 | |
| # AArch64 hosts (ubuntu-24.04-arm): exclude AVX-* slots and | |
| # the AVX2 X25519 lane. Same rationale as above, with | |
| # `aes-gcm-neon` / `chacha20-neon` / `sha3-neon` retained | |
| # on this side because AArch64 mandates NEON. SVE2 slots | |
| # are KEPT on this side — when the runner's silicon lacks | |
| # SVE2 the dispatcher reports the slot unsupported and the | |
| # test exits 77 (Skipped), preserving an audit trail. | |
| - os: ubuntu-24.04-arm | |
| slot: sha3-avx512x4 | |
| - os: ubuntu-24.04-arm | |
| slot: kyber-ntt-avx2 | |
| - os: ubuntu-24.04-arm | |
| slot: dilithium-ntt-avx2 | |
| - os: ubuntu-24.04-arm | |
| slot: chacha20-avx2x8 | |
| - os: ubuntu-24.04-arm | |
| slot: argon2-g-avx2 | |
| - os: ubuntu-24.04-arm | |
| slot: x25519-avx2 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Install dependencies | |
| run: | | |
| sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list /etc/apt/sources.list.d/azure-cli.list || true | |
| sudo apt-get update | |
| sudo apt-get install -y build-essential cmake | |
| - name: Build with dudect + every SIMD path the host supports | |
| run: | | |
| # AMA_ENABLE_SIMD / NEON / AVX2 default ON in CMakeLists.txt; | |
| # we set them explicitly here to insulate the sweep from a | |
| # future change of those defaults. AVX-512 is opt-in (see | |
| # CMakeLists.txt:64); enable it on x86-64 so the AVX-512 | |
| # 4-way Keccak kernel is sampled when the runner exposes it. | |
| cmake -B build \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DAMA_ENABLE_DUDECT=ON \ | |
| -DAMA_BUILD_TESTS=ON \ | |
| -DAMA_USE_NATIVE_PQC=ON \ | |
| -DAMA_AES_CONSTTIME=ON \ | |
| -DAMA_ENABLE_SIMD=ON \ | |
| -DAMA_ENABLE_AVX2=ON \ | |
| -DAMA_ENABLE_AVX512=ON \ | |
| -DAMA_ENABLE_NEON=ON \ | |
| -DAMA_ENABLE_LTO=OFF \ | |
| -DAMA_BUILD_EXAMPLES=OFF | |
| cmake --build build -j$(nproc) | |
| - name: Confirm dispatch slot resolves on this host | |
| # Before burning a 15-minute dudect run, assert via the | |
| # per-slot contract test that AMA_DISPATCH_ONLY actually | |
| # honored the requested slot on this runner. Exit code 77 | |
| # from the contract binary means "Skipped" — propagate that | |
| # as a step output (`supported=false`) so the dudect step | |
| # below skips this matrix cell entirely. Without the | |
| # supported-flag + downstream `if:`, a `exit 0` here would | |
| # NOT prevent the subsequent dudect lane from firing on a | |
| # scalar-fallback dispatch table — Copilot review #323 | |
| # follow-up. | |
| id: confirm | |
| env: | |
| AMA_DISPATCH_ONLY: ${{ matrix.slot }} | |
| # x25519-avx2 requires the opt-in flag in addition to | |
| # AMA_DISPATCH_ONLY (see dispatch_init_internal() | |
| # rationale in src/c/dispatch/ama_dispatch.c — the AVX2 | |
| # 4-way ladder is opt-in by default because the scalar fe64 | |
| # path beats it on Skylake-Cascade-class cores). Setting | |
| # the flag here for every cell is a no-op for the others. | |
| AMA_DISPATCH_USE_X25519_AVX2: '1' | |
| # Pin off the auto-tune microbench so a noisy runner can't | |
| # demote the requested SIMD slot back to generic and turn | |
| # the dudect measurement into a scalar-baseline run. | |
| AMA_DISPATCH_NO_AUTOTUNE: '1' | |
| run: | | |
| set -e | |
| if [ -x ./build/bin/test_dispatch_only_env ]; then | |
| TEST_BIN=./build/bin/test_dispatch_only_env | |
| else | |
| TEST_BIN=./build/tests/c/test_dispatch_only_env | |
| fi | |
| rc=0 | |
| "$TEST_BIN" || rc=$? | |
| if [ "$rc" -eq 77 ]; then | |
| echo "supported=false" >> "$GITHUB_OUTPUT" | |
| echo "::warning::Slot '${{ matrix.slot }}' unsupported on this host — skipping dudect lane." | |
| exit 0 | |
| elif [ "$rc" -ne 0 ]; then | |
| echo "supported=false" >> "$GITHUB_OUTPUT" | |
| echo "::error::Dispatch contract test failed for '${{ matrix.slot }}' (exit=$rc)" | |
| exit "$rc" | |
| fi | |
| echo "supported=true" >> "$GITHUB_OUTPUT" | |
| - name: Run dudect with AMA_DISPATCH_ONLY=${{ matrix.slot }} | |
| # Skip this lane entirely when the confirmation step above | |
| # reported the slot as unsupported — measuring a scalar | |
| # fallback under the per-slot label would produce a | |
| # misleading t-value attributed to a kernel that didn't run. | |
| if: steps.confirm.outputs.supported == 'true' | |
| env: | |
| MEASUREMENTS: ${{ github.event.inputs.measurements || '100000' }} | |
| AMA_DISPATCH_ONLY: ${{ matrix.slot }} | |
| AMA_DISPATCH_USE_X25519_AVX2: '1' | |
| AMA_DISPATCH_NO_AUTOTUNE: '1' | |
| run: | | |
| # Best-effort priority elevation — see `dudect-utility` for rationale. | |
| NICE_PREFIX="" | |
| if nice -n -10 true 2>/dev/null; then NICE_PREFIX="nice -n -10"; fi | |
| taskset -c 0 $NICE_PREFIX \ | |
| ./build/bin/test_dudect --measurements "$MEASUREMENTS" --timeout 900 |