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
name: Publish to crates.io
on:
push:
branches:
- main
tags:
- 'v*'
pull_request:
branches:
- main
workflow_dispatch:
inputs:
version:
description: 'Version to publish (e.g., v0.1.0)'
required: true
type: string
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Test Suite
runs-on: ubuntu-latest
# A tag push lands on a commit that already merged to `main` and was gated there
# (this same job, via the `branches: [main]` trigger above) — re-running the full
# suite again at tag/deploy time is redundant and a pure flake-exposure surface
# (dig_ecosystem #488). Deploy = build + package + publish only (see `publish` job).
if: "!startsWith(github.ref, 'refs/tags/v')"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
- name: Check formatting
run: cargo fmt --all -- --check
- name: Check clippy
run: cargo clippy --all-targets --all-features -- -D warnings
# `--all-features` above turns the `custody` / `hd-derivation` gates ON, so
# neither it nor the coverage run below can observe a broken gate, and neither
# can they observe a `missing_docs` or type error that only appears when the
# custody module is compiled OUT. These two steps are the only ones that build
# the crate in a configuration a gate-respecting consumer actually uses.
- name: Clippy (core-only, custody compiled out)
run: cargo clippy --no-default-features --features file-backend,testing --all-targets -- -D warnings
- name: Clippy (custody without hd-derivation)
run: cargo clippy --no-default-features --features file-backend,testing,custody --all-targets -- -D warnings
# Backwards-compatibility (SPEC.md §5.1 / dig_ecosystem CLAUDE.md §5.1): the
# opaque golden vectors must still decode in a build with no custody surface at
# all. `dig-node`'s seed_store.rs classifies old blobs through this exact
# configuration, so a DIGVK1/DIGLW1 magic that stopped being recognised here
# would silently change how it reads content written by earlier versions.
- name: Golden opaque vectors (core-only build)
run: cargo test --no-default-features --features file-backend,testing --test opaque_vectors
# Proves the feature gates by compiling throwaway consumers under each
# configuration and asserting the exact rustc diagnostic. See the script header
# for why this cannot be an ordinary #[test].
- name: Public-surface feature-gate check
run: bash scripts/surface-check.sh
# Coverage IS the test run: cargo-llvm-cov drives nextest under instrumentation,
# gates at >=80% line coverage (build fails below the floor), and emits lcov for
# visibility. nextest retries a failing test up to 2x (.config/nextest.toml) and
# natively reports "failed then passed on retry" as flaky (dig_ecosystem #489) —
# no separate flaky-tracking tooling needed. Single-threaded to match the
# tempfile/filesystem-backed tests' expectations.
- name: Install cargo-llvm-cov + cargo-nextest
uses: taiki-e/install-action@v2
with:
tool: cargo-llvm-cov,nextest
- name: Run tests with coverage (gate >=80% lines; nextest retries=2, flaky surfaced)
run: cargo llvm-cov nextest --all-features --test-threads=1 --fail-under-lines 80 --lcov --output-path lcov.info
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: lcov-coverage
path: lcov.info
if-no-files-found: error
# `default-members = ["."]`, so every step above operates on the root package
# ONLY — the `hardware/` member is invisible to all of them. Its ladder is
# deliberately platform-independent (the tier decision is the code that runs on
# hosts WITHOUT a trusted component, which is most of them), so it is gated here
# on ubuntu as well as on the platform matrix below. Omitting this would leave
# the degrade ladder tested only where the hardware exists, which is exactly
# backwards.
- name: Clippy dig-keystore-hardware (ladder; no trusted component here)
run: cargo clippy -p dig-keystore-hardware --all-targets -- -D warnings
# Gated at the same >=80% floor as the root crate (dig_ecosystem CLAUDE.md
# §2.3). Measured here rather than on the platform matrix because ubuntu
# compiles ONLY the platform-independent half — the ladder, the tier
# reporting, the probe classification and the TPM command codec — which is
# precisely the half a floor should bind: a device-conversation arm is
# unreachable without working (or malfunctioning) hardware, and including
# one would set the floor by what the runner happens to have in it.
#
# The three excluded files are exactly the device conversations. Two of
# them, `windows.rs` and `macos.rs`, were already excluded here by simply
# not compiling off their platforms; `linux/session.rs` is named because
# the Linux binding is pure `std` and therefore DOES compile everywhere,
# which is deliberate — it is what lets its refusal arms be tested on every
# host rather than on one CI leg. Excluding it from the FLOOR keeps that
# choice from importing unreachable lines into the gate.
#
# What stays inside the floor is the part that decides what happens on a
# machine with no trusted component, which is almost every machine:
# `platform/linux.rs` (probe classification), `platform/tpm2.rs` (the
# command codec, testable byte-for-byte on any host), `platform/content_key.rs`,
# `ladder.rs`, `lib.rs` and `platform.rs`. Every excluded file is still
# compiled, clippied with `-D warnings`, and has its reachable arms tested
# on every leg of the matrix below.
- name: Coverage for dig-keystore-hardware (gate >=80% lines)
run: |
cargo llvm-cov -p dig-keystore-hardware \
--ignore-filename-regex 'platform.(windows|macos).rs|linux.session.rs' \
--fail-under-lines 80
- name: Check documentation
run: cargo doc --no-deps --all-features
# The `test` job above runs on ubuntu only, where `OsKeychainBackend`'s real
# implementation and its `os_integration` test are both compiled OUT — the
# `keyring` dependency is target-gated to Windows/macOS. That left the whole
# OS-credential-store backend (dig_ecosystem #1024) shipping to crates.io
# without ever being compiled by CI, let alone exercised, and it is the same
# blind spot that blocks the platform hardware providers (dig_ecosystem
# #1693) from being added at all.
#
# This job is the narrow fix: build and test the platform-gated code on the
# platforms that have it. Coverage stays on ubuntu — running it three times
# measures the same lines, and the floor is a property of the crate, not of
# the runner.
platform:
name: Platform-gated code (${{ matrix.os }})
runs-on: ${{ matrix.os }}
# Same rationale as `test` above: this commit was already gated on its PR /
# main-branch push before being tagged — skip the redundant re-run on tag push.
if: "!startsWith(github.ref, 'refs/tags/v')"
strategy:
# One platform's failure must not cancel the other's run: when a
# target-gated regression appears, "which platforms are affected?" is the
# first question, and fail-fast answers it for only one of them.
fail-fast: false
matrix:
# ubuntu is here for the Linux TPM provider, not for the root crate:
# `default-members = ["."]` means the ubuntu `test` job above never
# builds `hardware/` at all, so without this entry the Linux arm of
# `platform::candidates` and the `unsupported_reason` agreement test
# would ship having been compiled by nothing.
os:
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
- name: Check clippy
run: cargo clippy --all-targets --all-features -- -D warnings
# `os_integration::real_os_store_round_trips_where_available` drives the
# host's actual credential store and self-skips where none is usable, so
# this stays deterministic on a runner whose keychain may be locked.
- name: Run tests
run: cargo test --all-features
# The whole reason `hardware/` is a separate member: its FFI cannot compile
# under the root crate's `unsafe_code = "forbid"`. This is the only job that
# compiles it against a real platform SDK.
#
# `DIG_KEYSTORE_REQUIRE_TPM` is deliberately NOT set. A GitHub runner is not
# documented to expose a TPM, and setting it would turn an unproven assumption
# about the runner into a red build. Unset, the hardware tests still run and
# report which properties they did NOT exercise — a pass here means "the
# degraded path is correct", never "the TPM path is correct". Set the variable
# on a self-hosted runner that genuinely has one.
- name: Clippy + test dig-keystore-hardware (platform FFI)
run: |
cargo clippy -p dig-keystore-hardware --all-targets -- -D warnings
cargo test -p dig-keystore-hardware -- --nocapture
wasm:
name: wasm build + clippy + wasm-bindgen-test
runs-on: ubuntu-latest
# Same rationale as `test` above: this commit was already gated on its PR /
# main-branch push before being tagged — skip the redundant re-run on tag push.
if: "!startsWith(github.ref, 'refs/tags/v')"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
components: clippy
targets: wasm32-unknown-unknown
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
- name: install wasm-pack (official installer)
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: clippy (dig-keystore-wasm, wasm32 target)
run: cargo clippy -p dig-keystore-wasm --target wasm32-unknown-unknown --all-targets -- -D warnings
- uses: actions/setup-node@v4
with:
node-version: 20
# `wasm-pack test --node` runs the wasm-bindgen-test suite (wasm/tests/opaque_wasm.rs),
# including the native<->wasm byte-compatibility KAT vectors (dig_ecosystem #147 Phase A).
- name: wasm-bindgen-test (Node)
working-directory: wasm
run: npm test
publish:
name: Publish to crates.io
runs-on: ubuntu-latest
needs:
# `test`/`platform`/`wasm` are skipped (not failed) on a tag push by design (see
# their `if:` above) — a tagged commit already passed those gates on its
# PR/main-branch push, so each is tolerated as `skipped` as well as `success`.
#
# `platform` is in this list because it is the only job that compiles the
# target-gated `OsKeychainBackend`; leaving it out let a `workflow_dispatch`
# publish proceed over a RED platform job, which is the same blind spot that hid
# that backend from CI for five releases.
if: |
always() &&
(needs.test.result == 'success' || needs.test.result == 'skipped') &&
(needs.platform.result == 'success' || needs.platform.result == 'skipped') &&
(needs.wasm.result == 'success' || needs.wasm.result == 'skipped') &&
(startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Cache dependencies
uses: Swatinem/rust-cache@v2
- name: Verify package can be built
run: cargo build --release
- name: Verify package can be packaged
run: cargo package --allow-dirty
- name: Check if CARGO_REGISTRY_TOKEN is available
run: |
if [ -z "${{ secrets.CARGO_REGISTRY_TOKEN }}" ]; then
echo "CARGO_REGISTRY_TOKEN secret is not set in repository settings"
exit 1
fi
- name: Publish to crates.io
run: cargo publish --allow-dirty --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
# `dig-keystore-hardware` depends on `dig-keystore` by VERSION (the `path` is
# stripped on publish), so it cannot be published until the version just pushed
# is resolvable from the index. The index lags the publish by seconds to minutes,
# and a `cargo publish` that runs too early fails with what reads exactly like
# "that version does not exist" — so this waits rather than guessing.
#
# The `User-Agent` header is REQUIRED: without it crates.io answers with
# something indistinguishable from "not published", which would make this loop
# time out on a perfectly good release.
- name: Wait for dig-keystore to appear in the crates.io index
run: |
set -euo pipefail
VERSION=$(python3 -c 'import tomllib;print(tomllib.load(open("Cargo.toml","rb"))["package"]["version"])')
for attempt in $(seq 1 60); do
if curl -sfH 'User-Agent: dig-keystore-ci' https://index.crates.io/di/g-/dig-keystore | grep -q "\"vers\":\"${VERSION}\""; then
echo "dig-keystore ${VERSION} is resolvable"
exit 0
fi
echo "attempt ${attempt}: dig-keystore ${VERSION} not in the index yet"
sleep 10
done
echo "dig-keystore ${VERSION} never appeared in the index" >&2
exit 1
- name: Publish dig-keystore-hardware to crates.io
working-directory: hardware
run: cargo publish --allow-dirty --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
create-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: publish
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Extract version from tag
id: extract_version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: "dig-keystore v${{ steps.extract_version.outputs.VERSION }}"
body: |
## dig-keystore v${{ steps.extract_version.outputs.VERSION }}
Encrypted secret-key storage for DIG Network binaries (BLS signing + L1 wallet keys):
typed per-scheme magic files, AES-256-GCM + Argon2id, zeroizing memory hygiene and
filesystem-atomic writes. The single audit surface for where validator and wallet
keys live.
### Installation
```toml
[dependencies]
dig-keystore = "${{ steps.extract_version.outputs.VERSION }}"
```
draft: false
prerelease: false