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/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")); +}