Skip to content

Commit f4ec9a5

Browse files
authored
Merge pull request #167 from jplomas/main
fix: harden CI verification, fuzzing, and release pipeline
2 parents e3de0cd + 7986641 commit f4ec9a5

51 files changed

Lines changed: 3270 additions & 439 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/actionlint.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,14 @@ jobs:
4747
run: pipx install zizmor
4848

4949
- name: Run zizmor
50-
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
51-
# about, including stylistic findings like missing job concurrency
50+
# Pedantic persona: zizmor reports everything it knows about,
51+
# including stylistic findings like missing job concurrency
5252
# limits, undocumented permissions, and template-injection
5353
# candidates. We hold the entire workflow surface to that bar —
5454
# any new finding at any severity fails the build. Configuration
55-
with:
55+
# exceptions live in .github/zizmor.yml (currently only the SLSA
5656
# reusable workflow ref-pin).
57+
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
58+
with:
5759
persona: pedantic
5860
config: .github/zizmor.yml

.github/workflows/acvp.yml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ concurrency:
1414
group: ${{ github.workflow }}-${{ github.ref }}
1515
cancel-in-progress: true
1616

17+
# Pinned to an explicit upstream commit so vector changes (or upstream
18+
# compromise) cannot silently alter what "verification passed" means.
19+
# Bump procedure: CONTRIBUTING.md "Updating pinned verification upstreams".
20+
env:
21+
ACVP_SERVER_PIN: 15c0f3deeefbfa8cb6cd32a99e1ca3b738c66bf0 # 2026-04-16
22+
1723
jobs:
1824
mldsa87-acvp:
1925
name: ML-DSA-87 ACVP Verification
@@ -28,20 +34,20 @@ jobs:
2834
with:
2935
node-version: 22.x
3036

31-
- name: Clone NIST ACVP-Server (sparse)
37+
- name: Clone NIST ACVP-Server (sparse, pinned)
3238
run: |
3339
# Sparse checkout of just the ML-DSA keyGen + sigGen vector
34-
# directories — the repo is large. Tracking master gives us
35-
# upstream additions automatically; pin to a specific commit
36-
# here if a future vector starts producing legitimate failures.
37-
git clone --depth 1 --no-checkout --filter=blob:none \
40+
# directories — the repo is large. Blobless clone + sparse
41+
# checkout fetches only the blobs under the selected paths at
42+
# the pinned commit.
43+
git clone --no-checkout --filter=blob:none \
3844
https://github.com/usnistgov/ACVP-Server.git /tmp/acvp-server
3945
cd /tmp/acvp-server
4046
git sparse-checkout init --cone
4147
git sparse-checkout set \
4248
gen-val/json-files/ML-DSA-keyGen-FIPS204 \
4349
gen-val/json-files/ML-DSA-sigGen-FIPS204
44-
git checkout
50+
git checkout "$ACVP_SERVER_PIN"
4551
echo "ACVP-Server commit: $(git rev-parse --short HEAD)"
4652
echo "ACVP-Server date: $(git log -1 --format=%ci)"
4753
ls -la gen-val/json-files/ML-DSA-keyGen-FIPS204/prompt.json

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ jobs:
2929
node-version: '22.x'
3030
- run: npm ci
3131
- run: npm run lint
32+
- name: Shared-file sync (dilithium5 ↔ mldsa87)
33+
run: npm run check-shared
3234

3335
test:
3436
name: Test (Node ${{ matrix.node-version }})
3537
runs-on: ubuntu-latest
3638
strategy:
3739
matrix:
38-
node-version: [20.x, 22.x]
40+
node-version: [20.x, 22.x, 24.x]
3941
steps:
4042
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
4143
with:

.github/workflows/coverage.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ jobs:
2929
- run: npm ci
3030
- run: npm test
3131
- run: npm run report-coverage
32+
# Push uploads gate hard (fail_ci_if_error: true); PR uploads are
33+
# best-effort so a Codecov hiccup can't block a PR — the coverage
34+
# status checks themselves still come from codecov.yml targets.
3235
- name: Upload dilithium5 coverage to Codecov
3336
if: github.repository == 'theQRL/qrypto.js' && github.event_name == 'push'
3437
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
@@ -45,3 +48,19 @@ jobs:
4548
files: ./packages/mldsa87/coverage.lcov
4649
flags: mldsa87
4750
fail_ci_if_error: true
51+
- name: Upload dilithium5 coverage to Codecov (PR)
52+
if: github.repository == 'theQRL/qrypto.js' && github.event_name == 'pull_request'
53+
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
54+
with:
55+
token: ${{ secrets.CODECOV_TOKEN }}
56+
files: ./packages/dilithium5/coverage.lcov
57+
flags: dilithium5
58+
fail_ci_if_error: false
59+
- name: Upload mldsa87 coverage to Codecov (PR)
60+
if: github.repository == 'theQRL/qrypto.js' && github.event_name == 'pull_request'
61+
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
62+
with:
63+
token: ${{ secrets.CODECOV_TOKEN }}
64+
files: ./packages/mldsa87/coverage.lcov
65+
flags: mldsa87
66+
fail_ci_if_error: false

.github/workflows/cross-verify.yml

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@ concurrency:
1414
group: ${{ github.workflow }}-${{ github.ref }}
1515
cancel-in-progress: true
1616

17+
# All upstream repositories are pinned to explicit commit SHAs so that a
18+
# change (or compromise) upstream cannot silently alter what "verification
19+
# passed" means, and so PR results are reproducible. Bump procedure:
20+
# CONTRIBUTING.md "Updating pinned verification upstreams".
21+
#
22+
# GO_QRLLIB_DILITHIUM5_PIN is permanently historical: upstream go-qrllib
23+
# removed crypto/dilithium in v0.9.0 (1ae1760, PR #109, 2026-06-10).
24+
# b2ee4790 = v0.8.0, the last release containing it; this leg verifies
25+
# interop with the frozen legacy implementation. The mldsa87 go-qrllib leg
26+
# tracks a current pin and should be bumped routinely.
27+
env:
28+
GO_QRLLIB_DILITHIUM5_PIN: b2ee4790ef041104d2a48ae87cf68c0de621c89e # v0.8.0 (2026-06-08), last release with crypto/dilithium
29+
GO_QRLLIB_MLDSA87_PIN: 6f9978367906233874b406bd5d55b0e8b8d01d9c # v0.9.0 (2026-06-10)
30+
PQCRYSTALS_DILITHIUM5_PIN: ac743d588c6532aed027ccb1e7a24bfe6e35a120 # Round 3 (pre-FIPS) reference
31+
PQCRYSTALS_MLDSA87_PIN: d35ba3fe5449bee3e6d43e1f296c3ca818bd36be # 2026-06-03, FIPS 204 reference
32+
1733
jobs:
1834
dilithium5-cross-verify:
1935
name: Dilithium5 Cross-Verification (qrypto.js ↔ go-qrllib)
@@ -37,11 +53,11 @@ jobs:
3753
- name: Install qrypto.js dependencies
3854
run: npm ci
3955

40-
- name: Clone go-qrllib
56+
- name: Clone go-qrllib (pinned)
4157
run: |
42-
git clone --depth 1 https://github.com/theQRL/go-qrllib.git /tmp/go-qrllib
43-
cd /tmp/go-qrllib
44-
echo "go-qrllib commit: $(git rev-parse --short HEAD)"
58+
git clone https://github.com/theQRL/go-qrllib.git /tmp/go-qrllib
59+
git -C /tmp/go-qrllib checkout "$GO_QRLLIB_DILITHIUM5_PIN"
60+
echo "go-qrllib commit: $(git -C /tmp/go-qrllib rev-parse --short HEAD)"
4561
4662
- name: Generate qrypto.js Dilithium5 signature
4763
run: node .github/cross-verify/dilithium5_sign.js
@@ -85,11 +101,11 @@ jobs:
85101
- name: Install qrypto.js dependencies
86102
run: npm ci
87103

88-
- name: Clone go-qrllib
104+
- name: Clone go-qrllib (pinned)
89105
run: |
90-
git clone --depth 1 https://github.com/theQRL/go-qrllib.git /tmp/go-qrllib
91-
cd /tmp/go-qrllib
92-
echo "go-qrllib commit: $(git rev-parse --short HEAD)"
106+
git clone https://github.com/theQRL/go-qrllib.git /tmp/go-qrllib
107+
git -C /tmp/go-qrllib checkout "$GO_QRLLIB_MLDSA87_PIN"
108+
echo "go-qrllib commit: $(git -C /tmp/go-qrllib rev-parse --short HEAD)"
93109
94110
- name: Generate qrypto.js ML-DSA-87 signature
95111
run: node .github/cross-verify/mldsa87_sign.js
@@ -128,12 +144,11 @@ jobs:
128144
- name: Install qrypto.js dependencies
129145
run: npm ci
130146

131-
- name: Clone pq-crystals Dilithium (Round 3)
147+
- name: Clone pq-crystals Dilithium (pinned, Round 3)
132148
run: |
133149
git clone https://github.com/pq-crystals/dilithium.git /tmp/dilithium-ref
134-
cd /tmp/dilithium-ref
135-
git checkout ac743d5 # Round 3 (pre-FIPS)
136-
echo "Reference commit: $(git rev-parse --short HEAD)"
150+
git -C /tmp/dilithium-ref checkout "$PQCRYSTALS_DILITHIUM5_PIN"
151+
echo "Reference commit: $(git -C /tmp/dilithium-ref rev-parse --short HEAD)"
137152
138153
- name: Generate qrypto.js Dilithium5 signature
139154
run: node .github/cross-verify/dilithium5_sign.js
@@ -186,11 +201,11 @@ jobs:
186201
- name: Install qrypto.js dependencies
187202
run: npm ci
188203

189-
- name: Clone pq-crystals Dilithium (FIPS 204 / ML-DSA)
204+
- name: Clone pq-crystals Dilithium (pinned, FIPS 204 / ML-DSA)
190205
run: |
191-
git clone --depth 1 https://github.com/pq-crystals/dilithium.git /tmp/mldsa-ref
192-
cd /tmp/mldsa-ref
193-
echo "Reference commit: $(git rev-parse --short HEAD)"
206+
git clone https://github.com/pq-crystals/dilithium.git /tmp/mldsa-ref
207+
git -C /tmp/mldsa-ref checkout "$PQCRYSTALS_MLDSA87_PIN"
208+
echo "Reference commit: $(git -C /tmp/mldsa-ref rev-parse --short HEAD)"
194209
195210
- name: Generate qrypto.js ML-DSA-87 signature
196211
run: node .github/cross-verify/mldsa87_sign.js
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
name: Scheduled Fuzz
2+
3+
# The per-push CI fuzz job runs the 10k-iteration quick profile only.
4+
# This workflow runs the 100k-iteration weekly profile every Sunday.
5+
# The 1M-iteration deep profile is deliberately NOT scheduled — it is
6+
# reserved for audit-level code review and can be launched on demand via
7+
# workflow_dispatch (or locally: scripts/fuzz/run-campaign.mjs --profile deep).
8+
on:
9+
schedule:
10+
- cron: '41 4 * * 0' # weekly profile, Sundays 04:41 UTC
11+
workflow_dispatch:
12+
inputs:
13+
profile:
14+
description: 'Fuzz profile (quick | weekly | deep)'
15+
required: false
16+
default: 'weekly'
17+
type: choice
18+
options:
19+
- quick
20+
- weekly
21+
- deep
22+
23+
permissions:
24+
contents: read
25+
26+
concurrency:
27+
group: ${{ github.workflow }}-${{ github.ref }}
28+
cancel-in-progress: false
29+
30+
jobs:
31+
fuzz:
32+
name: Fuzz campaign
33+
runs-on: ubuntu-latest
34+
# Deep (manual) is 1M iterations across all harnesses; leave generous
35+
# headroom under the 360-minute job ceiling so artifacts still upload.
36+
timeout-minutes: 350
37+
steps:
38+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
39+
with:
40+
persist-credentials: false
41+
42+
- name: Use Node.js 22.x
43+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
44+
with:
45+
node-version: '22.x'
46+
47+
- run: npm ci
48+
- run: npm run build
49+
50+
- name: Select profile
51+
id: select
52+
env:
53+
EVENT_NAME: ${{ github.event_name }}
54+
PROFILE_INPUT: ${{ inputs.profile }}
55+
run: |
56+
set -euo pipefail
57+
if [ "${EVENT_NAME}" = "workflow_dispatch" ]; then
58+
profile="${PROFILE_INPUT:-weekly}"
59+
else
60+
profile="weekly"
61+
fi
62+
echo "profile=${profile}" >> "$GITHUB_OUTPUT"
63+
echo "Selected profile: ${profile}"
64+
65+
- name: Run fuzz campaign
66+
env:
67+
PROFILE: ${{ steps.select.outputs.profile }}
68+
# run_id makes the campaign reproducible: the engine derives all
69+
# mutations deterministically from the master seed.
70+
MASTER_SEED: ${{ github.run_id }}
71+
run: node scripts/fuzz/run-campaign.mjs --profile "$PROFILE" --seed "$MASTER_SEED"
72+
73+
- name: Upload campaign results
74+
if: always()
75+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
76+
with:
77+
name: fuzz-results-${{ github.run_id }}
78+
path: fuzz-results/
79+
retention-days: 30
80+
if-no-files-found: warn

.github/workflows/release.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jobs:
3737

3838
- run: npm ci
3939
- run: npm run lint
40+
- run: npm run check-shared
4041
- run: npm test
4142
- run: npm run build
4243
- name: Verify dist/ is up to date
@@ -246,6 +247,34 @@ jobs:
246247
npm publish "dist/tarballs/${tarball_name}" --access public
247248
done < dist/released-packages.tsv
248249
250+
# Tags and GitHub releases were already created by multi-semantic-release
251+
# in the prepare job; if npm publishing silently failed we would otherwise
252+
# ship a tag/release with no npm artifact (this exact failure orphaned
253+
# wallet.js v6.2.0). Verify the registry actually serves every released
254+
# version before attaching release assets. Recovery runbook: RELEASE.md
255+
# "Recovering an orphaned release".
256+
- name: Verify packages are live on npm
257+
run: |
258+
set -euo pipefail
259+
while IFS=$'\t' read -r _package_path package_name package_version _release_tag _tarball_name; do
260+
echo "Verifying ${package_name}@${package_version} on the npm registry"
261+
ok=""
262+
for attempt in 1 2 3 4 5 6 7 8 9 10; do
263+
served="$(npm view "${package_name}@${package_version}" version 2>/dev/null || true)"
264+
if [ "${served}" = "${package_version}" ]; then
265+
echo " confirmed on attempt ${attempt}"
266+
ok=1
267+
break
268+
fi
269+
echo " not yet visible (attempt ${attempt}/10); retrying in 15s"
270+
sleep 15
271+
done
272+
if [ -z "${ok}" ]; then
273+
echo "::error::${package_name}@${package_version} was published but is not served by the registry after 10 attempts. Tag and GitHub release exist without an npm artifact — follow RELEASE.md 'Recovering an orphaned release'."
274+
exit 1
275+
fi
276+
done < dist/released-packages.tsv
277+
249278
- name: Generate SBOM SPDX
250279
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
251280
with:

.github/workflows/wycheproof.yml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ concurrency:
1414
group: ${{ github.workflow }}-${{ github.ref }}
1515
cancel-in-progress: true
1616

17+
# Pinned to an explicit upstream commit so vector changes (or upstream
18+
# compromise) cannot silently alter what "verification passed" means.
19+
# Bump procedure: CONTRIBUTING.md "Updating pinned verification upstreams".
20+
env:
21+
WYCHEPROOF_PIN: 6d7cccd0fcb1917368579adeeac10fe802f1b521 # 2026-06-06
22+
1723
jobs:
1824
mldsa87-wycheproof:
1925
name: ML-DSA-87 Wycheproof Verification
@@ -28,18 +34,18 @@ jobs:
2834
with:
2935
node-version: 22.x
3036

31-
- name: Clone C2SP/wycheproof (sparse)
37+
- name: Clone C2SP/wycheproof (sparse, pinned)
3238
run: |
3339
# Sparse checkout of testvectors_v1/ only; the repo is large
34-
# and we only need the ML-DSA JSON files. Tracking master gives
35-
# us upstream additions automatically; pin to a specific commit
36-
# here if a future vector starts producing legitimate failures.
37-
git clone --depth 1 --no-checkout --filter=blob:none \
40+
# and we only need the ML-DSA JSON files. Blobless clone +
41+
# sparse checkout fetches only the blobs under the selected
42+
# paths at the pinned commit.
43+
git clone --no-checkout --filter=blob:none \
3844
https://github.com/C2SP/wycheproof.git /tmp/wycheproof
3945
cd /tmp/wycheproof
4046
git sparse-checkout init --cone
4147
git sparse-checkout set testvectors_v1
42-
git checkout
48+
git checkout "$WYCHEPROOF_PIN"
4349
echo "Wycheproof commit: $(git rev-parse --short HEAD)"
4450
echo "Wycheproof date: $(git log -1 --format=%ci)"
4551
ls -la testvectors_v1/mldsa_87_*.json

0 commit comments

Comments
 (0)