Skip to content

Commit 10dbf23

Browse files
feat: initial dig-session facade (unlock/enroll/sign/inject) (#1)
## Task New crate **dig-session** (#1249, seam-7 session/keystore extraction) — the DIG session/keystore layer: unlock identity keys + inject signing primitives. Composes `dig-keystore` + `dig-identity` behind one curated, custody-safe facade. GPL-2.0-only. Tracks super-repo issue `DIG-Network/dig_ecosystem#1249`. ## Public API - `Session::unlock::<K: KeyScheme>(backend, path, password) -> Result<UnlockedIdentity<K>>` — generic `load -> unlock -> SignerHandle`. - `Session::enroll_identity(backend, path, password, seed) -> Result<UnlockedIdentity<L1WalletBls>>` — derive the canonical dig-identity key once, store it, return it unlocked. - `UnlockedIdentity<K>`: `public_key`, `sign`, `signing_fn`, `inject_into` (yields a bare `SigningFn` primitive so consumers stay identity-agnostic, #908). Zeroizing, redacting `Debug`, no `Clone`. - `SigningFn<K> = Arc<dyn Fn(&[u8]) -> K::Signature + Send + Sync>`. - `SessionError` / `Result`. ## Deviation from the locked design (custody-critical) — needs gate attention The locked design said "store the DERIVED sk via `Keystore::create` under **BlsSigning**". That is INCORRECT: `BlsSigning` reconstructs its stored bytes via `chia_bls::SecretKey::from_seed` (a one-way KDF) on every unlock, so storing an already-derived identity sk under `BlsSigning` re-derives a DIFFERENT key (the documented #64/#57 pitfall) whose public key would NOT match the DID-anchored identity key. **Resolution:** the identity path stores the derived sk under **`L1WalletBls`** (the `from_bytes` faithful round-trip scheme). `Session::unlock` stays generic, so `BlsSigning` is still available for seed-derived validator keys. A regression test (C-2) asserts the enrolled public key equals `dig_identity::public_key_bytes(derive_identity_sk(master))`, and would fail on any revert to `BlsSigning`. ## Scope No seal/decap (belongs to dig-message, same crate level). First cut = unlock/sign/inject only. ## Verification - `cargo test`: 8 integration tests + 1 doctest green. - `cargo fmt --check`: clean. `cargo clippy --all-targets -D warnings`: clean. - `cargo llvm-cov`: 98.18% lines / 96.34% regions (floor 80%). - Deps crates.io-only, reference-down (dig-keystore 0.4.1, dig-identity 0.4.2, both live). Layering legal. ## Blast radius New standalone crate, zero existing callers. Not yet a superproject submodule (main adds it after publish). No cross-repo symbol edits. Draft until CI green; do NOT merge (awaiting triple gate). Do NOT publish to crates.io yet. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 37dce31 commit 10dbf23

18 files changed

Lines changed: 2492 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
workflow_dispatch:
9+
10+
# Least privilege: CI only reads the checked-out repo (GHAS/CodeQL requirement).
11+
permissions:
12+
contents: read
13+
14+
env:
15+
CARGO_TERM_COLOR: always
16+
17+
jobs:
18+
test:
19+
name: Test Suite
20+
runs-on: ubuntu-latest
21+
steps:
22+
- name: Checkout code
23+
uses: actions/checkout@v4
24+
25+
- name: Install Rust toolchain
26+
uses: dtolnay/rust-toolchain@stable
27+
with:
28+
toolchain: stable
29+
components: rustfmt, clippy
30+
31+
- name: Cache dependencies
32+
uses: Swatinem/rust-cache@v2
33+
34+
- name: Install cargo-nextest
35+
uses: taiki-e/install-action@nextest
36+
37+
- name: Check formatting
38+
run: cargo fmt --all -- --check
39+
40+
- name: Check clippy
41+
run: cargo clippy --all-targets --all-features -- -D warnings
42+
43+
# nextest runs each test as its own process and natively detects+reports
44+
# flaky tests (failed-then-passed-on-retry) instead of silently hiding
45+
# them. `--test-threads=1` mirrors the prior serial run (some integration
46+
# tests assume serial execution).
47+
- name: Run tests (nextest, flaky-aware)
48+
run: cargo nextest run --all-features --retries 2 --test-threads=1
49+
50+
# nextest does NOT run doctests, so the headline `lib.rs` example is ungated
51+
# by the step above. Run it explicitly so a doc example can never rot.
52+
- name: Run doctests
53+
run: cargo test --doc --all-features
54+
55+
- name: Check documentation
56+
run: cargo doc --no-deps --all-features
57+
58+
coverage:
59+
name: Coverage (>=80% lines, gated)
60+
runs-on: ubuntu-latest
61+
steps:
62+
- name: Checkout code
63+
uses: actions/checkout@v4
64+
65+
- name: Install Rust toolchain
66+
uses: dtolnay/rust-toolchain@stable
67+
with:
68+
toolchain: stable
69+
components: llvm-tools-preview
70+
71+
- name: Cache dependencies
72+
uses: Swatinem/rust-cache@v2
73+
74+
- name: Install cargo-llvm-cov
75+
uses: taiki-e/install-action@cargo-llvm-cov
76+
77+
- name: Install cargo-nextest
78+
uses: taiki-e/install-action@nextest
79+
80+
# Measure line coverage and FAIL the build if it drops below 80%.
81+
# `cargo llvm-cov nextest` runs the flaky-aware nextest runner under
82+
# coverage instrumentation so coverage is still collected (running
83+
# `cargo nextest run` and `cargo llvm-cov` as separate steps collects
84+
# zero coverage and always fails the gate). `--test-threads 1` mirrors
85+
# the serial run used by the test suite (some integration tests assume
86+
# serial execution) — passed as a native nextest flag, NOT after `--`
87+
# (nextest rejects `--test-threads=1` forwarded to the test binary). A
88+
# summary report is printed; the gate is enforced by
89+
# `--fail-under-lines 80`.
90+
- name: Run coverage with 80% line gate
91+
run: cargo llvm-cov nextest --all-features --workspace --fail-under-lines 80 --retries 2 --test-threads 1

.github/workflows/commitlint.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Enforce Conventional Commits on every PR to the default branch (and the PR title).
2+
# Reads commitlint.config.mjs at the repo root (extends @commitlint/config-conventional).
3+
# No local install needed — the action bundles commitlint. See CLAUDE.md §3.2.
4+
name: Commitlint
5+
6+
on:
7+
pull_request:
8+
branches:
9+
- main
10+
types: [opened, edited, synchronize, reopened]
11+
12+
concurrency:
13+
group: ${{ github.ref }}-${{ github.workflow }}
14+
cancel-in-progress: true
15+
16+
permissions:
17+
contents: read
18+
pull-requests: read
19+
20+
jobs:
21+
commitlint:
22+
name: Lint commit messages
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@v4
27+
with:
28+
fetch-depth: 0
29+
30+
- name: Lint PR commits
31+
uses: wagoid/commitlint-github-action@v6
32+
with:
33+
configFile: commitlint.config.mjs
34+
35+
- name: Lint PR title
36+
if: github.event_name == 'pull_request'
37+
env:
38+
PR_TITLE: ${{ github.event.pull_request.title }}
39+
run: |
40+
set -euo pipefail
41+
# Install both @commitlint/cli AND the extended shareable config so the
42+
# `extends: ['@commitlint/config-conventional']` in commitlint.config.mjs resolves.
43+
echo "$PR_TITLE" | npx --yes -p @commitlint/cli -p @commitlint/config-conventional \
44+
commitlint --config commitlint.config.mjs
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Ensure the project version is incremented in every PR before it can merge to the default branch.
2+
# Looks for BOTH package.json and Cargo.toml:
3+
# - only package.json present -> its version must increase vs the base branch
4+
# - only Cargo.toml present -> its version must increase vs the base branch
5+
# - BOTH present -> both must increase AND equal each other on this branch
6+
# - neither present -> nothing to enforce (passes)
7+
# Version files added new on this branch (absent on base) pass. Modeled on Chia-Network/cadt.
8+
name: Check Version Increment
9+
10+
on:
11+
pull_request:
12+
branches:
13+
- main
14+
15+
# Least privilege: the version check only reads the checked-out branches (GHAS/CodeQL requirement).
16+
permissions:
17+
contents: read
18+
19+
concurrency:
20+
group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}
21+
cancel-in-progress: true
22+
23+
jobs:
24+
check-version:
25+
name: Check version increment
26+
runs-on: ubuntu-latest
27+
steps:
28+
- name: Checkout PR branch
29+
uses: actions/checkout@v4
30+
with:
31+
path: branch-repo
32+
33+
- name: Checkout base branch
34+
uses: actions/checkout@v4
35+
with:
36+
ref: main
37+
path: base-repo
38+
39+
- name: Check version increment
40+
shell: bash
41+
run: |
42+
set -euo pipefail
43+
44+
# Extract a version from package.json (.version) or Cargo.toml
45+
# ([package].version or [workspace.package].version). Empty string if
46+
# the file or the field is absent.
47+
pkg_version() {
48+
local f="$1/package.json"
49+
[ -f "$f" ] || { echo ""; return; }
50+
jq -r '.version // ""' "$f" 2>/dev/null || echo ""
51+
}
52+
# [package].version, or [workspace.package].version for workspace roots /
53+
# members that inherit ({ version.workspace = true }). Empty if absent.
54+
cargo_version() {
55+
local f="$1/Cargo.toml"
56+
[ -f "$f" ] || { echo ""; return; }
57+
python3 -c 'import sys,tomllib; d=tomllib.load(open(sys.argv[1],"rb")); v=d.get("package",{}).get("version") or d.get("workspace",{}).get("package",{}).get("version") or ""; print(v if isinstance(v,str) else "")' "$f" 2>/dev/null || echo ""
58+
}
59+
60+
# strictly-greater semver-ish compare using sort -V; true when $2 > $1
61+
greater() { [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1)" = "$2" ]; }
62+
63+
B=base-repo
64+
H=branch-repo
65+
has_pkg=false; has_cargo=false
66+
[ -f "$H/package.json" ] && has_pkg=true
67+
[ -f "$H/Cargo.toml" ] && has_cargo=true
68+
69+
if [ "$has_pkg" = false ] && [ "$has_cargo" = false ]; then
70+
echo "No package.json or Cargo.toml at repo root — no version gate to enforce."
71+
exit 0
72+
fi
73+
74+
fail=0
75+
76+
if [ "$has_pkg" = true ]; then
77+
bp=$(pkg_version "$B"); hp=$(pkg_version "$H")
78+
echo "package.json: base='$bp' head='$hp'"
79+
if [ -n "$bp" ]; then
80+
if ! greater "$bp" "$hp"; then
81+
echo "::error::package.json version must be incremented ($bp -> $hp) before merging."
82+
fail=1
83+
fi
84+
else
85+
echo "package.json is new on this branch (no base version) — OK."
86+
fi
87+
fi
88+
89+
if [ "$has_cargo" = true ]; then
90+
bc=$(cargo_version "$B"); hc=$(cargo_version "$H")
91+
echo "Cargo.toml: base='$bc' head='$hc'"
92+
if [ -n "$bc" ]; then
93+
if ! greater "$bc" "$hc"; then
94+
echo "::error::Cargo.toml version must be incremented ($bc -> $hc) before merging."
95+
fail=1
96+
fi
97+
else
98+
echo "Cargo.toml is new on this branch (no base version) — OK."
99+
fi
100+
fi
101+
102+
if [ "$has_pkg" = true ] && [ "$has_cargo" = true ]; then
103+
hp=$(pkg_version "$H"); hc=$(cargo_version "$H")
104+
if [ -n "$hp" ] && [ -n "$hc" ] && [ "$hp" != "$hc" ]; then
105+
echo "::error::package.json ($hp) and Cargo.toml ($hc) versions must match each other."
106+
fail=1
107+
fi
108+
fi
109+
110+
exit $fail

.github/workflows/publish.yml

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
name: Publish to crates.io
2+
3+
# Tag-driven release: pushing a version tag `vX.Y.Z` runs the gates, publishes to crates.io, and
4+
# cuts a GitHub Release. A normal push to `main` runs the gates only (see ci.yml) — it does NOT
5+
# publish. Mirrors the sibling DIG crates so the same org secrets apply.
6+
# dig-session's dependencies (dig-keystore, dig-identity, chia-bls, zeroize, thiserror) are all
7+
# crates.io-published versions — no git/path deps. Release-first ordering: dig-keystore and
8+
# dig-identity must be published (they are: v0.4.1 / v0.4.2) before dig-session publishes.
9+
10+
on:
11+
push:
12+
tags:
13+
- 'v*'
14+
workflow_dispatch:
15+
inputs:
16+
version:
17+
description: 'Version to publish (e.g., v0.4.0)'
18+
required: true
19+
type: string
20+
21+
permissions:
22+
contents: read
23+
24+
env:
25+
CARGO_TERM_COLOR: always
26+
27+
jobs:
28+
# No fmt/clippy/test/coverage re-run here: the PR that produced this tagged commit already went
29+
# through ci.yml's full gate set before it was allowed to merge. This workflow is build + package +
30+
# publish only.
31+
publish:
32+
name: Publish to crates.io
33+
runs-on: ubuntu-latest
34+
steps:
35+
- name: Checkout code
36+
uses: actions/checkout@v4
37+
38+
- name: Install Rust toolchain
39+
uses: dtolnay/rust-toolchain@stable
40+
with:
41+
toolchain: stable
42+
43+
- name: Cache dependencies
44+
uses: Swatinem/rust-cache@v2
45+
46+
- name: Verify the package builds
47+
run: cargo build --release
48+
49+
- name: Verify the package can be packaged
50+
run: cargo package --locked
51+
52+
- name: Check CARGO_REGISTRY_TOKEN is set
53+
run: |
54+
if [ -z "${{ secrets.CARGO_REGISTRY_TOKEN }}" ]; then
55+
echo "CARGO_REGISTRY_TOKEN secret is not set in repository settings"
56+
exit 1
57+
fi
58+
59+
- name: Publish to crates.io
60+
run: cargo publish --locked --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
61+
env:
62+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
63+
64+
create-release:
65+
name: Create GitHub Release
66+
runs-on: ubuntu-latest
67+
needs: publish
68+
if: startsWith(github.ref, 'refs/tags/v')
69+
permissions:
70+
contents: write
71+
steps:
72+
- name: Checkout code
73+
uses: actions/checkout@v4
74+
75+
- name: Extract version from tag
76+
id: extract_version
77+
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT"
78+
79+
- name: Create GitHub Release
80+
uses: softprops/action-gh-release@v2
81+
with:
82+
tag_name: ${{ github.ref_name }}
83+
name: "dig-session v${{ steps.extract_version.outputs.VERSION }}"
84+
body: |
85+
## dig-session v${{ steps.extract_version.outputs.VERSION }}
86+
87+
DIG session/keystore layer: unlock identity signing keys and inject signing primitives.
88+
Composes dig-keystore (encrypted key storage) + dig-identity (canonical BLS identity
89+
derivation) behind one curated, custody-safe facade.
90+
91+
### Installation
92+
```toml
93+
[dependencies]
94+
dig-session = "${{ steps.extract_version.outputs.VERSION }}"
95+
```
96+
draft: false
97+
prerelease: false

0 commit comments

Comments
 (0)