Skip to content

CI - dudect Constant-Time Verification #689

CI - dudect Constant-Time Verification

CI - dudect Constant-Time Verification #689

Workflow file for this run

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