From 11db5284a4936e5b1fbf116c504fea2dd9fec345 Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Mon, 20 Jul 2026 10:34:38 -0700 Subject: [PATCH 1/2] feat: initial dig-session facade (unlock/enroll/sign/inject) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce the DIG session/keystore layer: a custody-safe facade composing dig-keystore (encrypted storage) + dig-identity (canonical BLS derivation). - Session::unlock — generic load -> unlock -> SignerHandle - Session::enroll_identity — derive the canonical identity key once and store it under L1WalletBls (from_bytes round-trip), NOT BlsSigning (from_seed would re-derive a different key — dig_ecosystem #64/#57) - UnlockedIdentity — zeroizing, redacting Debug, no Clone; sign + inject a bare SigningFn primitive so consumers stay identity-agnostic (#908) No seal/decap (belongs to dig-message). SPEC.md is the normative contract. Full CI gate set + release/publish workflows from commit 1. Closes #1249 Co-Authored-By: Claude --- .github/workflows/ci.yml | 91 ++ .github/workflows/codeql.yml | 47 + .github/workflows/commitlint.yml | 44 + .../workflows/ensure-version-increment.yml | 110 ++ .github/workflows/publish.yml | 97 ++ .github/workflows/release.yml | 87 ++ .gitignore | 3 + CHANGELOG.md | 10 + Cargo.lock | 1307 +++++++++++++++++ Cargo.toml | 45 + README.md | 72 + SPEC.md | 116 ++ cliff.toml | 52 + commitlint.config.mjs | 19 + src/error.rs | 27 + src/lib.rs | 77 + src/session.rs | 89 ++ src/unlocked.rs | 92 ++ tests/facade.rs | 154 ++ 19 files changed, 2539 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/commitlint.yml create mode 100644 .github/workflows/ensure-version-increment.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 SPEC.md create mode 100644 cliff.toml create mode 100644 commitlint.config.mjs create mode 100644 src/error.rs create mode 100644 src/lib.rs create mode 100644 src/session.rs create mode 100644 src/unlocked.rs create mode 100644 tests/facade.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..43a043a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,91 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +# Least privilege: CI only reads the checked-out repo (GHAS/CodeQL requirement). +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Test Suite + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + components: rustfmt, clippy + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + + - name: Install cargo-nextest + uses: taiki-e/install-action@nextest + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Check clippy + run: cargo clippy --all-targets --all-features -- -D warnings + + # nextest runs each test as its own process and natively detects+reports + # flaky tests (failed-then-passed-on-retry) instead of silently hiding + # them. `--test-threads=1` mirrors the prior serial run (some integration + # tests assume serial execution). + - name: Run tests (nextest, flaky-aware) + run: cargo nextest run --all-features --retries 2 --test-threads=1 + + # nextest does NOT run doctests, so the headline `lib.rs` example is ungated + # by the step above. Run it explicitly so a doc example can never rot. + - name: Run doctests + run: cargo test --doc --all-features + + - name: Check documentation + run: cargo doc --no-deps --all-features + + coverage: + name: Coverage (>=80% lines, gated) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + components: llvm-tools-preview + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + + - name: Install cargo-nextest + uses: taiki-e/install-action@nextest + + # Measure line coverage and FAIL the build if it drops below 80%. + # `cargo llvm-cov nextest` runs the flaky-aware nextest runner under + # coverage instrumentation so coverage is still collected (running + # `cargo nextest run` and `cargo llvm-cov` as separate steps collects + # zero coverage and always fails the gate). `--test-threads 1` mirrors + # the serial run used by the test suite (some integration tests assume + # serial execution) — passed as a native nextest flag, NOT after `--` + # (nextest rejects `--test-threads=1` forwarded to the test binary). A + # summary report is printed; the gate is enforced by + # `--fail-under-lines 80`. + - name: Run coverage with 80% line gate + run: cargo llvm-cov nextest --all-features --workspace --fail-under-lines 80 --retries 2 --test-threads 1 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..079e2e2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,47 @@ +# CodeQL / GitHub Advanced Security static analysis for the Rust crate. +# Runs on every PR + push to main + a weekly schedule so newly-published CodeQL +# rules catch regressions in already-merged code. +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: '27 4 * * 1' + workflow_dispatch: + +# Least privilege: CodeQL needs to read the code and write security events. +permissions: + contents: read + security-events: write + +jobs: + analyze: + name: Analyze (rust) + runs-on: ubuntu-latest + 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: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: rust + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:rust" diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 0000000..b3f042e --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,44 @@ +# Enforce Conventional Commits on every PR to the default branch (and the PR title). +# Reads commitlint.config.mjs at the repo root (extends @commitlint/config-conventional). +# No local install needed — the action bundles commitlint. See CLAUDE.md §3.2. +name: Commitlint + +on: + pull_request: + branches: + - main + types: [opened, edited, synchronize, reopened] + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: read + +jobs: + commitlint: + name: Lint commit messages + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Lint PR commits + uses: wagoid/commitlint-github-action@v6 + with: + configFile: commitlint.config.mjs + + - name: Lint PR title + if: github.event_name == 'pull_request' + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + set -euo pipefail + # Install both @commitlint/cli AND the extended shareable config so the + # `extends: ['@commitlint/config-conventional']` in commitlint.config.mjs resolves. + echo "$PR_TITLE" | npx --yes -p @commitlint/cli -p @commitlint/config-conventional \ + commitlint --config commitlint.config.mjs diff --git a/.github/workflows/ensure-version-increment.yml b/.github/workflows/ensure-version-increment.yml new file mode 100644 index 0000000..09c2ae3 --- /dev/null +++ b/.github/workflows/ensure-version-increment.yml @@ -0,0 +1,110 @@ +# Ensure the project version is incremented in every PR before it can merge to the default branch. +# Looks for BOTH package.json and Cargo.toml: +# - only package.json present -> its version must increase vs the base branch +# - only Cargo.toml present -> its version must increase vs the base branch +# - BOTH present -> both must increase AND equal each other on this branch +# - neither present -> nothing to enforce (passes) +# Version files added new on this branch (absent on base) pass. Modeled on Chia-Network/cadt. +name: Check Version Increment + +on: + pull_request: + branches: + - main + +# Least privilege: the version check only reads the checked-out branches (GHAS/CodeQL requirement). +permissions: + contents: read + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }} + cancel-in-progress: true + +jobs: + check-version: + name: Check version increment + runs-on: ubuntu-latest + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + path: branch-repo + + - name: Checkout base branch + uses: actions/checkout@v4 + with: + ref: main + path: base-repo + + - name: Check version increment + shell: bash + run: | + set -euo pipefail + + # Extract a version from package.json (.version) or Cargo.toml + # ([package].version or [workspace.package].version). Empty string if + # the file or the field is absent. + pkg_version() { + local f="$1/package.json" + [ -f "$f" ] || { echo ""; return; } + jq -r '.version // ""' "$f" 2>/dev/null || echo "" + } + # [package].version, or [workspace.package].version for workspace roots / + # members that inherit ({ version.workspace = true }). Empty if absent. + cargo_version() { + local f="$1/Cargo.toml" + [ -f "$f" ] || { echo ""; return; } + 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 "" + } + + # strictly-greater semver-ish compare using sort -V; true when $2 > $1 + greater() { [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1)" = "$2" ]; } + + B=base-repo + H=branch-repo + has_pkg=false; has_cargo=false + [ -f "$H/package.json" ] && has_pkg=true + [ -f "$H/Cargo.toml" ] && has_cargo=true + + if [ "$has_pkg" = false ] && [ "$has_cargo" = false ]; then + echo "No package.json or Cargo.toml at repo root — no version gate to enforce." + exit 0 + fi + + fail=0 + + if [ "$has_pkg" = true ]; then + bp=$(pkg_version "$B"); hp=$(pkg_version "$H") + echo "package.json: base='$bp' head='$hp'" + if [ -n "$bp" ]; then + if ! greater "$bp" "$hp"; then + echo "::error::package.json version must be incremented ($bp -> $hp) before merging." + fail=1 + fi + else + echo "package.json is new on this branch (no base version) — OK." + fi + fi + + if [ "$has_cargo" = true ]; then + bc=$(cargo_version "$B"); hc=$(cargo_version "$H") + echo "Cargo.toml: base='$bc' head='$hc'" + if [ -n "$bc" ]; then + if ! greater "$bc" "$hc"; then + echo "::error::Cargo.toml version must be incremented ($bc -> $hc) before merging." + fail=1 + fi + else + echo "Cargo.toml is new on this branch (no base version) — OK." + fi + fi + + if [ "$has_pkg" = true ] && [ "$has_cargo" = true ]; then + hp=$(pkg_version "$H"); hc=$(cargo_version "$H") + if [ -n "$hp" ] && [ -n "$hc" ] && [ "$hp" != "$hc" ]; then + echo "::error::package.json ($hp) and Cargo.toml ($hc) versions must match each other." + fail=1 + fi + fi + + exit $fail diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..fd68c78 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,97 @@ +name: Publish to crates.io + +# Tag-driven release: pushing a version tag `vX.Y.Z` runs the gates, publishes to crates.io, and +# cuts a GitHub Release. A normal push to `main` runs the gates only (see ci.yml) — it does NOT +# publish. Mirrors the sibling DIG crates so the same org secrets apply. +# dig-session's dependencies (dig-keystore, dig-identity, chia-bls, zeroize, thiserror) are all +# crates.io-published versions — no git/path deps. Release-first ordering: dig-keystore and +# dig-identity must be published (they are: v0.4.1 / v0.4.2) before dig-session publishes. + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g., v0.4.0)' + required: true + type: string + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + +jobs: + # No fmt/clippy/test/coverage re-run here: the PR that produced this tagged commit already went + # through ci.yml's full gate set before it was allowed to merge. This workflow is build + package + + # publish only. + publish: + name: Publish to crates.io + runs-on: ubuntu-latest + 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 the package builds + run: cargo build --release + + - name: Verify the package can be packaged + run: cargo package --locked + + - name: Check CARGO_REGISTRY_TOKEN is set + 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 --locked --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') + permissions: + contents: write + 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: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: "dig-session v${{ steps.extract_version.outputs.VERSION }}" + body: | + ## dig-session v${{ steps.extract_version.outputs.VERSION }} + + DIG session/keystore layer: unlock identity signing keys and inject signing primitives. + Composes dig-keystore (encrypted key storage) + dig-identity (canonical BLS identity + derivation) behind one curated, custody-safe facade. + + ### Installation + ```toml + [dependencies] + dig-session = "${{ steps.extract_version.outputs.VERSION }}" + ``` + draft: false + prerelease: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..dda7eb8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,87 @@ +# On merge to the default branch: regenerate CHANGELOG.md from Conventional Commits (git-cliff), +# commit it to the default branch, THEN tag that commit vX.Y.Z and push the tag. The changelog is +# therefore INCLUDED in the tag. The pushed tag triggers this repo's deploy/release workflow +# (wired `on: push: tags: ['v*']`). +# +# The tag is pushed with RELEASE_TOKEN (a PAT) — a tag pushed by the default GITHUB_TOKEN does NOT +# trigger downstream workflows (GitHub anti-recursion), which would break deploy-on-tag. The same +# PAT pushes the changelog commit (its identity must be allowed past branch protection — +# enforce_admins is off; the PAT is an org-admin/owner). See CLAUDE.md §3.6. +# +# Idempotent + loop-safe: no-op if the version's tag already exists; skips its own changelog commit. +name: Release + +on: + push: + branches: + - main + # Manual trigger to dodge the new-crate first-release race: GitHub does NOT + # fire a `push: main` workflow on the squash-merge that first ADDS this file, + # so the very first release must be kicked with a workflow_dispatch (or an + # empty commit). See dig_ecosystem "new-crate first-release race". + workflow_dispatch: + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: write + +jobs: + release: + name: Changelog + tag + if: ${{ !startsWith(github.event.head_commit.message, 'chore(release):') }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }} + + - name: Resolve version + skip if already tagged + id: ver + shell: bash + run: | + set -euo pipefail + pkg() { [ -f package.json ] && jq -r '.version // ""' package.json 2>/dev/null || echo ""; } + crg() { + [ -f Cargo.toml ] || { echo ""; return; } + python3 -c 'import tomllib; d=tomllib.load(open("Cargo.toml","rb")); v=d.get("package",{}).get("version") or d.get("workspace",{}).get("package",{}).get("version") or ""; print(v if isinstance(v,str) else "")' 2>/dev/null || echo "" + } + PV="$(pkg)"; CV="$(crg)"; VER="${PV:-$CV}" + if [ -z "$VER" ]; then echo "skip=true" >>"$GITHUB_OUTPUT"; echo "No version file — nothing to release."; exit 0; fi + TAG="v$VER" + if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null \ + || git ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then + echo "skip=true" >>"$GITHUB_OUTPUT"; echo "Tag $TAG already exists — no-op."; exit 0 + fi + echo "skip=false" >>"$GITHUB_OUTPUT" + echo "tag=$TAG" >>"$GITHUB_OUTPUT" + echo "Releasing $TAG" + + - name: Install git-cliff + if: steps.ver.outputs.skip == 'false' + uses: taiki-e/install-action@v2 + with: + tool: git-cliff + + - name: Generate changelog + if: steps.ver.outputs.skip == 'false' + run: git-cliff --config cliff.toml --tag "${{ steps.ver.outputs.tag }}" --output CHANGELOG.md + + - name: Commit changelog, tag, push + if: steps.ver.outputs.skip == 'false' + shell: bash + run: | + set -euo pipefail + git config user.name "dig-release-bot" + git config user.email "release-bot@users.noreply.github.com" + git add CHANGELOG.md + # `chore(release):` prefix so this workflow skips its own push (loop guard above). + git commit -m "chore(release): ${{ steps.ver.outputs.tag }}" || echo "changelog unchanged" + git tag -a "${{ steps.ver.outputs.tag }}" -m "Release ${{ steps.ver.outputs.tag }}" + git push origin "HEAD:main" + git push origin "${{ steps.ver.outputs.tag }}" + echo "Pushed changelog commit + ${{ steps.ver.outputs.tag }} — deploy-on-tag will fire." diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7141ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +*.pdb diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..28cede2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project are documented here. This file is +regenerated from Conventional Commits by [git-cliff](https://git-cliff.org) on +release; entries below the first release are a placeholder. + +## [Unreleased] + +- Initial crate: `Session::unlock` / `Session::enroll_identity`, + `UnlockedIdentity`, and the injected `SigningFn` signing primitive. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6d9e29b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1307 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if 1.0.4", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "blake2b-rs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89a8565807f21b913288968e391819e7f9b2f0f46c7b89549c051cccf3a2771" +dependencies = [ + "cc", + "cty", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chia-bls" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86eaaa00a83a7511e8eb74ccb1ebe4358d927f43e06d45da5350e19ef2df1ca" +dependencies = [ + "blst", + "chia-sha2 0.22.0", + "chia-traits 0.22.0", + "hex", + "hkdf", + "linked-hash-map", + "sha2", + "thiserror 1.0.69", +] + +[[package]] +name = "chia-bls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc9ac7e90ae816e814dc26fb332615a5449d1d26e965eb7ad068ae530f9c3c7b" +dependencies = [ + "blst", + "chia-sha2 0.26.0", + "chia-traits 0.26.0", + "hex", + "hkdf", + "linked-hash-map", + "sha2", + "thiserror 1.0.69", +] + +[[package]] +name = "chia-protocol" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9256179e4c912313532d7a47a50f67f6128313ee86a3798e54050afb73d6042" +dependencies = [ + "chia-bls 0.26.0", + "chia-sha2 0.26.0", + "chia-traits 0.26.0", + "chia_streamable_macro 0.26.0", + "clvm-traits", + "clvm-utils", + "clvmr", + "hex", +] + +[[package]] +name = "chia-sdk-utils" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "738bc2a53dcece78184aca4de617df666e984af0c86560131213039889d927fc" +dependencies = [ + "bech32", + "chia-protocol", + "indexmap", + "rand", + "rand_chacha", + "thiserror 2.0.19", +] + +[[package]] +name = "chia-sha2" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b37c5362dfb1c9449902139e94a91bbd8d3773c13939972fd88e4f14c4f9d" +dependencies = [ + "sha2", +] + +[[package]] +name = "chia-sha2" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3ab374b1f248d87516c34e4c128a91d2086e76a46bb44e386c96a71ea39129" +dependencies = [ + "sha2", +] + +[[package]] +name = "chia-traits" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340ad823ef953d2ab9f937deae99b04bab73c0bf1a6aea1353331a5f875192e0" +dependencies = [ + "chia-sha2 0.22.0", + "chia_streamable_macro 0.22.0", + "thiserror 1.0.69", +] + +[[package]] +name = "chia-traits" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "440a88dfa8f685a87c4bae5f0b12c7bec1466ca1ff44dfc6b09acc605f7848fe" +dependencies = [ + "chia-sha2 0.26.0", + "chia_streamable_macro 0.26.0", + "thiserror 1.0.69", +] + +[[package]] +name = "chia_streamable_macro" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed16ee21f14ed878cfc23b0354c8c6703190a5565d03625ea44324a3f21717f1" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "chia_streamable_macro" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068278c78a0c53786012f6330a088f3f6ffe83bd39cb8f21d308eeec2f43a3dc" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clvm-derive" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ded863cb5482335498872e91989487283f012adb7ca4ddf54ad7c6995058db" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "clvm-traits" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469c44226c83de37509415482c89c00763f58de198844c34d2d62161b087d0ce" +dependencies = [ + "clvm-derive", + "clvmr", + "num-bigint", + "thiserror 1.0.69", +] + +[[package]] +name = "clvm-utils" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5646cd8d02f9c55c76ef6ff7598df54e6c8361f27fe8c9122e3ae9ef55935fcd" +dependencies = [ + "chia-sha2 0.26.0", + "clvm-traits", + "clvmr", + "hex", +] + +[[package]] +name = "clvmr" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6b7142674607f3a213addaf261bd70b07537297e460a9cc6a389068a62934f" +dependencies = [ + "bitvec", + "bumpalo", + "chia-bls 0.22.0", + "chia-sha2 0.22.0", + "hex", + "hex-literal", + "k256", + "lazy_static", + "num-bigint", + "num-integer", + "num-traits", + "p256", + "rand", + "sha1", + "sha3", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if 1.0.4", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "dig-identity" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b61a73d7c4ecedfb3364e0f265946ef696dcec84ab58472873b097426691b19" +dependencies = [ + "blst", + "chia-bls 0.26.0", + "chia-protocol", + "chia-sdk-utils", + "sha2", + "sparse-merkle-tree", + "thiserror 2.0.19", +] + +[[package]] +name = "dig-keystore" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63f0cb19c24be82aede131ea0b087518d9158e001cef875ebc4eee0f60e3c555" +dependencies = [ + "aes-gcm", + "argon2", + "chia-bls 0.26.0", + "crc32fast", + "hex", + "parking_lot", + "rand_chacha", + "rand_core", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "dig-session" +version = "0.1.0" +dependencies = [ + "chia-bls 0.26.0", + "dig-identity", + "dig-keystore", + "tempfile", + "thiserror 1.0.69", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "r-efi", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if 1.0.4", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if 1.0.4", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "sparse-merkle-tree" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8851f6c92491ebe5528eabc1244292175a739eb0162974f9f9670a7dc748748b" +dependencies = [ + "blake2b-rs", + "cc", + "cfg-if 0.1.10", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.2", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..10320e2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "dig-session" +version = "0.1.0" +edition = "2021" +rust-version = "1.75" +description = "DIG session/keystore layer: unlock identity signing keys and inject signing primitives. Composes dig-keystore (encrypted key storage) + dig-identity (canonical BLS identity derivation) behind one curated, custody-safe facade." +license = "GPL-2.0-only" +repository = "https://github.com/DIG-Network/dig-session" +homepage = "https://github.com/DIG-Network/dig-session" +documentation = "https://docs.rs/dig-session" +readme = "README.md" +keywords = ["dig-network", "session", "keystore", "identity", "bls"] +categories = ["cryptography::cryptocurrencies"] +# Ship only the library surface to crates.io. Explicit allow-list because the +# repo also tracks CI workflow files and build artefacts that must NOT end up in +# the published tarball. +include = [ + "src/**/*.rs", + "tests/**/*.rs", + "SPEC.md", + "Cargo.toml", + "Cargo.lock", + "README.md", + "LICENSE*", +] + +[dependencies] +# Encrypted, per-scheme secret-key storage (unlock -> SignerHandle). crates.io only. +dig-keystore = "0.4" +# Canonical DIG identity BLS derivation (master_secret_key_from_seed + +# derive_identity_sk at m/12381'/8444'/9'/0'). crates.io only. +dig-identity = { version = "0.4", features = ["bls"] } +# Canonical Chia BLS12-381 primitives (SecretKey / PublicKey / Signature). +chia-bls = "0.26" +# Memory hygiene: zeroize derived secrets on drop. +zeroize = { version = "1.7", features = ["derive"] } +thiserror = "1" + +[dev-dependencies] +dig-keystore = { version = "0.4", features = ["testing"] } +tempfile = "3" + +[lints.rust] +unsafe_code = "forbid" +missing_docs = "warn" diff --git a/README.md b/README.md new file mode 100644 index 0000000..92278b5 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# dig-session + +The DIG **session / keystore layer**: a small, custody-safe facade that turns +stored, encrypted key material into a live signer and injects a bare signing +primitive into downstream consumers. + +It composes two lower-level crates and adds **no cryptography of its own**: + +- [`dig-keystore`](https://crates.io/crates/dig-keystore) — encrypted, + per-scheme secret-key storage (`Keystore::::load -> unlock -> SignerHandle`). +- [`dig-identity`](https://crates.io/crates/dig-identity) — the canonical DIG BLS + identity derivation (`m/12381'/8444'/9'/0'`). + +## Install + +```toml +[dependencies] +dig-session = "0.1" +``` + +## Use + +```rust,no_run +use std::sync::Arc; +use dig_session::{Session, FileBackend, BackendKey, Password}; + +# fn main() -> dig_session::Result<()> { +let backend = Arc::new(FileBackend::new("/var/lib/dig/keys")); + +// Enroll a new identity from BIP-39 seed bytes (derives the canonical +// dig-identity signing key and stores it encrypted). +let identity = Session::enroll_identity( + backend.clone(), + BackendKey::new("identity"), + Password::from("correct horse battery staple"), + b"seed bytes", +)?; + +// Sign directly... +let _sig = identity.sign(b"message"); + +// ...or hand a downstream a bare signing primitive — it never sees a session type. +let sign = identity.signing_fn(); +let _sig = sign(b"message"); + +// Reopen later. +let identity = Session::unlock::( + backend, + BackendKey::new("identity"), + Password::from("correct horse battery staple"), +)?; +# let _ = identity; +# Ok(()) +# } +``` + +## Design notes + +- **No seal / decap.** Recipient message encryption belongs to `dig-message`, + not here. +- **Identity keys are stored with `L1WalletBls`, not `BlsSigning`.** The identity + key is already derived by dig-identity; storage must round-trip it via + `from_bytes`. `BlsSigning` would re-derive via `from_seed` and yield a + different key (the dig_ecosystem #64/#57 pitfall). +- **Custody-safe.** `UnlockedIdentity` zeroizes its secret on drop, never + `Debug`-prints key material, and must never cross an IPC boundary. + +See [`SPEC.md`](./SPEC.md) for the normative contract. + +## License + +GPL-2.0-only. diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..e4eb2c2 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,116 @@ +# dig-session — normative specification + +`dig-session` is the DIG **session / keystore layer**. It composes +[`dig-keystore`] (encrypted secret-key storage) and [`dig-identity`] (canonical +BLS identity derivation) into one curated, custody-safe facade for turning +stored key material into a live signer and injecting a bare signing primitive +into downstream consumers. It performs **no cryptography of its own** — every +cryptographic operation is delegated to those two crates. + +This document is normative: an independent reimplementation MUST satisfy every +MUST/MUST NOT below. + +## 1. Scope + +- **In scope (first cut):** unlock an existing key, enroll a new identity, sign, + and inject a signing primitive. +- **Out of scope:** recipient message encryption (seal / decap). That + composition lives in `dig-message` (same crate level). Implementations of + `dig-session` MUST NOT add seal/decap; doing so would duplicate a cross-repo + contract and invite byte-drift. + +## 2. Dependencies and layering + +- `dig-session` MUST depend only on crates at a strictly lower level: + `dig-keystore` and `dig-identity` (both level 00 foundation), plus `chia-bls`, + `zeroize`, and `thiserror`. It MUST NOT depend on any level-10 crate. +- Dependencies MUST be crates.io versions, never `git = …` deps. +- Required published minimums: `dig-keystore >= 0.4`, `dig-identity >= 0.4`. + +## 3. Public API surface + +The crate exposes exactly the following curated facade (plus re-exports of the +storage/scheme types a caller needs, so a consumer depends on JUST +`dig-session`): + +### 3.1 `Session` + +A stateless namespace. All methods are associated functions. + +- `Session::unlock::(backend, path, password) -> Result>` + - MUST load the keystore file at `path` via `dig_keystore::Keystore::::load` + and unlock it with `password` via `Keystore::unlock`, returning the + resulting `SignerHandle` wrapped in an `UnlockedIdentity`. + - MUST be generic over the scheme `K`. `L1WalletBls` is used for stored, + already-derived keys (identity signing key, wallet keys); `BlsSigning` for + seed-derived validator keys. + - MUST surface a scheme mismatch, wrong password, missing file, or tampered + ciphertext as `SessionError::Keystore`. + +- `Session::enroll_identity(backend, path, password, seed) -> Result>` + - MUST reject empty `seed` with `SessionError::EmptySeed`. + - MUST derive the identity signing key EXACTLY ONCE, as + `derive_identity_sk(master_secret_key_from_seed(seed))` — i.e. the hardened + dig-identity path `m/12381'/8444'/9'/0'`. + - MUST persist the derived secret key's canonical bytes + (`chia_bls::SecretKey::to_bytes`) under the **`L1WalletBls`** scheme via + `dig_keystore::Keystore::create`, so a later `unlock` reconstructs the key + byte-identically via `chia_bls::SecretKey::from_bytes`. + - MUST NOT store the derived key under `BlsSigning`. `BlsSigning` treats its + stored bytes as a *seed* and re-derives via `chia_bls::SecretKey::from_seed` + on unlock, which would produce a DIFFERENT key (dig_ecosystem #64/#57) whose + public key does not match the DID-anchored identity key. + - The returned identity's public key MUST equal + `dig_identity::public_key_bytes(derive_identity_sk(master_secret_key_from_seed(seed)))`. + +### 3.2 `UnlockedIdentity` + +A live, in-memory identity holding a decrypted `SignerHandle`. + +- `public_key(&self) -> &K::PublicKey` — the identity's public key. +- `sign(&self, msg: &[u8]) -> K::Signature` — sign a message. +- `signing_fn(&self) -> SigningFn` — a standalone signing primitive owning + its own zeroizing copy of the secret; MUST remain usable after the handle is + dropped. +- `inject_into(&self, consumer: impl FnOnce(SigningFn) -> T) -> T` — hand a + consumer ONLY a `SigningFn` primitive; the consumer's API MUST NOT mention any + `dig-session` or `dig-identity` type. This is what keeps a downstream (e.g. + `dig-wallet-backend`) identity-agnostic (dig_ecosystem #908). + +### 3.3 `SigningFn` + +`Arc K::Signature + Send + Sync>` — the injected primitive. + +### 3.4 `SessionError` / `Result` + +- `SessionError::Keystore(dig_keystore::KeystoreError)` — transparent wrap. +- `SessionError::EmptySeed` — enrollment given empty seed material. + +## 4. Custody invariants (MUST) + +- **Secret zeroization.** An `UnlockedIdentity` holds its secret inside the + wrapped `SignerHandle`'s `Zeroizing` buffer; the secret MUST be wiped when the + handle (and any injected `SigningFn` copy) is dropped. +- **No secret in debug output.** `UnlockedIdentity` MUST NOT derive `Debug`; its + `Debug` impl MUST redact the secret. It MUST NOT implement `Clone`. +- **No IPC crossing.** An `UnlockedIdentity` MUST NOT cross an IPC boundary; it + belongs solely to the user-app process that owns the identity (dig_ecosystem + #908). Downstreams receive a `SigningFn`, never the handle. +- **No unsafe.** The crate MUST forbid `unsafe` code (`#![forbid(unsafe_code)]`). + +## 5. Conformance tests + +An implementation MUST ship tests proving: + +- `enroll_identity` then `unlock` reconstruct the same public key. (C-1) +- The enrolled public key equals the dig-identity canonical key — the regression + guarding against a revert to `BlsSigning`/`from_seed`. (C-2) +- A produced signature verifies against the public key via `chia_bls::verify`. (C-3) +- An injected `SigningFn` still signs after the handle is dropped. (C-4) +- Unlock with the wrong password fails with `SessionError::Keystore`. (C-5) +- Enroll with empty seed fails with `SessionError::EmptySeed`. (C-6) +- `unlock` works generically for `BlsSigning`. (C-7) +- `Debug` output contains no secret material. (C-8) + +[`dig-keystore`]: https://crates.io/crates/dig-keystore +[`dig-identity`]: https://crates.io/crates/dig-identity diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..dbe3a58 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,52 @@ +# git-cliff changelog config — builds CHANGELOG.md from Conventional Commits. +# Uniform across every DIG repo (Rust + JS/TS). Consumed by .github/workflows/release.yml, +# which regenerates the changelog on merge and commits it BEFORE tagging (so the changelog +# is included in the vX.Y.Z tag). See CLAUDE.md §3.6. +[changelog] +header = """ +# Changelog + +All notable changes to this project are documented here. +This project adheres to [Semantic Versioning](https://semver.org) and +[Conventional Commits](https://www.conventionalcommits.org).\n +""" +body = """ +{% if version %}\ +## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ +## [Unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} +### {{ group | striptags | trim | upper_first }} +{% for commit in commits %}\ +- {% if commit.scope %}**{{ commit.scope }}:** {% endif %}{{ commit.message | upper_first }}\ +{% endfor %} +{% endfor %}\n +""" +trim = true +footer = "" + +[git] +conventional_commits = true +filter_unconventional = true +split_commits = false +protect_breaking_commits = true +filter_commits = false +tag_pattern = "v[0-9]*" +topo_order = false +sort_commits = "oldest" +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactor" }, + { message = "^docs", group = "Documentation" }, + { message = "^test", group = "Testing" }, + { message = "^build", group = "Build" }, + { message = "^ci", group = "CI" }, + { message = "^style", group = "Styling" }, + { message = "^chore\\(release\\)", skip = true }, + { message = "^chore", group = "Chores" }, + { body = ".*security", group = "Security" }, + { message = "^revert", group = "Reverts" }, +] diff --git a/commitlint.config.mjs b/commitlint.config.mjs new file mode 100644 index 0000000..6ad466d --- /dev/null +++ b/commitlint.config.mjs @@ -0,0 +1,19 @@ +// Conventional Commits enforcement for this repo. +// Every commit + PR title must be `type(scope): summary`, where type is one of the +// allowed types below. A breaking change appends `!` (feat!: …) and/or a +// `BREAKING CHANGE:` body footer. The type drives the SemVer bump for the release +// (fix -> patch, feat -> minor, ! / BREAKING CHANGE -> major). +// Enforced in CI by .github/workflows/commitlint.yml (wagoid/commitlint-github-action). +export default { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [ + 2, + 'always', + ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert'], + ], + 'subject-case': [0], // allow any subject casing (proper nouns, scheme literals like chia://) + 'body-max-line-length': [0], // long bodies (URLs, logs) are fine + 'footer-max-line-length': [0], + }, +}; diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..1861669 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,27 @@ +//! Error type for the session facade. + +use thiserror::Error; + +/// Errors returned by [`crate::Session`] operations. +/// +/// Wraps the underlying [`dig_keystore`] failure verbatim (via `#[from]`) so a +/// caller can match on the concrete storage/crypto error without dig-session +/// inventing a parallel error taxonomy, plus the small number of failures that +/// are specific to session enrollment. +#[derive(Debug, Error)] +pub enum SessionError { + /// A key-storage or decryption failure surfaced by [`dig_keystore`] + /// (missing file, wrong password, tampered ciphertext, scheme mismatch, …). + #[error(transparent)] + Keystore(#[from] dig_keystore::KeystoreError), + + /// Enrollment was asked to derive an identity from empty seed material. + /// + /// A caller must supply real BIP-39 seed bytes; deriving an identity key + /// from an empty seed would silently produce a fixed, guessable key. + #[error("seed material must be non-empty")] + EmptySeed, +} + +/// Convenience alias for `Result`. +pub type Result = std::result::Result; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9f4de03 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,77 @@ +//! # dig-session +//! +//! The DIG **session / keystore layer**: a small, custody-safe facade that +//! turns stored, encrypted key material into a live signer and injects a bare +//! signing primitive into downstream consumers. +//! +//! It composes two lower-level crates and adds no cryptography of its own: +//! +//! - [`dig_keystore`] — encrypted, per-scheme secret-key storage +//! (`Keystore::::load -> unlock -> SignerHandle`, AES-256-GCM + Argon2id). +//! - [`dig_identity`] — the canonical DIG BLS identity derivation +//! (`master_secret_key_from_seed` then `derive_identity_sk` at the hardened +//! path `m/12381'/8444'/9'/0'`). +//! +//! ## What this crate deliberately does NOT do +//! +//! There is **no seal / decap** (recipient message encryption) here. That +//! composition belongs to `dig-message` (the same 10-primitives level); adding +//! it here would duplicate a cross-repo contract and invite byte-drift. This +//! crate's first cut is strictly **unlock / sign / inject**. +//! +//! ## Enrollment stores the derived key with `L1WalletBls`, not `BlsSigning` +//! +//! The identity signing key is a key that dig-identity has *already derived* +//! (via EIP-2333 hardened steps). To reconstruct it on unlock byte-identically, +//! storage must use a scheme that round-trips raw secret-key bytes through +//! `chia_bls::SecretKey::from_bytes` — that is [`L1WalletBls`]. +//! +//! [`BlsSigning`] instead treats its stored 32 bytes as a *seed* and runs them +//! through `chia_bls::SecretKey::from_seed` on every unlock. Storing an +//! already-derived key under `BlsSigning` would therefore re-derive a +//! *different* key (the documented dig_ecosystem #64 / #57 pitfall), and the +//! resulting public key would not match the DID-anchored identity key. So the +//! identity path uses `L1WalletBls`; a regression test asserts the unlocked +//! public key equals `dig_identity::public_key_bytes(derive_identity_sk(master))`. +//! +//! [`Session::unlock`] stays generic over the scheme, so `BlsSigning` is still +//! available for seed-derived validator keys. +//! +//! ## Example +//! +//! ```no_run +//! use std::sync::Arc; +//! use dig_session::{Session, FileBackend, BackendKey, Password}; +//! +//! # fn main() -> dig_session::Result<()> { +//! let backend = Arc::new(FileBackend::new("/var/lib/dig/keys")); +//! let identity = Session::enroll_identity( +//! backend, +//! BackendKey::new("identity"), +//! Password::from("correct horse battery staple"), +//! b"BIP-39 seed bytes go here", +//! )?; +//! +//! // Hand a downstream a bare signing primitive — it never sees a session type. +//! let sign = identity.signing_fn(); +//! let _sig = sign(b"message"); +//! # Ok(()) +//! # } +//! ``` + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +mod error; +mod session; +mod unlocked; + +pub use error::{Result, SessionError}; +pub use session::Session; +pub use unlocked::{SigningFn, UnlockedIdentity}; + +// Re-export the storage/scheme types a caller needs, so a consumer depends on +// JUST dig-session rather than reaching into dig-keystore directly. +pub use dig_keystore::bls::{PublicKey, SecretKey, Signature}; +pub use dig_keystore::scheme::{BlsSigning, KeyScheme, L1WalletBls}; +pub use dig_keystore::{BackendKey, FileBackend, KeychainBackend, Password}; diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000..a79dd6a --- /dev/null +++ b/src/session.rs @@ -0,0 +1,89 @@ +//! The [`Session`] facade: unlock an existing key, or enroll a new identity. + +use std::sync::Arc; + +use dig_identity::{derive_identity_sk, master_secret_key_from_seed}; +use dig_keystore::scheme::KeyScheme; +use dig_keystore::{BackendKey, KdfParams, KeychainBackend, Keystore, L1WalletBls, Password}; +use zeroize::Zeroizing; + +use crate::{Result, SessionError, UnlockedIdentity}; + +/// Entry point for turning stored, encrypted key material into a live signer. +/// +/// `Session` is a stateless namespace over the compose-only flow +/// `dig_keystore::Keystore::::load -> unlock -> SignerHandle`, plus the +/// enrollment path that derives the canonical dig-identity signing key and +/// persists it. It holds no state of its own; every method is associated. +pub struct Session; + +impl Session { + /// Unlock an existing keystore file into an [`UnlockedIdentity`]. + /// + /// Generic over the storage scheme `K`: use [`dig_keystore::L1WalletBls`] + /// for a stored, already-derived key (the identity signing key, wallet + /// keys) and [`dig_keystore::BlsSigning`] for a seed-derived validator key. + /// The scheme is verified against the file's magic on load, so unlocking a + /// file with the wrong scheme fails cleanly rather than yielding a bogus key. + /// + /// # Errors + /// + /// Returns [`SessionError::Keystore`] if the file is missing, the password + /// is wrong, the ciphertext is tampered, or the scheme does not match. + pub fn unlock( + backend: Arc, + path: BackendKey, + password: Password, + ) -> Result> { + let keystore = Keystore::::load(backend, path)?; + let signer = keystore.unlock(password)?; + Ok(UnlockedIdentity::new(signer)) + } + + /// Enroll a new identity: derive the canonical dig-identity BLS signing key + /// from `seed`, persist it encrypted under `password`, and return it + /// unlocked and ready to sign. + /// + /// The identity key is derived exactly once, via + /// [`dig_identity::master_secret_key_from_seed`] followed by + /// [`dig_identity::derive_identity_sk`] (the hardened path + /// `m/12381'/8444'/9'/0'`), and the resulting secret key's canonical bytes + /// are stored. See the module-level note in [`crate`] on why the storage + /// scheme is [`L1WalletBls`] (faithful `from_bytes` round-trip) rather than + /// `BlsSigning` (which would re-derive via `from_seed` and produce a + /// different key). + /// + /// # Errors + /// + /// Returns [`SessionError::EmptySeed`] if `seed` is empty, or + /// [`SessionError::Keystore`] if a file already exists at `path` or the + /// write fails. + pub fn enroll_identity( + backend: Arc, + path: BackendKey, + password: Password, + seed: &[u8], + ) -> Result> { + if seed.is_empty() { + return Err(SessionError::EmptySeed); + } + + // Derive the canonical identity signing key ONCE. + let master = master_secret_key_from_seed(seed); + let identity_sk = derive_identity_sk(&master); + let secret = Zeroizing::new(identity_sk.to_bytes().to_vec()); + + // Persist the already-derived key. `unlock` needs the password again, + // and `create` consumes it, so clone before the move. + let unlock_password = Password::new(password.as_bytes()); + let keystore = Keystore::::create( + backend, + path, + password, + Some(secret), + KdfParams::DEFAULT, + )?; + let signer = keystore.unlock(unlock_password)?; + Ok(UnlockedIdentity::new(signer)) + } +} diff --git a/src/unlocked.rs b/src/unlocked.rs new file mode 100644 index 0000000..e8920f0 --- /dev/null +++ b/src/unlocked.rs @@ -0,0 +1,92 @@ +//! The unlocked-key handle and the signing primitive it injects. + +use std::sync::Arc; + +use dig_keystore::scheme::KeyScheme; +use dig_keystore::SignerHandle; + +/// A scheme-parameterized signing primitive: a plain callable that maps a +/// message to a signature and carries no dig-session or dig-identity type. +/// +/// This is the shape [`UnlockedIdentity::inject_into`] hands to a consumer, so +/// the consumer's API surface mentions only `&[u8]` in and `K::Signature` +/// (a `chia_bls::Signature` for the BLS schemes) out — never a session or +/// identity type. That is what keeps a consumer such as `dig-wallet-backend` +/// identity-agnostic (see dig_ecosystem #908). +pub type SigningFn = Arc ::Signature + Send + Sync>; + +/// A live, in-memory identity whose secret key has been decrypted and is ready +/// to sign. +/// +/// Obtained from [`crate::Session::unlock`] or [`crate::Session::enroll_identity`]. +/// The secret bytes live inside the wrapped [`SignerHandle`], which stores them +/// in a `Zeroizing` buffer and wipes them when this value is dropped — so the +/// decrypted key never lingers in freed memory. The type deliberately does not +/// implement `Clone` and its `Debug` impl redacts the secret, so an +/// `UnlockedIdentity` cannot be duplicated into a log line or a debug dump. +/// +/// # Boundaries +/// +/// An `UnlockedIdentity` must never cross an IPC boundary: it holds raw key +/// material and belongs solely to the user-app process that owns the identity +/// (dig_ecosystem #908). Hand a downstream a [`SigningFn`] via +/// [`inject_into`](Self::inject_into) instead of the handle itself. +pub struct UnlockedIdentity { + signer: SignerHandle, + public_key: K::PublicKey, +} + +impl UnlockedIdentity { + /// Wrap a freshly unlocked [`SignerHandle`], caching its public key. + pub(crate) fn new(signer: SignerHandle) -> Self { + let public_key = signer.public_key().clone(); + Self { signer, public_key } + } + + /// The public key of this identity. + /// + /// For the identity scheme this is byte-identical to + /// `dig_identity::public_key_bytes(derive_identity_sk(master))`, i.e. the + /// key anchored in the DID profile — so signatures produced by this handle + /// verify against the published identity. + pub fn public_key(&self) -> &K::PublicKey { + &self.public_key + } + + /// Sign `msg` with the unlocked secret key. + pub fn sign(&self, msg: &[u8]) -> K::Signature { + self.signer.sign(msg) + } + + /// Produce a standalone [`SigningFn`] primitive that signs with this + /// identity's key. + /// + /// The returned closure owns its own zeroizing copy of the secret, so it + /// keeps working after this handle is dropped and wipes its copy when the + /// closure itself is dropped. + pub fn signing_fn(&self) -> SigningFn { + let signer = self.signer.clone(); + Arc::new(move |msg: &[u8]| signer.sign(msg)) + } + + /// Inject this identity's signing capability into a consumer as a bare + /// [`SigningFn`] primitive, returning whatever the consumer builds from it. + /// + /// The consumer receives only a callable — never an `UnlockedIdentity`, + /// a `SignerHandle`, or any identity type — which is how a downstream stays + /// identity-agnostic while still being able to sign. + pub fn inject_into(&self, consumer: impl FnOnce(SigningFn) -> T) -> T { + consumer(self.signing_fn()) + } +} + +/// Redacting `Debug`: shows the type name only, never the secret (or even the +/// public key, to avoid accidental correlation in logs). +impl core::fmt::Debug for UnlockedIdentity { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("UnlockedIdentity") + .field("scheme", &K::NAME) + .field("secret", &"") + .finish() + } +} diff --git a/tests/facade.rs b/tests/facade.rs new file mode 100644 index 0000000..b761105 --- /dev/null +++ b/tests/facade.rs @@ -0,0 +1,154 @@ +//! Integration tests for the dig-session facade, exercised against an +//! in-memory keystore backend. +//! +//! These double as behaviour documentation for the unlock / enroll / inject +//! flow and pin the custody-critical property that enrollment reproduces the +//! canonical dig-identity key on unlock. + +use std::sync::Arc; + +use dig_identity::{derive_identity_sk, master_secret_key_from_seed, public_key_bytes}; +use dig_keystore::{BackendKey, BlsSigning, KdfParams, Keystore, MemoryBackend, Password}; +use dig_session::{Session, SessionError}; + +const SEED: &[u8] = b"dig-session integration-test seed material"; +const PASSWORD: &str = "correct horse battery staple"; + +fn backend() -> Arc { + Arc::new(MemoryBackend::new()) +} + +#[test] +fn enroll_then_unlock_roundtrips_the_same_key() { + let be = backend(); + let path = BackendKey::new("identity"); + + let enrolled = + Session::enroll_identity(be.clone(), path.clone(), Password::from(PASSWORD), SEED).unwrap(); + let enrolled_pk = enrolled.public_key().to_bytes(); + drop(enrolled); + + let reopened = + Session::unlock::(be, path, Password::from(PASSWORD)).unwrap(); + assert_eq!( + reopened.public_key().to_bytes(), + enrolled_pk, + "unlock must reconstruct the same public key that enrollment produced" + ); +} + +#[test] +fn enrolled_public_key_matches_dig_identity_canonical() { + // The custody-critical regression: storing the derived key under + // `L1WalletBls` (from_bytes) must reproduce dig-identity's canonical key. + // If the storage scheme ever reverts to `BlsSigning` (from_seed), the key + // would be re-derived and this assertion fails. + let master = master_secret_key_from_seed(SEED); + let identity_sk = derive_identity_sk(&master); + let expected = public_key_bytes(&identity_sk); + + let enrolled = Session::enroll_identity( + backend(), + BackendKey::new("identity"), + Password::from(PASSWORD), + SEED, + ) + .unwrap(); + + assert_eq!( + enrolled.public_key().to_bytes(), + expected, + "enrolled identity key must equal dig_identity::public_key_bytes(derive_identity_sk(master))" + ); +} + +#[test] +fn signature_verifies_against_public_key() { + let enrolled = Session::enroll_identity( + backend(), + BackendKey::new("identity"), + Password::from(PASSWORD), + SEED, + ) + .unwrap(); + + let msg = b"authorize this action"; + let sig = enrolled.sign(msg); + assert!(dig_keystore::bls::verify(&sig, enrolled.public_key(), msg)); +} + +#[test] +fn injected_signing_fn_works_after_handle_dropped() { + let enrolled = Session::enroll_identity( + backend(), + BackendKey::new("identity"), + Password::from(PASSWORD), + SEED, + ) + .unwrap(); + let pk = *enrolled.public_key(); + + // inject_into hands the consumer only a bare signing primitive. + let sign = enrolled.inject_into(|f| f); + drop(enrolled); + + let msg = b"signed by the injected primitive"; + let sig = sign(msg); + assert!(dig_keystore::bls::verify(&sig, &pk, msg)); +} + +#[test] +fn unlock_with_wrong_password_fails() { + let be = backend(); + let path = BackendKey::new("identity"); + Session::enroll_identity(be.clone(), path.clone(), Password::from(PASSWORD), SEED).unwrap(); + + let err = Session::unlock::(be, path, Password::from("wrong")).err(); + assert!(matches!(err, Some(SessionError::Keystore(_)))); +} + +#[test] +fn enroll_with_empty_seed_is_rejected() { + let err = Session::enroll_identity( + backend(), + BackendKey::new("identity"), + Password::from(PASSWORD), + b"", + ) + .err(); + assert!(matches!(err, Some(SessionError::EmptySeed))); +} + +#[test] +fn unlock_is_generic_over_bls_signing_scheme() { + // Session::unlock also serves seed-derived validator keys (BlsSigning). + let be = backend(); + let path = BackendKey::new("validator"); + Keystore::::create( + be.clone(), + path.clone(), + Password::from(PASSWORD), + None, // generate a fresh seed + KdfParams::FAST_TEST, + ) + .unwrap(); + + let signer = Session::unlock::(be, path, Password::from(PASSWORD)).unwrap(); + let msg = b"validator attestation"; + let sig = signer.sign(msg); + assert!(dig_keystore::bls::verify(&sig, signer.public_key(), msg)); +} + +#[test] +fn debug_does_not_leak_secret() { + let enrolled = Session::enroll_identity( + backend(), + BackendKey::new("identity"), + Password::from(PASSWORD), + SEED, + ) + .unwrap(); + let rendered = format!("{enrolled:?}"); + assert!(rendered.contains("")); + assert!(rendered.contains("L1WalletBls")); +} From 383c48c7be47d9f7f3504abaa8d96adcd95b968e Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Mon, 20 Jul 2026 10:38:56 -0700 Subject: [PATCH 2/2] ci: drop advanced CodeQL workflow; rely on org default setup The org has GitHub Advanced Security default setup enabled (as do sibling crates dig-keystore/dig-identity, which ship no codeql.yml). An advanced CodeQL config cannot upload SARIF while default setup is enabled, so the custom workflow failed. Remove it and let default setup provide CodeQL. Co-Authored-By: Claude --- .github/workflows/codeql.yml | 47 ------------------------------------ 1 file changed, 47 deletions(-) delete mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 079e2e2..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,47 +0,0 @@ -# CodeQL / GitHub Advanced Security static analysis for the Rust crate. -# Runs on every PR + push to main + a weekly schedule so newly-published CodeQL -# rules catch regressions in already-merged code. -name: CodeQL - -on: - push: - branches: [main] - pull_request: - branches: [main] - schedule: - - cron: '27 4 * * 1' - workflow_dispatch: - -# Least privilege: CodeQL needs to read the code and write security events. -permissions: - contents: read - security-events: write - -jobs: - analyze: - name: Analyze (rust) - runs-on: ubuntu-latest - 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: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: rust - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:rust"