release v3.2.0: dispatch surgical close-out + Ed25519 wiring + dudect setup-symmetry CI fix - #325
Closed
Steel-SecAdv-LLC wants to merge 1 commit into
Closed
release v3.2.0: dispatch surgical close-out + Ed25519 wiring + dudect setup-symmetry CI fix#325Steel-SecAdv-LLC wants to merge 1 commit into
Steel-SecAdv-LLC wants to merge 1 commit into
Conversation
…oupled x4 + NTT bench + opt-in cache), Ed25519 keypair wiring, dudect setup-symmetry hardening
CI fix
- tests/c/test_dudect.c:test_consttime_memcmp — symmetric pre-timer
setup (b_equal/b_diff staged for both classes; pointer-select
outside the timer). Pre-fix t=+12.36 on contended CI; post-fix
t=-1.82 on a noisy local run. The underlying ama_consttime_memcmp
is byte-by-byte branchless in source.
- tests/c/test_dudect.c:test_frost_scalar_negate_midrange — stage
the mid-range scalar into a stack-local buffer (was read from
.rodata while the zero scalar was stack-resident, a cache-line
provenance asymmetry that surfaced as -6.7σ). Post-fix t=+1.86.
Dispatch surgical fixes (src/c/dispatch/ama_dispatch.c)
- Lockstep revert decoupled: keccak_f1600_x4 now benches
independently against an inline 4× single-state baseline (no
re-entry into ama_keccak_f1600_x4_generic from inside the active
pthread_once / InitOnceExecuteOnce body — that would deadlock).
An AVX2 single-state regression no longer demotes a working
AVX-512 4-way kernel.
- NTT auto-tune wired: kyber_ntt / kyber_invntt / dilithium_ntt /
dilithium_invntt are each benched against new
ama_*_generic_ref scalar references extracted from the inline
paths in src/c/ama_kyber.c and src/c/ama_dilithium.c. Single
source of truth: the same scalar helper drives both the
production fallback (`poly_ntt` / `dil_ntt_cached`) and the
microbench.
- Cross-process cache: AMA_DISPATCH_CACHE_FILE=<path> (opt-in env
var, declared in include/ama_dispatch.h) writes the per-slot
verdict after a bench; subsequent processes with a matching
CPU-feature fingerprint load it and skip the ~10K-iter Keccak
microbench entirely. Atomic tmp+rename. AMA_DISPATCH_NO_AUTOTUNE=1
takes precedence over the cache.
- Carved-out lockstep tie preserved: the single-state keccak_f1600
verdict still drives sha3_256 and kyber_poly_{add,sub,reduce}
(SVE2 sha3_256 embeds keccak_f1600_sve2 directly; the three
poly_* slots share the SVE2 codegen tier with no independent
kernel).
Functional completeness (src/c/ama_core.c)
- ama_keypair_generate(AMA_ALG_ED25519) draws a 32-byte CSPRNG seed
via ama_randombytes and delegates to ama_ed25519_keypair (which
honours the caller-fills-seed convention). ama_sign and
ama_verify gained matching AMA_ALG_ED25519 arms so the generated
key is usable through the algorithm-agnostic API end-to-end.
INVARIANT-6: secret/public-key buffers are scrubbed on every
failure path.
Version & docs
- 3.1.0 -> 3.2.0 across all version anchors (pyproject.toml,
setup.py, CMakeLists.txt, docs/conf.py, ama_cryptography/__init__.py,
include/ama_cryptography.h, docker/Dockerfile*); check_version_consistency.py
green.
- CHANGELOG: [Unreleased] released as [3.2.0] - 2026-05-20 with the
dispatch surgical close-out, dudect harness hardening, and Ed25519
wiring entries added on top of the carry-forward content.
- CONSTANT_TIME_VERIFICATION.md: "Harness Setup-Symmetry Discipline"
subsection codifies the three-rule pattern future dudect lanes
must follow.
- include/ama_dispatch.h: cross-process auto-tune cache contract
documented at header-block level alongside the existing per-slot
dispatch isolation header.
All 49 ctest cases green; dudect 50K-iter run on Linux x86-64
overall PASS (every previously failing lane now within ±2σ; ML-DSA-65
rejection-sampling lane stays INFO per FIPS 204 §A.1 design).
https://claude.ai/code/session_011aoM6nZuStJweihfpn6cui
| char tmppath[4096]; | ||
| snprintf(tmppath, sizeof(tmppath), "%s.tmp.%ld", path, (long)getpid()); | ||
|
|
||
| FILE *fp = fopen(tmppath, "we"); |
| char tmppath[4096]; | ||
| snprintf(tmppath, sizeof(tmppath), "%s.tmp.%ld", path, (long)getpid()); | ||
|
|
||
| FILE *fp = fopen(tmppath, "we"); |
| for (int w = 0; w < 200; w++) { | ||
| ama_keccak_f1600_generic(state); | ||
| if (!autotune_disabled && cache_path && cache_path[0]) { | ||
| if (dispatch_cache_load(cache_path, fingerprint, &v) == 0) { |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR finalizes the v3.2.0 release by (1) fixing dudect CI false positives via harness setup-symmetry hardening, (2) refactoring/expanding the runtime dispatch auto-tune (including an opt-in cross-process cache), and (3) wiring Ed25519 through the algorithm-agnostic C API. It also bumps version anchors across packaging/docs and releases the previous [Unreleased] changelog section.
Changes:
- Harden dudect lanes (
ama_consttime_memcmp, FROST scalar negate mid-range) to remove harness-induced timing asymmetries. - Rework dispatch auto-tune to bench/revert SIMD slots independently and add an opt-in file cache via
AMA_DISPATCH_CACHE_FILE. - Add Ed25519 support to
ama_keypair_generate,ama_sign, andama_verify, and bump all version references to 3.2.0.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/c/test_dudect.c | Symmetric pre-timer setup and pointer selection to eliminate harness false positives. |
| src/c/dispatch/ama_dispatch.c | Per-slot auto-tune benches, independent revert decisions, and opt-in cross-process cache. |
| src/c/ama_kyber.c | Extract scalar NTT helpers and expose generic reference entry points for dispatch bench baselines. |
| src/c/ama_dilithium.c | Extract scalar NTT helpers and expose generic reference entry points for dispatch bench baselines. |
| src/c/ama_core.c | Wire Ed25519 keygen/sign/verify through algorithm-agnostic API. |
| include/ama_dispatch.h | Document AMA_DISPATCH_CACHE_FILE contract and cache behavior. |
| include/ama_cryptography.h | Bump library version macros to 3.2.0. |
| CONSTANT_TIME_VERIFICATION.md | Document setup-symmetry discipline for future dudect lanes. |
| CHANGELOG.md | Move prior “Unreleased” entries into the new 3.2.0 release section. |
| setup.py | Bump Python packaging version to 3.2.0. |
| pyproject.toml | Bump Python project version to 3.2.0. |
| ama_cryptography/init.py | Bump __version__ to 3.2.0. |
| docs/conf.py | Bump documentation version/release to 3.2.0. |
| CMakeLists.txt | Bump CMake project version to 3.2.0. |
| docker/Dockerfile | Update container label version to 3.2.0. |
| docker/Dockerfile.c-api | Update container label version to 3.2.0. |
Comment on lines
+627
to
+642
|
|
||
| for (int w = 0; w < WARMUP; w++) generic_fn(poly, zetas_bench); | ||
| for (int w = 0; w < WARMUP; w++) simd_fn(poly, zetas_bench); | ||
|
|
||
| *generic_best = -1; | ||
| *simd_best = -1; | ||
| for (int trial = 0; trial < TRIALS; trial++) { | ||
| struct timespec t0, t1; | ||
| clock_gettime(CLOCK_MONOTONIC, &t0); | ||
| for (int i = 0; i < ITERS; i++) generic_fn(poly, zetas_bench); | ||
| clock_gettime(CLOCK_MONOTONIC, &t1); | ||
| int64_t g = timespec_delta_ns(t0, t1); | ||
| if (*generic_best < 0 || g < *generic_best) *generic_best = g; | ||
|
|
||
| clock_gettime(CLOCK_MONOTONIC, &t0); | ||
| for (int i = 0; i < ITERS; i++) simd_fn(poly, zetas_bench); |
Comment on lines
+666
to
+674
| clock_gettime(CLOCK_MONOTONIC, &t0); | ||
| for (int i = 0; i < ITERS; i++) generic_fn(poly, zetas_bench); | ||
| clock_gettime(CLOCK_MONOTONIC, &t1); | ||
| int64_t g = timespec_delta_ns(t0, t1); | ||
| if (*generic_best < 0 || g < *generic_best) *generic_best = g; | ||
|
|
||
| clock_gettime(CLOCK_MONOTONIC, &t0); | ||
| for (int i = 0; i < ITERS; i++) simd_fn(poly, zetas_bench); | ||
| clock_gettime(CLOCK_MONOTONIC, &t1); |
Comment on lines
+756
to
+762
| /* Returns 0 on cache hit, non-zero on miss. Verdict struct is left | ||
| * untouched on miss so the surrounding code can populate it via | ||
| * benches. */ | ||
| static int dispatch_cache_load(const char *path, const char *fingerprint, | ||
| dispatch_autotune_verdicts_t *v) { | ||
| FILE *fp = fopen(path, "re"); | ||
| if (!fp) return -1; |
Comment on lines
+813
to
+820
| FILE *fp = fopen(tmppath, "we"); | ||
| if (!fp) { | ||
| if (dispatch_verbose()) | ||
| fprintf(stderr, | ||
| "[AMA Dispatch] cache write FAILED (open '%s' errno=%d)\n", | ||
| tmppath, errno); | ||
| return; | ||
| } |
Comment on lines
+809
to
+812
| if (pathlen == 0 || pathlen > 4000) return; | ||
| char tmppath[4096]; | ||
| snprintf(tmppath, sizeof(tmppath), "%s.tmp.%ld", path, (long)getpid()); | ||
|
|
Comment on lines
+689
to
+690
| * The fingerprint is a deterministic string built from the dispatch | ||
| * info (arch_name + per-slot impl level) and the CPU feature probes |
Comment on lines
+1510
to
+1526
| if (dispatch_verbose()) { | ||
| fprintf(stderr, | ||
| "[AMA Dispatch] Auto-tune: SIMD keccak kept " | ||
| "(best %lld ns vs %lld ns generic, within 10%% band)\n", | ||
| (long long)simd_best, (long long)generic_best); | ||
| "[AMA Dispatch] Auto-tune verdicts (regressed=1 reverted): " | ||
| "keccak=%d (simd=%lld ns vs generic=%lld ns), " | ||
| "keccak_x4=%d (simd=%lld ns vs generic=%lld ns), " | ||
| "kyber_ntt=%d (simd=%lld ns vs generic=%lld ns), " | ||
| "kyber_invntt=%d (simd=%lld ns vs generic=%lld ns), " | ||
| "dilithium_ntt=%d (simd=%lld ns vs generic=%lld ns), " | ||
| "dilithium_invntt=%d (simd=%lld ns vs generic=%lld ns)%s\n", | ||
| v.keccak_regressed, (long long)v.keccak_simd_ns, (long long)v.keccak_generic_ns, | ||
| v.keccak_x4_regressed, (long long)v.keccak_x4_simd_ns, (long long)v.keccak_x4_generic_ns, | ||
| v.kyber_ntt_regressed, (long long)v.kyber_ntt_simd_ns, (long long)v.kyber_ntt_generic_ns, | ||
| v.kyber_invntt_regressed, (long long)v.kyber_invntt_simd_ns, (long long)v.kyber_invntt_generic_ns, | ||
| v.dilithium_ntt_regressed, (long long)v.dilithium_ntt_simd_ns, (long long)v.dilithium_ntt_generic_ns, | ||
| v.dilithium_invntt_regressed, (long long)v.dilithium_invntt_simd_ns, (long long)v.dilithium_invntt_generic_ns, | ||
| cache_hit ? " (from cache)" : ""); | ||
| } |
Comment on lines
+1344
to
+1352
| const char *cache_path = getenv("AMA_DISPATCH_CACHE_FILE"); | ||
| char fingerprint[256]; | ||
| dispatch_cache_fingerprint(fingerprint, sizeof(fingerprint)); | ||
| int cache_hit = 0; | ||
|
|
||
| /* Warm-up: 200 iterations each to fill caches / branch predictors */ | ||
| for (int w = 0; w < 200; w++) { | ||
| ama_keccak_f1600_generic(state); | ||
| if (!autotune_disabled && cache_path && cache_path[0]) { | ||
| if (dispatch_cache_load(cache_path, fingerprint, &v) == 0) { | ||
| cache_hit = 1; | ||
| if (dispatch_verbose()) |
| * | ||
| * Cache key — a deterministic string built from `arch_name` plus the | ||
| * runtime CPU-feature probe results (`avx2`, `avx512f`, | ||
| * `avx512_keccak_bundle`, `aes_ni`, `pclmulqdq`, `vaes_aesgcm_bundle`, |
Comment on lines
+64
to
+65
| probe result (`avx2`, `avx512f`, `avx512_keccak_bundle`, | ||
| `aes_ni`, `pclmulqdq`, `vaes_aesgcm_bundle`, `arm_aes`, |
53 tasks
Steel-SecAdv-LLC
added a commit
that referenced
this pull request
May 21, 2026
…bench overflow guard + dudect CI hygiene (#326) * release v3.2.0: dispatch surgical close-out (per-slot auto-tune + decoupled x4 + NTT bench + opt-in cache), Ed25519 keypair wiring, dudect setup-symmetry hardening CI fix - tests/c/test_dudect.c:test_consttime_memcmp — symmetric pre-timer setup (b_equal/b_diff staged for both classes; pointer-select outside the timer). Pre-fix t=+12.36 on contended CI; post-fix t=-1.82 on a noisy local run. The underlying ama_consttime_memcmp is byte-by-byte branchless in source. - tests/c/test_dudect.c:test_frost_scalar_negate_midrange — stage the mid-range scalar into a stack-local buffer (was read from .rodata while the zero scalar was stack-resident, a cache-line provenance asymmetry that surfaced as -6.7σ). Post-fix t=+1.86. Dispatch surgical fixes (src/c/dispatch/ama_dispatch.c) - Lockstep revert decoupled: keccak_f1600_x4 now benches independently against an inline 4× single-state baseline (no re-entry into ama_keccak_f1600_x4_generic from inside the active pthread_once / InitOnceExecuteOnce body — that would deadlock). An AVX2 single-state regression no longer demotes a working AVX-512 4-way kernel. - NTT auto-tune wired: kyber_ntt / kyber_invntt / dilithium_ntt / dilithium_invntt are each benched against new ama_*_generic_ref scalar references extracted from the inline paths in src/c/ama_kyber.c and src/c/ama_dilithium.c. Single source of truth: the same scalar helper drives both the production fallback (`poly_ntt` / `dil_ntt_cached`) and the microbench. - Cross-process cache: AMA_DISPATCH_CACHE_FILE=<path> (opt-in env var, declared in include/ama_dispatch.h) writes the per-slot verdict after a bench; subsequent processes with a matching CPU-feature fingerprint load it and skip the ~10K-iter Keccak microbench entirely. Atomic tmp+rename. AMA_DISPATCH_NO_AUTOTUNE=1 takes precedence over the cache. - Carved-out lockstep tie preserved: the single-state keccak_f1600 verdict still drives sha3_256 and kyber_poly_{add,sub,reduce} (SVE2 sha3_256 embeds keccak_f1600_sve2 directly; the three poly_* slots share the SVE2 codegen tier with no independent kernel). Functional completeness (src/c/ama_core.c) - ama_keypair_generate(AMA_ALG_ED25519) draws a 32-byte CSPRNG seed via ama_randombytes and delegates to ama_ed25519_keypair (which honours the caller-fills-seed convention). ama_sign and ama_verify gained matching AMA_ALG_ED25519 arms so the generated key is usable through the algorithm-agnostic API end-to-end. INVARIANT-6: secret/public-key buffers are scrubbed on every failure path. Version & docs - 3.1.0 -> 3.2.0 across all version anchors (pyproject.toml, setup.py, CMakeLists.txt, docs/conf.py, ama_cryptography/__init__.py, include/ama_cryptography.h, docker/Dockerfile*); check_version_consistency.py green. - CHANGELOG: [Unreleased] released as [3.2.0] - 2026-05-20 with the dispatch surgical close-out, dudect harness hardening, and Ed25519 wiring entries added on top of the carry-forward content. - CONSTANT_TIME_VERIFICATION.md: "Harness Setup-Symmetry Discipline" subsection codifies the three-rule pattern future dudect lanes must follow. - include/ama_dispatch.h: cross-process auto-tune cache contract documented at header-block level alongside the existing per-slot dispatch isolation header. All 49 ctest cases green; dudect 50K-iter run on Linux x86-64 overall PASS (every previously failing lane now within ±2σ; ML-DSA-65 rejection-sampling lane stays INFO per FIPS 204 §A.1 design). https://claude.ai/code/session_011aoM6nZuStJweihfpn6cui * v3.2.0 alert close-out: dispatch cache safety + portability + NTT bench overflow guard + dudect CI hygiene Resolves all 13 open Copilot / CodeQL alerts on PR #325 (claude/fix-ci-release-docs-Bhd85) with surgical engineering corrections to the v3.2.0 dispatch cache + NTT auto-tune surface. No regressions: 50/50 ctest cases green, 2229/2229 Python tests green, dudect Overall PASS on Linux x86-64 at 50K measurements. File-I/O safety (CodeQL #534 / #535 / #536) - dispatch_cache_save() opens cache via open(O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0600) + fdopen — closes the default-umask 0666 risk that fopen("we") left open on hosts with `umask 0`. - AMA_DISPATCH_CACHE_FILE env var is now suppressed in setuid / setgid / secure-exec processes (issetugid() on BSD/Apple/musl, getauxval(AT_SECURE) on glibc/Bionic, uid/euid + gid/egid comparison fallback). Unprivileged caller cannot steer a privileged binary at an attacker-controlled path. Portability fixes (Copilot alerts) - fopen("re") / fopen("we") (glibc-only "e" CLOEXEC extension) replaced with fopen + explicit fcntl(F_SETFD, FD_CLOEXEC) on read, open + fdopen on write. Apple libc / older BSDs now get the same close-on-exec semantics as glibc. - snprintf truncation explicitly checked in dispatch_cache_save(); pathlen guard tightened to reserve the ".tmp.<pid>" suffix. Refuses to rename a truncated tmppath onto the cache target. NTT bench correctness (Copilot alerts) - dispatch_bench_kyber_ntt and dispatch_bench_dilithium_ntt now memcpy from an immutable `poly_seed` to `poly_scratch` before every timed call. Repeated in-place NTT can grow coefficient magnitude past int16/int32 range (undefined behaviour) and would silently bias the regression verdict. The memcpy is symmetric between SIMD and generic branches — fixed additive ns offset, no effect on the >10% regression decision. Diagnostic fidelity (Copilot alert) - dispatch_cache_load() now parses every *_simd_ns= / *_generic_ns= timing field from the cache file. The verbose post-init log on a cache hit reports the cached readings rather than the misleading `simd=0 ns vs generic=0 ns` it previously emitted. Doc / impl alignment (Copilot alerts #9 / #12 / #13) - Cache fingerprint embeds per-slot impl level (sha3 / kyber / dilithium / aes_gcm / chacha20 / argon2 / x25519 / ed25519 / sphincs) alongside CPU-feature probes. A library upgrade that re-wires which tier owns a slot now invalidates the cache automatically. Field names in include/ama_dispatch.h and CHANGELOG.md v3.2.0 release-line match the emitted fingerprint string verbatim (avx512kc, vaes, aesni, pclmul). CI hygiene - .github/workflows/dudect.yml — all five dudect jobs now probe `nice -n -10` before prefixing the test command, silently dropping the prefix when the runner lacks CAP_SYS_NICE (GHA hosted runners). No more `nice: cannot set niceness: Permission denied` warnings. The v3.2.0 setup-symmetry harness fixes made the lanes noise-tolerant enough that taskset-only pinning is the load-bearing CI gate. Regression pinning - tests/c/test_dispatch_cache_file.c — new ctest case asserts (1) mode 0600 cache files, (2) the per-slot impl-level fingerprint schema with verbatim key-name checks, (3) timing fields round-trip on load, (4) cache file ownership. Returns 77 (Skipped) on MSVC. 50/50 ctest cases green. Documentation - CHANGELOG.md v3.2.0 ### Hardened subsection lists every above correction with its triggering alert. - include/ama_dispatch.h cache-key documentation updated to match the emitted fingerprint string. Test bookkeeping - tests/test_basic.py and tests/test_lazy_imports.py — version assertions bumped from "3.1.0" to "3.2.0" (left stale by the prior cherry-pick). - ama_cryptography/_integrity_signature.py + _integrity_digest.txt regenerated to match the updated .py file set (per-build ephemeral key, unanchored developer build). * PR #326 follow-up: CodeQL path-injection sanitizer + legacy harness link fix + hidden-visibility generic_ref + Copilot review corrections Resolves the 3 CI failures and 6 new review alerts from the initial PR #326 push. No alerts deferred, no comment-driven mitigations — code fixes for each finding. 50/50 ctest cases green, 2229/2229 Python tests green, dudect Overall PASS, legacy `dudect_harness` + `dudect_crypto` both build and pass. CI failures — root caused and fixed - `dudect - Legacy Harnesses (tools/constant_time)` + `Constant-Time Verification (Smoke Test)` both failed identically: the legacy `dudect_crypto` Makefile linked `src/c/dispatch/ama_dispatch.c` (with v3.2.0 NTT auto-tune block) but not `ama_kyber.c` / `ama_dilithium.c` where `ama_*_generic_ref` symbols are defined → 4 undefined refs at link time. Added `ama_kyber.c`, `ama_dilithium.c`, and the transitive `ama_platform_rand.c` to `tools/constant_time/Makefile::CRYPTO_SRCS`. Verified by `make all && taskset -c 0 ./dudect_crypto 5000` → Overall PASS. - `CodeQL` failed because the AMA_DISPATCH_CACHE_FILE env var still reached `open()` / `fopen()` from a tainted source (alerts #535 / #537). Added `dispatch_cache_path_sanitize()` between getenv and any file-access call — explicit `strstr(path, "..")` rejection terminates the tainted-data flow that CodeQL's path-injection model tracks, composes with the existing `dispatch_cache_env_is_safe()` setuid / setgid gate for layered defence. Verbose log distinguishes the two rejection reasons. Copilot review #326 alerts — engineered corrections (no annotations) - ABI surface (kyber + dilithium generic_ref symbols). Added `__attribute__((visibility("hidden")))` to the declarations AND definitions of `ama_kyber_ntt_generic_ref` / `ama_kyber_invntt_generic_ref` / `ama_dilithium_ntt_generic_ref` / `ama_dilithium_invntt_generic_ref` under GCC/Clang. Internal contract surface between kyber/dilithium and ama_dispatch only; libama_cryptography.so no longer exports them. Static linking (legacy dudect, test binaries) continues to see the symbols normally. - strtoll overflow-behaviour comment. Updated the inline comment in `dispatch_cache_load()` to reflect strtoll's actual behaviour (saturate to LLONG_MIN/LLONG_MAX + set errno) rather than the incorrect "falls back to 0" claim. No code change — the consumer is diagnostic-only. - test_dispatch_cache_file timing assertion under generic keccak. Pre-fix, the test unconditionally required `keccak_simd_ns > 0`, but the keccak microbench only runs when SIMD is active. Hosts/builds with generic keccak (no AVX2/NEON/SVE2 or `-DAMA_ENABLE_SIMD=OFF`) legitimately write 0 there and the assertion would spuriously fail. Now branches on `ama_get_dispatch_info()->sha3`: requires `>0` when SIMD is wired, requires `==0` when generic. Tight in both directions. CI hygiene - `.github/workflows/ci.yml::Constant-Time Verification (Smoke Test)` step now uses the same best-effort `nice -n -10 true` probe pattern as the five `dudect.yml` jobs — silently drops the prefix when the runner lacks CAP_SYS_NICE (GHA hosted runners) so the `nice: cannot set niceness: Permission denied` warning stops appearing in CI logs. Test coverage - `tests/c/test_dispatch_cache_file.c` extended with a sanitizer rejection contract: fork-per-bad-path (dispatch_init is `pthread_once`-protected and unrepeatable in-process) → set the bad env value → run `ama_dispatch_init()` → assert NO file written at the resolved-traversal target. Covers `..`-segment + trailing-`..` + empty paths. Documentation - CHANGELOG.md v3.2.0 ### Hardened subsection gained a new sub-section "PR #326 follow-up corrections" enumerating each alert with its triggering review/CodeQL ID and the corrective action taken. * v3.2.0 scope addition: native_hmac_sha256 + native_hmac_sha256_2 Python bindings (FIPS 198-1 inventory close-out) Resolves the v3.2.0 inventory gap where the ACVP-validated `ama_hmac_sha256` C symbol (150/150 NIST CAVP vectors per docs/compliance/ACVP_SELF_ATTESTATION.md, exported from libama_cryptography.so since v3.1.0) was never wrapped at the Python layer — forcing downstream consumers needing HMAC-SHA-256 (JWT HS256 per RFC 7518 §3.2, TLS PRFs, future HKDF-SHA-256) to either fall back to stdlib `hmac.new(..., 'sha256')` (violating INVARIANT-1 at the consumer) or maintain a parallel ctypes shim against the same C symbol (bypassing INVARIANT-7 Python-layer enforcement, fragile across AMA releases). ama_cryptography/pqc_backends.py: - `_setup_hmac_sha256_ctypes(lib)` mirrors the existing SHA-512 setup pattern; binds both `ama_hmac_sha256` (5-arg one-shot) and `ama_hmac_sha256_2` (7-arg two-segment) with `restype = None` matching the C `void` return signature documented in src/c/ama_hmac_sha256.h. - `_HMAC_SHA256_NATIVE_AVAILABLE` flag + module-init wiring at the existing `_HMAC_SHA512_NATIVE_AVAILABLE = _setup_...` block. - `native_hmac_sha256(key, msg) -> bytes` — canonical one-shot signer, 32-byte output, raises RuntimeError when the symbol wasn't bound (older AMA installs). - `native_hmac_sha256_2(key, msg1, msg2) -> bytes` — two-segment variant exposing the existing C entry point, byte-identical to `native_hmac_sha256(key, msg1 + msg2)`. Specifically shaped for JWT signing input (b64(header) || '.' || b64(payload)) so the caller doesn't materialise the concat in Python. tests/test_pqc_backends_coverage.py::TestHMACFunctions: 6 new tests pinning the binding: - test_native_hmac_sha256_shape — 32-byte output contract. - test_native_hmac_sha256_rfc4231 — RFC 4231 §4.2 KAT (test case 1, basic). - test_native_hmac_sha256_rfc4231_long_key — RFC 4231 §4.7 KAT (test case 6, oversized key — exercises the RFC 2104 §2 internal-hash path so callers do NOT need to pre-hash). - test_native_hmac_sha256_matches_stdlib — byte-equivalence to `hmac.new(key, msg, hashlib.sha256).digest()` across boundary cases (empty, key-eq-block-1, key-eq-block, key-eq-block+1, oversized) so consumers migrating from stdlib see no wire-byte change in JWT / TLS PRF / similar outputs. - test_native_hmac_sha256_2_equivalent_to_concat — two-segment variant byte-identical to materialised concat. - test_native_hmac_sha256_deterministic — PRF invariant. All 2235 Python tests pass (+6 new); 50/50 ctest cases pass; dudect Overall PASS unchanged. Downstream consumer note: this unblocks omni-mercury-engine's JWT HS256 close-out without a Mercury-side ctypes shim — Mercury imports `native_hmac_sha256` (or `native_hmac_sha256_2` for the header.payload signing form) the same way it already imports `native_hmac_sha512`. Single binding, owned by AMA, INVARIANT-7 preserved end-to-end. * PR #326 follow-up: lint+black formatting on new HMAC-SHA-256 tests, SBOM regen to v3.2.0 Fixes the two CI gates that turned red on the v3.2.0 HMAC-SHA-256 binding push (40a933c) without touching the binding logic itself: - Lint and Format Check / Code Quality Checks: * `tests/test_pqc_backends_coverage.py` — ruff I001 (import block un-sorted) on the stdlib-equivalence test (`import hashlib` moved above `import hmac` per ruff isort). Black collapsed the RFC 4231 KAT hex literals from two-line implicit concat to single-line literals — semantically identical, lint-clean. No test behaviour changes; the 6 KAT/equivalence/2-segment tests still pass byte-identically. - SBOM Generation (CycloneDX) / docs/compliance/sbom-c-library.json: * `tools/generate_sbom.py --check` flagged drift because the committed SBOM still carried `version: 3.1.0` across the eleven AMA C components. Regenerated from `pyproject.toml` (the single source of truth per audit Issue 2 close-out) so the SBOM now reads 3.2.0 across the board. Verified by re-running `--check`: "OK: SBOM matches pyproject.toml version '3.2.0'". Re-verified locally: - ruff check . → All checks passed - black --check --diff . → 132 files unchanged - mypy --strict ama_cryptography/ tests/ → Success: no issues found - bandit -r ama_cryptography/ Medium+ → 0 findings - pytest tests/test_pqc_backends_coverage.py -k hmac_sha256 → 6 passed - python tools/generate_sbom.py --check → OK - python tools/check_version_consistency.py → all 8 anchors at 3.2.0 The remaining macOS-only CI failures (`C Library (macos-latest, clang)`, `Python {3.9..3.13} on macos-latest`) reproduced on `40a933c` BEFORE this commit and are unrelated to either the HMAC binding or these formatting/SBOM fixes — those lanes were failing on the prior commit (13f7a5f) too. Tracking separately as a macOS runner / toolchain issue, not part of the v3.2.0 release-line scope. * PR #326 follow-up: remove unused `safety` dev-dep + transitive CVE chain (close pip-audit failure) Resolves the `Security Audit`, `Python Security Audit`, and `SBOM Generation (CycloneDX)` CI failures on PR #326 by removing the root cause rather than papering over it with `--ignore-vuln` suppressions. Root cause ---------- `pip-audit --strict --requirement requirements-lock.txt` (run in both .github/workflows/ci-build-test.yml::security and .github/workflows/security.yml::security-audit) flagged two CVEs with no upstream fix versions: joblib 1.5.3 PYSEC-2024-277 (NumpyArrayWrapper deserialization; supplier-disputed, only used in trusted-content caching) nltk 3.9.4 PYSEC-2026-97 (nltk.util.filestring arbitrary file read) Both packages were pulled in transitively by `safety` (a security scanner declared in pyproject.toml::[project.optional-dependencies].dev since v2.x but **never invoked by any CI workflow** — verified via `grep -rn safety .github/ tools/ Makefile`). Vulnerability scanning was already 100% covered by `pip-audit --strict` in the two audit workflows above; `safety` was redundant tooling carrying a vulnerable sub-tree that landed in the lock file. Surgical fix (no debt-for-debt trade) ------------------------------------- - pyproject.toml: removed `"safety>=2.3.0"` from the dev extras. Documented inline why it's gone + why pip-audit is the sole vulnerability-scanning tool going forward. - requirements-lock.txt: regenerated from a fresh `pip install "ama-cryptography[dev]"` resolve. Deleted the safety chain (safety, safety-schemas, nltk implicit, joblib, dparse, ruamel.yaml, tenacity, tomlkit, typer, plus the Authlib/pydantic/httpx/httpcore/h11/anyio sub-transitives). Header comment documents the v3.2.0 close-out. - CHANGELOG.md: ### Hardened sub-section "PR #326 follow-up: vulnerable transitive `safety` chain removed" enumerates the two CVEs, the supplier-dispute / unreachable-call analyses, and the reasoning for removal over suppression. Verification ------------ - Clean venv + `pip install -r requirements-lock.txt`: OK - pip-audit --strict --requirement requirements-lock.txt: OK "No known vulnerabilities found" - ruff check .: PASS - black --check .: PASS (132 files unchanged) - mypy --strict ama_cryptography/ tests/: PASS (102 files) - bandit -r ama_cryptography Medium+: 0 findings - pytest tests/ -q --ignore tests/c -k "not slow": 2248 passed, 11 skipped - python tools/check_version_consistency.py: all 8 anchors at 3.2.0 - python tools/generate_sbom.py --check: OK (v3.2.0) INVARIANT-1 (zero-runtime-dep posture) preserved — the library still has dependencies=[] in pyproject.toml. INVARIANT-14 (CVE-ignore-list hygiene) preserved without adding any entries: the chain is deleted from inventory, not annotated as ignored. INVARIANT-11 (SBOM artefact) regenerated by the prior `8eb6f22` commit; this commit's lock-file changes do not affect the C-library SBOM (which lists AMA C components, not Python deps). * PR #326 follow-up: sanitizer rejection test rebuilt as direct unit test (Copilot review r3275565655) Resolves the new Copilot review alert flagging that the prior fork-based sanitizer rejection probe in `tests/c/test_dispatch_cache_file.c` did not actually exercise the contract it claimed to test. Root cause ---------- Two architectural problems compounded: 1. Linux `fork()` inherits the parent's `pthread_once` state, so the child saw the dispatch table as "already initialised" and never re-entered `dispatch_init_internal()` — the sanitizer was never called on the bad env value. The cache code path fired exactly zero times across all four child runs. 2. The hardcoded probe `/tmp/etc/ama_evil` lives in a directory that by default doesn't exist on Linux, so even a hypothetical sanitizer bypass with the cache code path running would still fail to create the probe (ENOENT) for an unrelated reason. Net effect: a green test that wasn't validating anything. Surgical fix ------------ - src/c/dispatch/ama_dispatch.c: new test-only export `ama_test_dispatch_cache_path_sanitize(path)` under `#ifdef AMA_TESTING_MODE` (mirrors the existing ama_test_force_*_scalar pattern). MSVC stub returns NULL since the cache code path is compiled out there. - tests/c/test_dispatch_cache_file.c: replaced the fork+probe block with a direct unit-test table enumerating 17 classes + a dynamically-built oversized 4001-byte input: REJECT classes (10): embedded `..`, leading `..`, trailing `..`, `..` mid-segment, empty, newline injection, CR, tab, DEL (0x7F), low control (0x01). ACCEPT classes (7): simple absolute, relative, subdir, single dot in name, multi-dot, high-bit UTF-8, parens+dashes. OVERSIZED: 4001-byte input (exceeds the 4000-byte limit reserved for the `.tmp.<pid>` suffix in dispatch_cache_save). Accept cases additionally assert pointer identity — sanitizer is contract-bound to NOT allocate or mutate, since the cache code path passes the returned pointer straight to fopen/open. - Removed the now-unused `#include <sys/wait.h>` from the test. Sanity-checked end-to-end: $ sed -i 's|if (strstr(path,.*||' src/c/dispatch/ama_dispatch.c $ cmake --build build && ctest -R test_dispatch_cache_file ... FAIL: dispatch_cache_path_sanitize(case='embedded `..`', ...) returned ACCEPT; expected REJECT FAIL: dispatch_cache_path_sanitize(case='leading `..`', ...) returned ACCEPT; expected REJECT FAIL: dispatch_cache_path_sanitize(case='trailing `..`', ...) returned ACCEPT; expected REJECT FAIL: dispatch_cache_path_sanitize(case='`..` mid-segment', ...) returned ACCEPT; expected REJECT 0% tests passed Then restored — test passes cleanly with the real sanitizer in place. Verification ------------ - ctest -j: 50/50 passed - ruff check .: PASS - black --check .: PASS (132 files unchanged) - Sanity-bypass round-trip: catches 4 of 10 reject-class violations (the `..` family) as expected; control-char / empty cases would catch the remaining rejection branches if those were bypassed. * PR #326 follow-up: every-Python-lane CI close-out + CodeQL realpath sanitizer + macOS-clang issetugid + CI gate hygiene Closes five real (not "stale") issues blocking PR #326's CI ship-readiness. Each is root-caused and fixed in code; no annotation-driven mitigations. 1. EVERY PYTHON LANE on every OS (ubuntu / macos / windows × 3.9..3.13) was red with `ERROR at setup of TestAESGCMInterop.test_native_encrypt_pyca_decrypt: CI FAILURE: Native AES-256-GCM library not available`. Root cause: `tests/conftest.py::pytest_runtest_makereport` iterated every `skipif` marker on the failing item and triggered on the first whose `reason` text matched a backend keyword (`native`, `aes`, ...) without checking whether THAT marker's condition actually caused the skip. `tests/test_aes_gcm_native.py ::TestAESGCMInterop` carries both `@skip_no_native` and `@skip_no_pyca` (it cross-checks the native AES-GCM kernel against PyCA cryptography), and CI's `pip install -e ".[dev]"` doesn't include PyCA (that's under the `[legacy]` extra), so: - skipped legitimately via `@skip_no_pyca` (PyCA not installed) - was reclassified as a backend failure because the hook iterated the sibling `@skip_no_native` marker (reason contains "native") and ignored that its condition `not NATIVE_AVAILABLE` was False (native backend WAS present — every C lane was green). Fix: hook now re-checks each backend-related skipif's condition before treating it as the cause of the skip. Backend marker with condition False is no longer mistaken for the trigger; legitimate PyCA skip stays a skip. The hook's load-bearing purpose (failing CI loudly when a backend really IS missing) is preserved — a backend marker with condition True still flips skip → setup-phase failure with the same diagnostic text. `tests/test_conftest_backend_skip_scoping.py` adds 6 regression tests (3 unit on `_is_backend_skip`, 3 pytester-driven subprocess tests on the actual hook): dual-skipif PyCA-trigger stays a skip; single backend-skipif True flips to ERROR with `CI FAILURE: ...` text; no `AMA_CI_REQUIRE_BACKENDS=1` keeps any backend skip a skip. 2. CodeQL `cpp/path-injection` (#535 / #537) genuinely closed via realpath() canonicalisation, not just an in-source predicate. The earlier `dispatch_cache_path_sanitize()` rejected `..`-containing inputs but returned the same getenv-storage pointer to its caller — CodeQL's flow tracker saw the env-var source flow unchanged to `open()` / `fopen()` and kept the alert open against `src/c/dispatch/ama_dispatch.c:1062` and `:1641` even after the v3.2.0 alert close-out. The sanitizer now runs the validated path through a new `dispatch_cache_path_canonicalize()` helper that calls `realpath(3)` (recognised by CodeQL's path-injection sanitizer model) and falls back to `realpath(dirname) + "/" + basename` for the cache-write case where the file does not exist yet. Return value is now a pointer into a function-local static buffer (`AMA_DISPATCH_PATH_MAX` = `PATH_MAX` from `<limits.h>` or 4096 fallback), so call sites pass a canonical, sanitiser-detached path to file I/O — not the env-var pointer. `tests/c/test_dispatch_cache_file.c` accept-case pointer-identity assertion was relaxed (sanitizer now returns canonical buffer, not input pointer); accept inputs narrowed to `/tmp/...` filenames so realpath() can resolve the dirname on every CI lane; a new "realpath probe" case asserts `/tmp/./xyz` and `/tmp/xyz` canonicalise to the same string — forcing the realpath barrier to actually engage rather than silently regressing to identity-return. 3. macOS-clang lane red since `58e7a2d`: root cause = `_POSIX_C_SOURCE 200809L` puts Apple libc into strict-POSIX mode, which hides `issetugid()` (Apple gates BSD-lineage helpers in `<unistd.h>` on `!defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)`). Apple Clang's default-on `-Werror=implicit-function-declaration` then fails the build at the `dispatch_cache_env_is_safe()` call site that exists specifically to gate setuid / setgid / tainted-exec contexts away from the env-var-controlled cache file path. PR #323 (the merge-base for #326) was green on `C Library (macos-latest, clang)` — the failure was introduced by `58e7a2d`'s dispatch cache scope, not a pre-existing weakness. Fix: define `_DARWIN_C_SOURCE` on `__APPLE__` to re-expose the BSD surface without removing the POSIX baseline (Apple's headers accept both defines simultaneously). No-op on Linux glibc / musl / *BSD. Also defined `_DEFAULT_SOURCE` so glibc's `<stdlib.h>` exposes `realpath` (gated on `__USE_MISC || __USE_XOPEN_EXTENDED`, neither implied by `_POSIX_C_SOURCE 200809L` on Ubuntu 24.04 glibc 2.39). 4. clang-tidy CI gate's pipe-tee silently swallowed clang-tidy's non- zero exit (the runner loop's `if ! clang-tidy ... | tee ...` checked tee's status, not clang-tidy's, without `set -o pipefail`) — which is how the pre-existing `cert-err34-c` atoi() findings in `dispatch_cache_load()` (introduced in `58e7a2d`) were silently passing CI under the documented "FAIL-CLOSED" policy. Added `set -o pipefail` + explicit `shell: bash` to the `.github/workflows/static-analysis.yml::clang-tidy::Run` step so the gate is now genuinely fail-closed. Replaced the 6 atoi() calls in `dispatch_cache_load()` with a strtol() + endpoint/errno block that refuses anything but the literal "0" / "1" cache file value (CERT-ERR34-C); partial digits / trailing junk / overflow all map to flag = 0, matching the surrounding "no measurement" fallback. 5. `.github/workflows/ci-build-test.yml::c-library::Build` now re-emits per-command verbose output on failure (parallel happy path, serial `--verbose --clean-first -j1` fall-back that `tee`s to a log file, first `error:` block grep-extracted into a GitHub Actions log group). Original non-zero exit preserved. Future opaque "Process completed with exit code 2" failures across any of the four `C Library (os, compiler)` cells now surface the actual failing compile command and diagnostic directly in the job log — no local repro needed, no guessing. Verification (local, x86-64 Linux): ctest -j → 50/50 pass pytest tests/ --ignore tests/c → 2250 passed, 16 legit skipped (PyCA / SoftHSM), 7 subtests ruff check + black --check + mypy --strict + bandit Medium+ → all clean clang-tidy on every src/c + include file (no vendor) → 0 errors dudect_crypto (5000 iter) → Overall PASS tools/check_version_consistency.py → 3.2.0 across all 8 anchors tools/check_suppression_hygiene.py → INVARIANT-13 clean https://claude.ai/code/session_01VmC1Kw5NDAuiJvoj1jk8NS * PR #326 follow-up: close clang-lane regression (hex escape) + diagnostic ctest verbose Two fixes shipping together because they're coupled — the hex-escape fix unblocks the strict-warnings (clang) lane, and the ctest diagnostic addition surfaces the remaining 8-test failures on the clang lanes that I introduced and can't otherwise see without local log access. 1. `tests/c/test_dispatch_cache_file.c::cases[]` high-bit UTF-8 entry. My PR #326 follow-up commit `0776fa1` updated the accept-class sanitizer test to use `/tmp/<utf8-bytes>cache` (no `/` between the utf8 and `cache`) so the dirname is `/tmp/` for realpath() to resolve. The exact byte sequence `\xe2\x9c\x94cache` triggers clang's `error: hex escape sequence out of range`: clang's hex-escape lexer reads digits greedily until a non-hex char, so `\x94c` is parsed as a 3-digit hex literal `0x94c` (= 2380) which exceeds the `unsigned char` range. GCC accepts this with a warning; clang rejects under `-Wall` (`-Werror=invalid-hex-escape` default-on for some clang configs). Fix is the C11 §5.1.1.2 adjacent-string-literal trick: split the literal as `"/tmp/\xe2\x9c\x94" "cache"` — the compiler's translation-phase-6 concatenation makes the on-wire byte sequence identical while the `\x94` escape now terminates at the closing quote rather than absorbing the next character. Verified locally with `-Wall -Wextra -Wpedantic -Wshadow -Wformat=2 -Wconversion -Wno-sign-conversion -DCMAKE_C_COMPILER=clang` — build succeeds, ctest runs to 50/50 pass. 2. `.github/workflows/ci-build-test.yml::c-library::Test`: replace the bare `cd build && ctest --output-on-failure` with the `--verbose` form. Without `--verbose`, ctest hides the stdout / stderr of any test whose failure mode was an `exit !=0` rather than a CTest-driven assertion — which is precisely how the `C Library (ubuntu-latest, clang)` and `C Library (macos-latest, clang)` lanes were reporting "Process completed with exit code 8" without revealing WHICH 8 tests failed. `--verbose` prints the per-test stdout/stderr in the CI log so any future regression self-diagnoses. https://claude.ai/code/session_01VmC1Kw5NDAuiJvoj1jk8NS * chore(gitignore): ignore build-clang/ local-reproduction directory Mirrors the existing per-build-config `build-asan/`, `build-strict/`, `build-tidy/` ignore entries. Used locally to reproduce the `C Library (ubuntu-latest, clang)` / `C Library (macos-latest, clang)` CI lanes when triaging PR #326's clang-specific regressions — see neighbouring `build-tidy/` (clang-tidy compile_commands.json scratch). https://claude.ai/code/session_01VmC1Kw5NDAuiJvoj1jk8NS * PR #326 follow-up: Windows Python lanes (AMA_API export) + Copilot review r3276471155 / r3276471202 Three coupled fixes — all root-caused, no annotation-driven mitigations. 1. WINDOWS PYTHON LANES. Root cause = the v3.2.0 commit `40a933c` added `ama_hmac_sha256` / `ama_hmac_sha256_2` C symbols in `src/c/ama_hmac_sha256.h` WITHOUT `AMA_API`. On Linux/macOS, default symbol visibility exposes the function from the .so / .dylib so `lib.ama_hmac_sha256` binds at module import time. On Windows MSVC building a shared library (`AMA_BUILDING_SHARED` defined), `AMA_API = __declspec(dllexport)` — without the attribute, the symbol is hidden from `libama_cryptography.dll`'s export table. The Python ctypes binding's `lib.ama_hmac_sha256` lookup then raises `AttributeError`, the `_setup_hmac_sha256_ctypes()` setup catches it and sets `_HMAC_SHA256_NATIVE_AVAILABLE = False`, and the six `TestHMACFunctions::test_native_hmac_sha256_*` cases (decorated only with `@skip_no_native` which checks `_native_lib is not None` — not the per-symbol flag) then raise `RuntimeError("HMAC-SHA-256 native backend not available...")` from `native_hmac_sha256()`. This broke EVERY `Python {3.9..3.13} on windows-latest` lane (`ci-build-test.yml::python-package`) AND every parallel `Test windows-latest / Python ...` lane (`ci.yml::test`) since `40a933c` introduced the binding. PR #323 (the merge-base for #326) was green on Windows Python — this is a PR #326 regression, not a pre-existing weakness. Fix: add `AMA_API` to both declarations in the header (matches existing pattern on `ama_hmac_sha3_256` / `ama_hmac_sha512` in `include/ama_cryptography.h`) + `#include "ama_cryptography.h"` so the macro is in scope. No-op on GCC/Clang; load-bearing on MSVC. 2. COPILOT REVIEW r3276471155 (real bug in `dispatch_bench_keccak_x4()` baseline). Slot 2 bench was passing `dispatch_table.keccak_f1600` as the 4× single-state baseline, but at that point in `dispatch_init_internal()` slot 1's verdict has been COMPUTED (`v.keccak_regressed`) but NOT yet APPLIED (the `dispatch_table.keccak_f1600 = ama_keccak_f1600_generic` revert lives below the per-slot bench block). If slot 1 IS regressed, the slot 2 bench was using its regressed SIMD pointer as the "scalar" baseline — inflating the baseline timing past what the runtime actually does (the runtime would resolve to `ama_keccak_f1600_x4_generic` ≈ 4× generic), making the x4 SIMD look faster than reality and potentially masking an x4 regression. Fix: pass `ama_keccak_f1600_generic` directly to `dispatch_bench_keccak_x4` as the single-state baseline. Slot 1's verdict is now decoupled from slot 2's comparison, restoring the decoupled-verdict architecture the v3.2.0 close-out commit `58e7a2d` was specifically designed to deliver. 3. COPILOT REVIEW r3276471202 (misleading `include/ama_dispatch.h` cache-doc). The block-comment previously suggested packagers can ship a pre-warmed cache in `/etc` — but `dispatch_cache_save()` creates files with mode 0600 owned by the writing EUID, so a root-owned `/etc/ama-cryptography.cache` would be unreadable by a non-root service (perpetual miss + a verbose-log read-failure line per init) AND the atomic-rename path requires the writer to own the target directory. Replaced the misleading paragraph with `$XDG_CACHE_HOME/ama-cryptography/<file>` as the recommended location plus per-user `install -m 0600 -o $user -g $user` guidance for packagers wishing to ship a pre-warmed cache. Verification (local, x86-64 Linux, clang-18 + gcc-13): ctest -j → 50/50 pass pytest tests/ --ignore tests/c → 2250 passed, 16 legit skipped ruff check + black --check + mypy --strict + bandit Medium+ → all clean clang-tidy on every src/c + include file (no vendor) → 0 errors dudect_crypto (5000 iter) → Overall PASS tools/check_version_consistency.py → 3.2.0 across all 8 anchors https://claude.ai/code/session_01VmC1Kw5NDAuiJvoj1jk8NS * Harden dispatch cache file access with openat Co-Authored-By: Andrew E. A. <steel.sa.llc@gmail.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the CI dudect failure on
main, the three surgical-engineering items called out in the audit, and the Ed25519 functional-completeness gap; then bumps to 3.2.0 and releases the[Unreleased]section.CI failure — fixed
The dudect job failed on
maindue to two harness-side false positives:ama_consttime_memcmpFROST scalar_negate (mid-range)Both underlying primitives are byte-by-byte branchless in source. The leaks were harness setup-asymmetries (extra
rand()+ branchy XOR before the timer onclass_idx==1, and a.rodatavs stack-array cache-line provenance mismatch). Codified the three-rule setup-symmetry discipline inCONSTANT_TIME_VERIFICATION.mdso future lanes can't regress the same way.Surgical executions
Lockstep revert decoupled (
src/c/dispatch/ama_dispatch.c).keccak_f1600_x4is now benched independently against a 4× single-state baseline. An AVX2 single-state regression no longer demotes a working AVX-512 4-way kernel. Verified locally on a qemu host where AVX2 single-state did regress: the AVX2 4-way path stayed wired (772K ns vs 2.4M ns scalar) — pre-fix that 3× speedup would have been silently discarded.NTT auto-tune added.
kyber_ntt/kyber_invntt/dilithium_ntt/dilithium_invntteach bench against newama_*_generic_refscalar references extracted from the inline paths insrc/c/ama_kyber.c/src/c/ama_dilithium.c. Single source of truth: the same scalar helper drives both the production NULL-slot fallback and the microbench.Cross-process cache (
AMA_DISPATCH_CACHE_FILE=<path>, opt-in, declared ininclude/ama_dispatch.h). Writes the per-slot verdict after a successful bench; subsequent processes with a matching CPU-feature fingerprint load it and skip the ~10K-iter Keccak microbench entirely. Atomic tmp+rename, deterministic fingerprint,AMA_DISPATCH_NO_AUTOTUNE=1takes precedence.Functional completeness
ama_keypair_generate(AMA_ALG_ED25519)wired through toama_ed25519_keypair(CSPRNG seed → backend).ama_sign/ama_verifygained matchingAMA_ALG_ED25519arms so the generated keypair is usable end-to-end through the algorithm-agnostic API. INVARIANT-6 scrubbing on every failure path.Release
check_version_consistency.pygreen).[Unreleased]released as[3.2.0] - 2026-05-20.CONSTANT_TIME_VERIFICATION.md: harness setup-symmetry discipline subsection.include/ama_dispatch.h: cache contract documented.Test plan
ctest -j4all 49 tests pass (100% tests passed, 0 tests failed out of 49)test_dudect --measurements 50000overall PASS (previously failing lanes now within ±2σ)test_dudect --measurements 100000overall PASS (matches the failing CI configuration)check_version_consistency.pygreen on all 8 anchorsama_keypair_generate→ama_sign→ama_verify(and tamper rejection)Generated by Claude Code