-
-
Notifications
You must be signed in to change notification settings - Fork 0
884 lines (798 loc) · 38 KB
/
Copy pathci.yml
File metadata and controls
884 lines (798 loc) · 38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
# Copyright (C) 2025-2026 Steel Security Advisors LLC
# SPDX-License-Identifier: Apache-2.0
name: CI - Testing and Code Quality
on:
push:
branches: [ main, develop, 'feature/**', 'fix/**' ]
pull_request:
branches: [ main, develop ]
workflow_dispatch:
permissions:
contents: read
# Collapse overlapping runs on the same ref to avoid runner-queue
# saturation when rapid pushes fire both push- and pull_request-triggered
# workflows in parallel. Cancelling in-flight runs is safe for feature
# branches and PR heads; `main` and scheduled runs are preserved by
# excluding them from ``cancel-in-progress``.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.event_name != 'schedule' }}
jobs:
test:
name: Test ${{ matrix.os }} / Python ${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
include:
# ARM64 (AArch64) runners — GitHub-hosted ubuntu arm64
- os: ubuntu-24.04-arm
python-version: "3.11"
- os: ubuntu-24.04-arm
python-version: "3.13"
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python ${{ matrix.python-version }}
id: setup-python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
continue-on-error: true
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
# First-party retry path: actions/setup-python intermittently fails on
# GitHub-hosted Windows runners during the Python 3.13 install
# (toolcache / cache-restore flake; see PR #306 review). The retry
# is non-fatal on the first miss and only re-runs when needed, so the
# happy path remains a single setup-python call.
- name: Set up Python ${{ matrix.python-version }} (retry without cache)
if: steps.setup-python.outcome == 'failure'
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ matrix.python-version }}
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
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: Install system dependencies (Windows)
if: runner.os == 'Windows'
shell: pwsh
# CMake is pre-installed on GitHub-hosted Windows runners
# (see https://github.com/actions/runner-images/ image manifests),
# so the common case is a no-op. Only install via Chocolatey —
# with retries — if ``cmake`` is genuinely missing, so a transient
# Chocolatey CDN outage cannot break the build. Matches the
# pattern already used in ``ci-build-test.yml``; PR #258 review
# fix for the flaky ``Install system dependencies (Windows)``
# step that was single-attempt ``choco install cmake``.
run: |
if (Get-Command cmake -ErrorAction SilentlyContinue) {
Write-Host "CMake already available: $(cmake --version | Select-Object -First 1)"
} else {
Write-Host "CMake not found; installing via Chocolatey with retries..."
$maxAttempts = 5
for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
Write-Host "Attempt ${attempt}/${maxAttempts}: choco install cmake..."
choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System' -y 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "Successfully installed cmake"
break
}
if ($attempt -lt $maxAttempts) {
$delay = $attempt * 15
Write-Host "Install failed, waiting ${delay}s before retry..."
Start-Sleep -Seconds $delay
}
}
if ($LASTEXITCODE -ne 0) {
Write-Error "All ${maxAttempts} attempts to install cmake via Chocolatey failed"
exit 1
}
Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
refreshenv
}
- name: Build native C library (Linux)
if: runner.os == 'Linux'
run: |
cmake -B build -DAMA_USE_NATIVE_PQC=ON -DCMAKE_BUILD_TYPE=Release -DAMA_BUILD_TESTS=OFF -DAMA_BUILD_EXAMPLES=OFF
cmake --build build -j$(nproc)
- name: Build native C library (Windows)
if: runner.os == 'Windows'
run: |
cmake -B build -DAMA_USE_NATIVE_PQC=ON -DCMAKE_BUILD_TYPE=Release -DAMA_BUILD_TESTS=OFF -DAMA_BUILD_EXAMPLES=OFF
cmake --build build --config Release --parallel
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install "Cython>=3.0.0" "numpy>=1.24.0,<3.0.0"
# [legacy] + [benchmark] supply PyCA cryptography / PyNaCl, and
# pycryptodome backs the AES-GCM differential harness. Without them the
# cross-implementation validation tests (tests/test_differential.py,
# the PyCA interop cases in test_ed25519_native.py / test_aes_gcm_native.py
# / test_hkdf_sha3_256.py) silently SKIP, so a divergence between AMA's
# native C primitives and a reference implementation would go unnoticed.
# Installing them converts those skips into real assertions.
pip install -e ".[dev,legacy,benchmark]" pycryptodome
- name: Verify module integrity digest is current
shell: bash
run: |
python -c "
from ama_cryptography._self_test import verify_module_integrity
ok, detail = verify_module_integrity()
if not ok:
raise SystemExit(
f'FATAL: Module integrity digest is stale or tampered: {detail}\n'
'Run: python -m ama_cryptography.integrity --update'
)
print(f'Module integrity: OK ({detail})')
"
- name: Verify crypto backends are available
shell: bash
run: |
python -c "
from ama_cryptography.pqc_backends import (
DILITHIUM_AVAILABLE, KYBER_AVAILABLE, SPHINCS_AVAILABLE,
_ED25519_NATIVE_AVAILABLE, _AES_GCM_NATIVE_AVAILABLE,
)
backends = {
'Dilithium': DILITHIUM_AVAILABLE,
'Kyber': KYBER_AVAILABLE,
'SPHINCS+': SPHINCS_AVAILABLE,
'AES-GCM native': _AES_GCM_NATIVE_AVAILABLE,
'Ed25519 native': _ED25519_NATIVE_AVAILABLE,
}
missing = [name for name, avail in backends.items() if not avail]
for name, avail in backends.items():
status = 'OK' if avail else 'MISSING'
print(f' {name}: {status}')
if missing:
raise SystemExit(
f'FATAL: Crypto backends not available in CI: {missing}. '
'The C library must be built before running tests.'
)
print('All crypto backends verified.')
"
- name: Verify cross-implementation reference libraries are importable
shell: bash
# The differential / interop suites validate AMA's native C primitives
# against independent reference implementations. They are written to SKIP
# when a reference library is absent, which means a missing install would
# quietly delete that coverage instead of failing. Assert the imports here
# so the skip path can never silently return in CI.
run: |
python -c "
import importlib
refs = {
'cryptography': 'PyCA interop (Ed25519 / AES-GCM / HKDF cross-checks)',
'nacl': 'PyNaCl differential validation',
'Crypto': 'pycryptodome AES-GCM differential validation',
}
missing = []
for mod, why in refs.items():
try:
importlib.import_module(mod)
print(f' {mod}: OK ({why})')
except ImportError:
missing.append(f'{mod} ({why})')
print(f' {mod}: MISSING ({why})')
if missing:
raise SystemExit(
'FATAL: cross-implementation reference libraries missing in CI: '
f'{missing}. Interop tests would SKIP and silently drop coverage.'
)
print('All cross-implementation reference libraries verified.')
"
- name: Run pytest test suite
env:
AMA_CI_REQUIRE_BACKENDS: "1"
run: |
python -m pytest tests/ -v --tb=short --no-cov
- name: "Wycheproof corpus (vendored, offline, fail-closed)"
# Google's Project Wycheproof is a corpus of the cases that *break*
# implementations. Running it against this library for the first time
# found a real Ed25519 malleability defect (RFC 8032 §5.1.7) in under a
# minute — and then a second divergence in X25519 non-canonical
# u-coordinate handling. Neither had any standing gate; this is it.
#
# The corpus is vendored under wycheproof_vectors/ and pinned by
# manifest.json (upstream commit, per-file SHA-256, per-file vector
# count), so nothing is fetched at test time and a swapped vector file
# fails before a single vector runs. Every one of the 2,733 vectors
# lands in a named bucket with an asserted count — there is no silent
# skip, and an unrunnable vector is a red build with a reason.
run: |
python wycheproof_vectors/run_wycheproof.py
- name: Run demonstration
env:
PYTHONUTF8: "1"
run: |
python -m ama_cryptography
- name: Verify demonstration output
shell: bash
run: |
python -c "
import os
import subprocess
import sys
env = os.environ.copy()
env['PYTHONUTF8'] = '1'
result = subprocess.run([sys.executable, '-m', 'ama_cryptography'],
capture_output=True, text=True, timeout=60,
encoding='utf-8', errors='replace', env=env)
if result.returncode != 0:
print('ERROR: Demonstration failed')
print(result.stderr)
sys.exit(1)
output = result.stdout
# Verify critical outputs
checks = [
('AMA Cryptography', 'Title present'),
('Generating key management system', 'Key generation'),
('Creating Omni-Code cryptographic package', 'Package creation'),
('Signing package', 'Signing process'),
('Verifying cryptographic package', 'Verification'),
('ALL VERIFICATIONS PASSED', 'Success confirmation')
]
failed = []
for check, desc in checks:
if check not in output:
failed.append(f'{desc}: \"{check}\" not found')
if failed:
print('ERROR: Output validation failed:')
for fail in failed:
print(f' - {fail}')
sys.exit(1)
print('All verification checks passed')
"
code-quality:
name: Code Quality Checks
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.11"
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
# Pin the exact linter versions from requirements-lock.txt so the gate
# matches the pinned dev toolchain and pre-commit. An unpinned ruff
# (previously ``ruff>=0.4``) lets a routine ruff release start failing
# an unrelated PR; mypy floated on 1.x while the [dev] extra and
# [tool.mypy] config target 2.x, so local and CI could disagree.
# types-PyYAML keeps tools/check_workflow_commands.py and its tests
# inside `mypy --strict` rather than behind an ignore_missing_imports
# override; tests/ imports the tool, so mypy follows into it.
pip install "black==26.5.1" "ruff==0.15.20" "mypy==2.1.0" \
"PyYAML==6.0.3" "types-PyYAML==6.0.12.20260724"
- name: Lint with ruff
run: |
ruff check .
- name: Check code formatting with Black
run: |
black --check --diff .
- name: Type checking with MyPy (--strict)
continue-on-error: false
run: |
mypy --strict ama_cryptography/ tests/
- name: "INVARIANT-13: Suppression hygiene check"
run: |
python tools/check_suppression_hygiene.py
- name: "Uniform license headers (SPDX)"
# The tree carried five different header shapes at once — a two-line
# Apache note, the full thirteen-line boilerplate block, a bare SPDX
# tag on four files, a one-off "Apache License 2.0" spelling, and the
# C block-comment forms of each. No machine license scanner could
# read that reliably and a new file could pick any shape. This gate
# pins the canonical two-line header with the *registered* SPDX
# identifier (`Apache-2.0`; `Apache 2.0` is not a valid identifier).
# Detection and normalization are both pinned by tests/test_headers.py,
# so the gate cannot silently degrade into a no-op.
run: |
python tools/check_headers.py --check
docs-build:
# Run sphinx-build with -W on every PR / push so any docstring-format
# regression (e.g. malformed Raises: blocks, dangling indent) is caught
# at PR time, not on the post-merge auto-docs job whose failure does
# not block the merge that introduced it.
name: Sphinx Docs Build (Python API)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.11"
cache: 'pip'
- name: Install documentation dependencies
run: |
python -m pip install --upgrade pip
python -m pip install sphinx sphinx_rtd_theme sphinx-autodoc-typehints
- name: Build Sphinx HTML — warnings are errors
env:
# INVARIANT-7 docs-build gate. Lift the native-backend import
# guards for autodoc only; ``--keep-going`` reports every warning
# in the run while ``-W`` makes the job fail on any of them.
AMA_SPHINX_BUILD: '1'
run: sphinx-build -W --keep-going -b html docs docs/_build/html
security-checks:
name: Security Checks
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
# PyYAML is pinned to the requirements-lock.txt version because
# tools/check_workflow_commands.py (INVARIANT-25) runs in this job and
# parses the workflow files structurally.
pip install pip-audit bandit "PyYAML==6.0.3"
- name: Audit dependencies (strict, lock-file scoped)
# Scope the audit to requirements-lock.txt — this project's pinned
# dependency set — rather than auditing the whole runner environment.
# A bare `pip-audit` reports CVEs in packages AMA does not ship (pip,
# pyjwt, urllib3 and whatever else the GitHub runner image preinstalls),
# so the gate turned red for reasons unrelated to this repository and
# unfixable from it. Lock-file scoping makes the result reproducible
# across reruns and runner-image rolls, and matches the contract already
# used by security.yml. `--strict` still fails the job on any
# vulnerability found in our own pinned set.
run: pip-audit --strict --desc --requirement requirements-lock.txt
- name: Security linting with Bandit
run: |
bandit -r ama_cryptography/ -l -f json -o bandit-report.json
bandit -r ama_cryptography/ -l
- name: Upload Bandit report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success()
with:
name: bandit-security-report
path: bandit-report.json
- name: Semgrep security scan
continue-on-error: false
run: |
python -m pip install semgrep==1.74.0
semgrep --config .semgrep.yml ama_cryptography/ --json -o semgrep-report.json
- name: Upload Semgrep report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success()
with:
name: semgrep-security-report
path: semgrep-report.json
- name: "INVARIANT-24: Verify pinned action SHAs resolve upstream"
# A SHA-pinned action is only a supply-chain control if the SHA is real.
# release.yml carried a pypa/cibuildwheel pin whose commit existed
# nowhere upstream, so EVERY wheel job aborted with "unable to find
# version" — which is why the v3.2.0 and v3.3.0 releases shipped with
# zero binary artefacts. Nothing caught it because release.yml only
# runs on a tag push. Checking here means a bad pin fails on the PR
# that introduces it, not on release day.
run: |
python tools/check_action_pins.py --strict
- name: "INVARIANT-25: Verify workflow runner labels and command strings"
# release.yml runs only on a tag push, so anything wrong inside it stays
# invisible until release day. Three defects shipped that way, each on
# its own enough to produce a release with zero binary artefacts: a
# retired `macos-13` runner label (the job queued until timeout), a
# `python -c` payload broken by YAML folding (every wheel built, then
# failed with IndentationError), and POSIX single-quoting handed to
# cmd.exe (every Windows wheel died on `Invalid requirement: "'cmake"`).
# All three are decidable without running anything, so they are decided
# here — on the pull request that introduces them. Both directions are
# pinned by tests/test_workflow_command_checks.py.
run: |
python tools/check_workflow_commands.py
- name: "INVARIANT-31: Verify every pull-request job is reachable from its gate"
# Branch protection requires the aggregating gate context of each
# workflow, not the individual job names. A job missing from its gate's
# `needs:` therefore still runs, still shows its own red X on the pull
# request, and still cannot block the merge — "all required checks
# passed" stays true next to a visibly failing job.
#
# `c-library-no-native-pqc` was exactly that: the guard for the
# AMA_USE_NATIVE_PQC=OFF build, absent from ci-build-test.yml's gate
# while commit f3dd0c2 had to repair that very configuration after it
# broke undetected. Every gate comment in this repository asserts that
# each job in the workflow must be green; this check is what makes that
# true. Both directions are pinned by tests/test_gate_coverage.py.
run: |
python tools/check_gate_coverage.py
- name: "INVARIANT-32: Verify documented install extras are declared"
# pip does not fail on an extra a distribution does not provide — it
# warns, installs WITHOUT it, and exits 0. A stale name in an install
# instruction therefore yields an incomplete install and a success
# message, surfacing much later as an ImportError from a subsystem the
# reader believes they enabled.
#
# wiki/Installation.md — published to the public wiki by wiki-sync.yml —
# shipped `pip install -e ".[secure-memory]"`, described as "libsodium
# secure memory bindings", in its own "Everything at once" line. No such
# extra ever existed, secure_memory is stdlib-only, and INVARIANT-1
# forbids libsodium outright. Both directions are pinned by
# tests/test_documented_extras.py.
run: |
python tools/check_documented_extras.py
- name: "INVARIANT-33: Verify every fuzz harness is registered everywhere"
# A fuzz harness is registered in three independent lists: the CMake
# target lists, the fuzzing.yml job matrix, and oss-fuzz/build.sh.
# Nothing tied them together, and they had drifted: fuzz_agent_binding
# was added to CMake and to the CI matrix when the agent-binding layer
# landed, and never to build.sh — so OSS-Fuzz never built it. build.sh
# skips a missing target with a warning and exits 0, which is why the
# omission stayed invisible.
#
# A harness nobody runs is indistinguishable from one that finds
# nothing. Both directions are pinned by
# tests/test_fuzz_target_registration.py.
run: |
python tools/check_fuzz_target_registration.py
- name: "INVARIANT-23: Secret scan (in-house)"
# tools/check_secrets.py is written in house rather than adopting a
# third-party scanner — see the module docstring for why a generic
# entropy scanner is the wrong tool for a repository whose tracked
# content is largely published high-entropy test vectors. Detection
# and non-detection are pinned by tests/test_secret_scanner.py, so this
# gate cannot silently degrade into a no-op.
run: |
python tools/check_secrets.py
- name: Enforce safe os.fdopen usage (fd-ownership, AST-verified)
# Replaces a grep-plus-filename-allowlist check. That construction could
# not tell a correctly guarded call from a leaking one — it only asked
# whether the file was on a list, so it was satisfied by editing the list
# — and it had already rotted: the allowlist named `key_storage.py`, a
# module that does not exist in this package, while omitting the module
# that actually performs the call. tools/check_fdopen_safety.py instead
# parses the AST and verifies the property that prevents the leak: every
# os.fdopen call sits in a try whose handlers (or finally) can close the
# raw descriptor when the hand-off fails. No allowlist to maintain, and
# it applies to every tracked module rather than three named ones.
# Both directions are pinned by tests/test_fdopen_safety.py.
run: |
python tools/check_fdopen_safety.py
benchmark-regression:
name: Benchmark Regression Detection (${{ matrix.runner_cpu_class }})
# x86_64 entry uses ubuntu-latest; arm64 entry uses ubuntu-24.04-arm
# (GitHub-hosted Cortex-A78 / Graviton-class AArch64 runner). The
# arm-baseline.json arm entry exercises the NEON paths wired by this
# PR (AES-GCM, ChaCha20, Argon2) end-to-end so any byte-level
# divergence between the NEON kernel and the scalar reference shows
# up as a regression-detector hit, not a silent passing build.
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
runner_cpu_class: x86_64
baseline_path: benchmarks/baseline.json
- os: ubuntu-24.04-arm
runner_cpu_class: aarch64
baseline_path: benchmarks/arm-baseline.json
runs-on: ${{ matrix.os }}
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.11"
cache: 'pip'
- name: Install system 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 util-linux
- name: Capture runner CPU class
id: cpu_class
run: |
# lscpu reports the canonical "Architecture:" field which we
# treat as the source of truth. Pin the value into the runner
# matrix entry so a benchmark JSON produced by ubuntu-24.04-arm
# is unambiguously distinguishable from one produced by
# ubuntu-latest x86_64 (the regression detector keys off this).
ARCH=$(lscpu | awk -F: '/^Architecture:/ {gsub(/^ +/, "", $2); print $2}')
echo "arch=${ARCH}" >> "$GITHUB_OUTPUT"
echo "matrix_class=${{ matrix.runner_cpu_class }}" >> "$GITHUB_OUTPUT"
- name: Build native C library
run: |
# AMA_ENABLE_AVX512=ON enables the in-house AVX-512 4-way Keccak
# kernel on x86-64 hosts (gated on runtime CPUID at dispatch
# init, so a no-op on AArch64). AMA_ENABLE_NATIVE_ARCH=ON adds
# -march=native so the benchmark exercises the host's full
# microarchitecture (AES-NI, BMI2, AVX2/AVX-512, NEON Crypto
# Extensions as appropriate). Without these flags the benchmark
# numbers reflect the lowest-common-denominator generic C path,
# not what users actually run.
cmake -B build \
-DAMA_USE_NATIVE_PQC=ON \
-DCMAKE_BUILD_TYPE=Release \
-DAMA_BUILD_TESTS=OFF \
-DAMA_BUILD_EXAMPLES=OFF \
-DAMA_ENABLE_AVX512=ON \
-DAMA_ENABLE_NATIVE_ARCH=ON
cmake --build build -j$(nproc)
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install "Cython>=3.0.0" "numpy>=1.24.0,<3.0.0"
pip install -e ".[dev]"
- name: Run benchmark regression tests
env:
AMA_RUNNER_CPU_CLASS: ${{ steps.cpu_class.outputs.arch }}
AMA_MATRIX_BASELINE: ${{ matrix.baseline_path }}
run: |
# Pin to a single core (`taskset -c 0`) and raise scheduling
# priority (`nice -n -10`) so the benchmark numbers are not
# contaminated by other tasks on the shared GitHub-hosted
# runner. Mirrors the same harness dudect.yml uses for its
# constant-time measurements at the identical priority — the
# dudect t-statistic and the benchmark medians both depend on
# the same noise floor.
#
# `--baseline "$AMA_MATRIX_BASELINE"` is the per-matrix entry
# baseline path (Copilot review #3249188319): the x86 entry
# compares against benchmarks/baseline.json, the AArch64 entry
# against benchmarks/arm-baseline.json. Without this, both
# entries default to the x86 baseline and the AArch64 job
# reports spurious regressions on every PR.
taskset -c 0 nice -n -10 python benchmarks/benchmark_runner.py \
--verbose \
--baseline "$AMA_MATRIX_BASELINE" \
--require-runner-class "$AMA_RUNNER_CPU_CLASS" \
--require-populated-baseline \
--output benchmarks/benchmark-results.json \
--markdown benchmark-report.md
# Annotate the result JSON with the runner CPU class so the
# arm and x86 artefacts are unambiguously distinguishable.
# The downstream `check_baseline_justification.py` uses this
# tag to compare against the matching baseline.
python - <<'PY'
import json, os, pathlib
path = pathlib.Path("benchmarks/benchmark-results.json")
if path.exists():
data = json.loads(path.read_text())
data.setdefault("metadata", {})["runner_cpu_class"] = (
os.environ.get("AMA_RUNNER_CPU_CLASS", "unknown")
)
data["metadata"]["matrix_baseline_path"] = (
os.environ.get("AMA_MATRIX_BASELINE", "")
)
path.write_text(json.dumps(data, indent=2) + "\n")
PY
- name: Upload benchmark results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success()
with:
name: benchmark-results-${{ matrix.runner_cpu_class }}
path: |
benchmarks/benchmark-results.json
benchmark-report.md
constant-time-check:
name: Constant-Time Verification (Smoke Test)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install build 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: |
mkdir -p build
cd build
cmake ..
make
- name: Build and run dudect harnesses
run: |
cd tools/constant_time
make clean
make all
# Run smoke tests with 50K iterations (faster for CI).
#
# Pin to a single core (`taskset -c 0`) and best-effort
# elevate priority (`nice -n -10`) to suppress CPU-contention
# noise on shared GitHub-hosted runners. Without this, the
# dudect t-statistic at 50K iterations can drift past its
# leakage threshold from background noise alone.
#
# GHA hosted runners lack CAP_SYS_NICE so the negative-nice
# request prints "Permission denied" but still execs the
# binary; probe once up front and drop the prefix when the
# runner refuses (PR #326), so the log stays clean. The
# v3.2.0 dudect setup-symmetry harness fixes made the lanes
# noise-tolerant enough that taskset-only pinning is the
# load-bearing CI gate; the nice prefix is opportunistic on
# privileged self-hosted runners. The full dudect.yml gate
# (path-filtered to src/c/** changes) remains the
# authoritative constant-time check; this smoke test exists
# as a fast pre-gate for every PR regardless of path filter.
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
- name: Upload timing analysis results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success()
with:
name: constant-time-results
path: tools/constant_time/
# ============================================================================
# AVX-512 SHA3 4-way KAT (PR C — 2026-04)
#
# Best-effort gate: GitHub-hosted ubuntu-latest currently rotates among
# Cascade Lake / Ice Lake / Sapphire Rapids host CPUs, so AVX-512 hits
# only some fraction of runs. We probe /proc/cpuinfo for the avx512f
# feature flag and skip honestly when it's missing — the build/test
# body itself never uses continue-on-error (INVARIANT-2: fail-closed
# CI on the steps that actually run). Local validation ladder for
# pre-merge: SDE (`sde64 -spr -- ./test_sha3_avx512_kat`) plus a
# quarterly bare-metal bench post-merge.
# ============================================================================
test-avx512:
name: AVX-512 SHA3 4-way KAT
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Probe /proc/cpuinfo for AVX-512F + AVX-512VL
id: cpu
run: |
if grep -q '\bavx512f\b' /proc/cpuinfo && grep -q '\bavx512vl\b' /proc/cpuinfo; then
echo "have_avx512=1" >> "$GITHUB_OUTPUT"
echo "::notice::ubuntu-latest runner reports AVX-512F + AVX-512VL — running KAT"
else
echo "have_avx512=0" >> "$GITHUB_OUTPUT"
echo "::notice::ubuntu-latest runner lacks AVX-512F or AVX-512VL — skipping KAT job"
fi
- name: Install build dependencies
if: steps.cpu.outputs.have_avx512 == '1'
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 cmake build-essential
- name: Configure with AVX-512 enabled
if: steps.cpu.outputs.have_avx512 == '1'
run: cmake -S . -B build -DAMA_ENABLE_AVX512=ON -DAMA_BUILD_TESTS=ON
- name: Build AVX-512 KAT test
if: steps.cpu.outputs.have_avx512 == '1'
run: cmake --build build --target test_sha3_avx512_kat --parallel
- name: Run AVX-512 KAT test (byte-identity vs scalar + AVX2)
if: steps.cpu.outputs.have_avx512 == '1'
run: ctest --test-dir build -R test_sha3_avx512_kat --output-on-failure
ed25519-backend-differential:
name: Ed25519 backend differential (donna vs fe51)
# WHY THIS JOB EXISTS
#
# This repository ships TWO Ed25519 implementations and picks between them
# at configure time (CMakeLists.txt removes src/c/ama_ed25519.c from the
# source list and substitutes src/c/ed25519_donna_shim.c whenever
# AMA_ED25519_ASSEMBLY is ON, which is the default on x86-64):
#
# * ed25519-donna — x86-64 assembly, what every x86-64 lane builds
# * fe51 — the portable path in ama_ed25519.c
#
# Until this job existed, no CI lane ever built fe51 on x86-64. It was
# exercised only incidentally on the ubuntu-24.04-arm runners, and nothing
# anywhere compared the two against each other.
#
# That is not a theoretical gap. INVARIANT-26 fixed an RFC 8032 §5.1.7
# canonical-S defect that was present in BOTH backends but for DIFFERENT
# reasons — donna's `RS[63] & 224` test was too weak, while fe51 had no
# range check at all — so a fix written against one could easily have left
# the other broken. Two implementations of one public API that nothing
# compares will drift; the only question is when it is noticed.
#
# This job builds both on the same runner and asserts they return the same
# verdict for every signature in a shared corpus. It is a differential
# test, not a KAT: it does not care which answer is right, only that the
# two backends cannot disagree without failing the build.
runs-on: ubuntu-latest
# Two full CMake builds of the native library plus a C test run. Measured
# at roughly 4 minutes locally; 30 leaves headroom for a cold runner
# without letting a genuinely hung build sit for an hour.
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.11"
- name: Install build toolchain
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake
- name: Build donna backend (x86-64 default)
run: |
cmake -S . -B build-donna \
-DAMA_ED25519_ASSEMBLY=ON \
-DAMA_USE_NATIVE_PQC=ON \
-DCMAKE_BUILD_TYPE=Release \
-DAMA_BUILD_TESTS=ON \
-DAMA_BUILD_EXAMPLES=OFF
cmake --build build-donna -j"$(nproc)"
- name: Build fe51 backend (portable path, never built on x86-64 before)
run: |
cmake -S . -B build-fe51 \
-DAMA_ED25519_ASSEMBLY=OFF \
-DAMA_USE_NATIVE_PQC=ON \
-DCMAKE_BUILD_TYPE=Release \
-DAMA_BUILD_TESTS=ON \
-DAMA_BUILD_EXAMPLES=OFF
cmake --build build-fe51 -j"$(nproc)"
- name: Run the C test suite against the fe51 backend
# The donna backend's C tests already run in ci-build-test.yml on every
# x86-64 lane. This is the half that had no coverage.
working-directory: build-fe51
run: ctest --output-on-failure
- name: Differential — both backends must agree on every signature
run: |
python tools/check_ed25519_backend_parity.py \
--donna build-donna/lib/libama_cryptography.so \
--fe51 build-fe51/lib/libama_cryptography.so
ci-gate:
name: CI Gate
# Single aggregating status check for this workflow. Branch protection
# should require ONLY this context (one per primary CI workflow) instead of
# the individual job names: adding, renaming, or matrix-expanding a job then
# updates this needs: list under code review rather than drifting the
# branch-protection config out of sync (required-context drift). `needs:` is
# workflow-local, so each of ci.yml / ci-build-test.yml / static-analysis.yml
# carries its own gate.
#
# if: always() so the gate always resolves to a definitive red/green even
# when a dependency fails — otherwise it would be reported as `skipped` and
# leave the PR stuck "waiting for status".
if: always()
needs:
- test
- code-quality
- docs-build
- security-checks
- benchmark-regression
- constant-time-check
- test-avx512
- ed25519-backend-differential
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- name: Fail unless every required job succeeded
# STRICT. Every job in this workflow runs unconditionally — none carries a
# job-level `if:` — so on the authoritative head-commit run each MUST be
# `success`. `skipped` now fails the gate too: on this workflow it can only
# mean a `needs:` dependency failed (already red) or a future job's `if:`
# drifted so it silently stopped running — exactly the gap a lenient gate
# hides. `cancelled` also fails, but only ever appears on a run superseded
# by a newer push or a manual abort, neither of which gates the latest head
# commit that branch protection evaluates. Matrix jobs are fail-fast:false
# and timeouts report as `failure`.
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled') }}
run: |
echo "::error::CI Gate failed — a required job did not succeed."
echo "Dependency results: ${{ join(needs.*.result, ', ') }}"
exit 1
- name: All required jobs succeeded
run: echo "CI Gate passed — all required jobs are green."