Skip to content

fix(crypto): bring chia-bls onto the chia-wallet-sdk ceiling (0.36.1) - #57

Merged
MichaelTaylor3d merged 1 commit into
mainfrom
chore/3161-chia-ceiling
Aug 26, 2026
Merged

fix(crypto): bring chia-bls onto the chia-wallet-sdk ceiling (0.36.1)#57
MichaelTaylor3d merged 1 commit into
mainfrom
chore/3161-chia-ceiling

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

DRAFT — DO NOT MERGE. Handing back for the gate round.

Slice of https://github.com/DIG-Network/dig_ecosystem/issues/3161 for DIG-Network/digs.

The target is the chia-wallet-sdk ceiling, not crates.io latest: the SDK's latest is
0.36.0 and it resolves the 0.36.1 primitives, so the 0.48.x primitives are unreachable.
A crate already on the ceiling is compliant; being above it is the defect.

Two fixes were in scope. One landed. One is BLOCKED upstream and is reported rather than
forced
— the delta does not line up, and bridging two SDK lines with a shim would be the
byte-drift class §4.1 exists to prevent.


1. digstore-crypto — LANDED: chia-bls = "=0.45.0" -> "0.36.1"

This was the third of the three splits the epic is named for, and the one above the ceiling.

Why the exact pin existed

No reason was ever recorded, and the pin does not appear to have been deliberate.
git log -L on the line resolves to a single commit:

5fe545e  chore(crypto): pin chia-bls=0.45.0 and scaffold digstore-crypto manifest
         Michael Taylor, 2026-06-08

That is the commit that created the manifest — the pin was born with the scaffold, carried
no explanatory comment, and predates the SDK ceiling being understood. Nothing in the crate
depends on 0.4x behaviour. I judged the reason stale and lowered it; the replacement carries
a comment stating the ceiling so the next reader does not have to re-derive this.

Why nothing caught it for ~11 weeks — the important part

digstore-crypto wraps every chia-bls type in an opaque newtype
(pub struct PublicKey(ChiaPublicKey), private field) and returns digstore_core::Bytes48
/ Bytes96. Verified: no chia_bls type reaches its public API, and no consumer crate
names chia_bls at all.

So a wrong version here fails silently, not loudly. The compiler structurally cannot see
the split — it would simply have produced two different BLS implementations in one workspace
with no diagnostic. The only thing standing between that and a signature-verification
divergence is the frozen known-answer vector set. That is the direction this fails in, and it
is why the golden evidence below is the whole proof rather than a formality.

BLS evidence — goldens reproduce BYTE-UNMODIFIED

crates/digstore-crypto/tests/fixtures/bls_vectors.json is unchanged in this diff
(git status reports nothing under tests/fixtures/).

committed_bls_fixture_matches_generated is load-bearing, not vacuous — I checked that
BlsFixtureSet::generate() genuinely recomputes: it walks four fixed seeds and calls
bls_keygen / bls_sign against the live crate, then compares to the parsed on-disk
file. It does not reload the fixture. Any change to keygen or AugScheme signing output would
change pubkey_hex / signature_hex and fail the comparison.

test committed_bls_fixture_matches_generated ... ok
test fixture_set_self_verifies_and_tags_scheme ... ok
test chia_aug_scheme_known_vector ... ok

The 0.45 -> 0.36.1 move therefore altered no cryptographic output and required no source
change
— the API this crate uses (sign / verify / SecretKey::from_seed / to_bytes /
from_bytes) is identical across both lines. This matches the dig-capsule precedent.

Lock effect

Removes the whole orphaned 0.45 family: chia-bls 0.45.0, chia-sha2 0.45.1,
chia-traits 0.45.1, chia_streamable_macro 0.45.1. chia-bls lines go 4 -> 3
(was 0.28.2 / 0.36.1 / 0.42.1 / 0.45.0).

The two survivors are not this repo's declarations and are not fixable here — they are
deep transitives of the SDK itself, exactly the case CLAUDE.md §2.4b calls out:

chia-bls v0.42.1 <- chialisp v0.4.6 <- chia-sdk-types 0.34 <- ... <- chia-wallet-sdk 0.34
chia-bls v0.28.2 <- clvmr v0.16.4   <- chia-consensus 0.36.1 <- chia-sdk-driver 0.34 <- ...

2. digstore-chain — BLOCKED, comment only, no version change

Intended: chia-sdk-coinset / chia-sdk-test / chia-sdk-types / chia-wallet-sdk
0.34 -> 0.36.0. I made that change, measured it, and reverted it.

datalayer-driver 4.0.0 is the latest published release and hard-requires
chia-wallet-sdk ^0.34.0.
It is not optional here: digstore-chain passes DataLayer
singleton types straight across the SDK boundary, so moving only this crate resolves
chia-sdk-driver twice and the build fails with the two lines meeting in singleton.rs:

error[E0432]: unresolved imports `chia_wallet_sdk::driver::Verification`,
              `VerificationAsserter`, `VerifiedData`     (removed in 0.36)
error[E0308]: mismatched types  --> crates/digstore-chain/src/singleton.rs:108, 148, 210, 248, 974
              expected `DataStore` (chia-sdk-driver 0.34)
              found struct `Datastore<DataStoreMetadata>` (chia-sdk-driver 0.36)

The rest of the family has already moved — the holdout is singular:

crate latest chia-wallet-sdk req
dig-wallet-backend 0.31.0 ^0.36
dig-cat / dig-tips / dig-offers 0.3.0 ^0.36
dig-options 0.4.0 ^0.36
datalayer-driver 4.0.0 ^0.34.0

Unblocked by a datalayer-driver release onto 0.36; then this crate and dig-wallet-backend
move together in one cascade. Recorded as a manifest comment in the crate's existing
house style (it already documents a deliberate dig-constants hold for the same
splits-the-tree-rather-than-advancing-it reason), so the next lane does not repeat this.

This needs a child ticket on the epic for the datalayer-driver release.


Blast radius checked

  • digstore-crypto is depended on by 9 crates (dig-client-wasm, cli, compiler, guest,
    host, prover, remote, stage, store) — effectively the whole workspace. Its public surface is
    chia-free newtypes, so the radius is behavioural (BLS output), not type-level; the golden KAT
    is the gate on that radius and it is green.
  • digstore-chain is depended on by dig-resolver, cli, subscription — untouched apart from a
    comment.

Verification

gate result
cargo fmt --all --check exit 0
cargo clippy --workspace --all-targets --locked -- -D warnings -A … (CI's exact line) exit 0
cargo build --workspace --locked green
cargo test --workspace --locked 1571 passed, 0 failed, 169 suites
cargo deny check advisories bans sources advisories ok, bans ok, sources ok
scripts/check-workspace-dep-versions.sh all in-repo workspace dependencies declare version 0.28.1

Installed binary (§3.5)

Guest wasm built first (cargo build -p digstore-guest --target wasm32-unknown-unknown --release), then cargo install --path crates/digstore-cli --force --locked (exit 0):

digs 0.28.1
dig-store 0.28.1
  • scripts/local-push-test.sh5/5 pass, push/pull/clone over a real in-process §21
    server. This exercises the BLS request-signing path (sign_request / verify_request)
    end-to-end under the new chia-bls, including
    clone_rejects_unauthenticated_or_forged_head.
  • Headless digs compile roundtrip on the installed artifact (no chain, no spend):
    deterministic root bb53bead…8d77, exit 0.

init was deliberately not exercised — it mints on mainnet and nothing here needed it.

Leftover agent worktree

.claude/worktrees/agent-afa366a6361dbae8c/ confirmed untracked and gitignored
(.gitignore:17 .claude/worktrees/), so it is not part of any published crate. Its stale
=0.45.0 copy was left alone as instructed. No finding.

Ceiling audit

.claude/scripts/chia-ceiling-audit.sh reads origin/main, not the working tree, so it
still reports the pre-merge state — currently 5 violations for apps/digs.

  • 1 fixed by this PR (chia-bls).
  • 4 remain after merge: the chia-sdk-* / chia-wallet-sdk entries in
    digstore-chain, all blocked on the datalayer-driver release above.

Version

Patch — dependency-only, no behaviour change. Workspace 0.28.0 -> 0.28.1, with the coupled
[workspace.dependencies] digstore-core declaration moved in lockstep (CI enforces it);
digstore-crypto 0.1.0 -> 0.1.1.

`digstore-crypto` declared `chia-bls = "=0.45.0"`, an exact pin ABOVE the
chia-wallet-sdk ceiling. The SDK's latest is 0.36.0 and resolves the 0.36.1
primitives, so the 0.4x line is unreachable for every other crate in this
workspace. This was the third of the three splits tracked by the epic, and it
put four `chia-bls` lines in one lock (0.28.2 / 0.36.1 / 0.42.1 / 0.45.0).

The pin carried no stated reason: it was introduced by 5fe545e (2026-06-08,
"pin chia-bls=0.45.0 and scaffold digstore-crypto manifest"), the commit that
CREATED the manifest — born with the scaffold, uncommented, and predating the
SDK ceiling being understood. Moving to 0.36.1 also drops the orphaned
chia-sha2 / chia-traits / chia_streamable_macro 0.45.1 line, taking `chia-bls`
from four resolved lines to three. The two survivors are deep transitives of the
SDK itself (0.42.1 via `chialisp`, 0.28.2 via `clvmr`), not declarations here.

Nothing caught this for ~11 weeks because it CANNOT fail loudly: this crate
wraps every chia-bls type in an opaque newtype and no `chia_bls` type reaches
its public API, so a split would silently produce two BLS implementations in one
workspace with no diagnostic. The frozen known-answer vectors are the only gate
on that, which is why they are the whole proof here.

The AugScheme vectors in `tests/fixtures/bls_vectors.json` reproduce
BYTE-UNMODIFIED under 0.36.1: `committed_bls_fixture_matches_generated`
recomputes every keypair and signature from the live crate via `bls_keygen` /
`bls_sign` over four fixed seeds and compares against the committed file. The
0.45 -> 0.36.1 move therefore changed no cryptographic output and required no
source change — the API this crate uses (sign / verify / SecretKey::from_seed /
to_bytes / from_bytes) is identical across both lines.

`digstore-chain` gains a COMMENT ONLY, no version change: its `chia-sdk-*`
declarations cannot reach the 0.36 ceiling yet. `datalayer-driver` 4.0.0 is the
latest published release and hard-requires `chia-wallet-sdk ^0.34.0`; because
this crate passes DataLayer singleton types across the SDK boundary, moving it
alone resolves `chia-sdk-driver` twice and `singleton.rs` fails to compile
(`DataStore` 0.34 vs `Datastore<DataStoreMetadata>` 0.36, plus `Verification` /
`VerificationAsserter` / `VerifiedData` removed in 0.36). Bridging the two lines
with a shim would be the byte-drift class §4.1 exists to prevent, so the hold is
recorded in the manifest instead. The rest of the family has already moved to
^0.36 (dig-wallet-backend 0.31.0, dig-cat/dig-tips/dig-offers 0.3.0,
dig-options 0.4.0); datalayer-driver is the sole holdout.

Patch bump: dependency-only, no behaviour change. Workspace 0.28.0 -> 0.28.1
with the coupled `[workspace.dependencies] digstore-core` declaration moved in
lockstep, and digstore-crypto 0.1.0 -> 0.1.1.

Refs DIG-Network/dig_ecosystem#3161

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

loop-security audit — INTERIM (static leg complete, build leg running)

Auditing head 8f258f59468b4a16db4ef4d5274b98f294c46c7a, merge-base 0237e162be532b98e95178ceb3c6a2f65cd87ba8.
Read-only; probes run in a private worktree, never this checkout.

Confirmed so far

The split was real and in-tree. At the merge-base, crates/digstore-chain/Cargo.toml:40 already
declared chia-bls = "0.36.1" while crates/digstore-crypto/Cargo.toml:15 pinned = 0.45.0. Two
chia-bls lines in one workspace, one of them on the crate that signs every 21.9 request.

Point 3 (opaque newtypes) — VERIFIED, exhaustively. chia_bls appears in exactly one file,
crates/digstore-crypto/src/bls.rs, at the use on line 14 and in three tuple structs whose inner
fields are private (lines 21, 25, 29):

pub struct SecretKey(ChiaSecretKey);
pub struct PublicKey(ChiaPublicKey);
pub struct Signature(ChiaSignature);

Grepped the whole crate for ChiaPublicKey|ChiaSecretKey|ChiaSignature|chia_bls across every source
file: zero hits outside bls.rs. No pub field, no From, no Deref/AsRef, no public fn returning
a chia type. The interchange with every consumer is Bytes48/Bytes96/[u8; 32].

Consequence, and it is the important one: the compiler structurally could not have caught this
split.
A duplicate-crate error only fires when two versions of a type meet at a signature, and no
chia_bls type ever reaches a signature here. So the lane's reasoning is right — the golden KAT is
the only guard, which is exactly why it sat undetected. It also means the KAT's adequacy is the
whole question, not a formality.

Point 2 (KAT is not circular) — VERIFIED. committed_bls_fixture_matches_generated
(crates/digstore-crypto/tests/bls_fixtures.rs:24) reads the committed JSON and separately calls
BlsFixtureSet::generate(), which at crates/digstore-crypto/src/fixtures.rs:129-133 runs four fixed
seeds through bls_keygen/bls_sign — i.e. through ChiaSecretKey::from_seed and aug_sign on the
live crate — then compares pubkey_hex and signature_hex. It recomputes; it does not reload.

Stronger still, and worth recording because the lane undersold it: chia_aug_scheme_known_vector
(crates/digstore-crypto/tests/bls_roundtrip.rs:135) carries its ground truth as inline hex
literals
in the test source, not in any file. A regenerated fixture file could not launder that one.

tests/fixtures/bls_vectors.json is absent from the diff (4 files changed: Cargo.lock, Cargo.toml,
crates/digstore-chain/Cargo.toml, crates/digstore-crypto/Cargo.toml) — byte-unchanged, confirmed
from the diff itself rather than from git status.

Point 4 (pin provenance) — VERIFIED, and the framing sharpens. git log -L on the declaration
returns exactly one commit ever: 5fe545e, 2026-06-08, "chore(crypto): pin chia-bls=0.45.0 and
scaffold digstore-crypto manifest"
. Full commit body is the subject plus a Co-Authored-By trailer —
no rationale, no issue reference, no comment. Nothing has touched the line since. Stale, as judged.

One detail that strengthens the evidence rather than weakening it: 5fe545e is an ancestor of
823848c, the single commit that ever wrote bls_vectors.json (also 2026-06-08). So the committed
fixture was generated under chia-bls 0.45.0. The inline KAT says so in its own comment
(bls_roundtrip.rs:147, "Real chia-bls 0.45 AugScheme reference values"). If those reproduce under
0.36.1 unmodified, that is a direct byte-level equivalence proof across the boundary, with 0.45 as the
reference side.

Still running

Version-delta review of the chia_bls API actually used, advisory check on the downgrade direction,
lock-collapse verification from the resolved lock, the digstore-chain reversion check, and the 3.5
installed-binary + local-push-test.sh leg. Verdict follows.

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

loop-security audit — INTERIM 2: the version-delta review (point 1)

Head 8f258f59468b4a16db4ef4d5274b98f294c46c7a. This is the leg the dispatch flagged as "not a
formality", so it was done by diffing the two published crate sources rather than by reading a
changelog.

Fetched chia-bls-0.36.1.crate and chia-bls-0.45.0.crate from static.crates.io and diffed
src/. Identical file sets. Six of nine source files are byte-identicalbls_cache.rs,
derive_keys.rs, error.rs, gtelement.rs, lib.rs, parse_hex.rs. Only three differ, totalling
49 diff lines:

file delta reaches this crate?
public_key.rs Arbitrary impl only, behind feature = "arbitrary" no
signature.rs Arbitrary impl only, behind feature = "arbitrary" no
secret_key.rs pyclass(frozen), Arbitrary, new as_hex_string(), Debug redaction see below

chia-bls declares no default features ([features] = arbitrary, py-bindings, serde, no
default), and digstore-crypto declares it bare, so the arbitrary and py-bindings hunks are
dead code here.

Nothing on the keygen / sign / verify / serialize path differs at all. derive_keys.rs is
byte-identical, and the changed regions of secret_key.rs do not touch from_seed, public_key,
to_bytes or from_bytes. So there is no mechanism by which output could change — which the KAT
then confirms empirically rather than being asked to establish alone.

The one security-relevant delta, and it runs in the downgrade direction

chia-bls 0.45.0 redacts the private key in its Debug impl. 0.36.1 — the version this PR
adopts — prints it in full
:

impl fmt::Debug for SecretKey {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_fmt(format_args!(
            "<PrivateKey {}>",
            &hex::encode(self.to_bytes())
        ))
    }
}

0.45.0 replaced that whole body with formatter.write_str("<PrivateKey>"). Moving 0.45.0 -> 0.36.1
therefore re-adopts a Debug that renders raw BLS private key material as hex — in the crate that
holds the identity key signing every 21.9 request. That is exactly the class the KAT is blind to: the
vectors pin output bytes, and a Debug impl produces no output bytes.

It is NOT reachable here, and I checked rather than assumed. Three independent reasons, all at
crates/digstore-crypto/src/bls.rs:

  1. The wrapper carries no Debug at allpub struct SecretKey(ChiaSecretKey); at line 21 has
    no derive. (PublicKey and Signature at lines 25/29 derive only Clone; SecretKey derives
    nothing.) {:?} on a digstore_crypto SecretKey is a compile error, not a leak.
  2. The inner field is private, so no consumer can reach the chia_bls::SecretKey to format it.
  3. Grepped the workspace for {:?} / dbg! near any sk|secret|priv|key identifier: zero hits.

So this is defense-in-depth, not a live vulnerability, and I am not gating on it. Recommend a
follow-up ticket: give digstore_crypto::bls::SecretKey an explicit redacting Debug
(f.write_str("<SecretKey>")) so the protection is owned locally and does not silently depend on
which chia-bls the workspace happens to resolve. Neither 0.36.1 nor 0.45.0 zeroizes on drop, so
that is unchanged by this PR and is a separate pre-existing item.

Supporting facts

  • No RUSTSEC advisory exists for any chia-* crate (rustsec/advisory-db has no chia-*
    directory), so the downgrade walks back no published fix. Independently, CI's supply-chain audit
    job runs cargo deny check (advisories/yanked/bans/sources) and is SUCCESS on this head.
  • No chia-bls version in 0.32.0..0.48.0 is yanked.
  • 0.36.1 is the line the rest of the ecosystem is on, not a backwater. From the registry:
    chia-wallet-sdk 0.36.0 declares chia-bls ^0.36.1, and datalayer-driver 4.0.0 declares
    chia-bls ^0.36.1. The =0.45.0 pin was the outlier that could never unify with either.

Point 5 — lock collapse, verified from the resolved lock

Counted [[package]] name = "chia-bls" blocks directly, not via cargo tree -d:

  • base 0237e162: 4 lines — 0.28.2, 0.36.1, 0.42.1, 0.45.0
  • head 8f258f5: 3 lines — 0.28.2, 0.36.1, 0.42.1

The 0.45.0 line and its private 0.45.1 subtree (chia-sha2, chia-traits,
chia_streamable_macro) are removed from the lock. Survivors are deep SDK transitives, as claimed.

Point 4 addendum — the reversion is clean, and its comment is accurate

crates/digstore-chain/Cargo.toml has 0 deletions and 0 non-comment additions in this diff — all
18 added lines are # comments. Nothing was half-reverted; no version moved in that file.

Every factual claim the comment makes checks out against the crates.io index:

claim registry
datalayer-driver 4.0.0 is latest, needs chia-wallet-sdk ^0.34.0 confirmed
SDK ceiling is 0.36.0 confirmed (chia-wallet-sdk latest = 0.36.0)
dig-wallet-backend 0.31.0 on ^0.36 confirmed
dig-cat / dig-tips / dig-offers 0.3.0 on ^0.36 confirmed
dig-options 0.4.0 on ^0.36 confirmed
datalayer-driver is the sole holdout confirmed

One correction to the lane's reasoning (does not change the verdict)

The PR reasons that the split could not fail loudly and that "only the golden KAT could" catch it.
The first half is right; the second is not. Since 0.36.1 and 0.45.0 produce byte-identical
keygen/sign output — now established from the source diff and from the vectors reproducing
unmodified — the KAT passed at the merge-base under 0.45.0 too. The KAT could not have detected
this split either, and still cannot.
It is a drift guard on the crypto output, not a version guard.

The manifest comment itself is honest about this ("pin the cryptographic output"), so nothing in the
tree overclaims. But it means that after this merges, nothing mechanical prevents the same drift
recurring
: deny.toml:49 sets multiple-versions = "warn", not deny, and it cannot be raised to
deny because chia-wallet-sdk legitimately resolves several chia-bls lines through clvmr and
chialisp. Follow-up ticket, not a gate: a targeted workspace lint asserting every first-party
chia-bls declaration equals the SDK ceiling.

Remaining: the 3.5 installed-binary + local-push-test.sh leg. Verdict follows.

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

loop-security VERDICT: PASS

Audited head: 8f258f59468b4a16db4ef4d5274b98f294c46c7a — resolved myself from the remote at the
start of the audit AND re-confirmed unchanged at the end, so every probe below ran against the tree that
would merge. Merge-base 0237e162be532b98e95178ceb3c6a2f65cd87ba8. Full tier, as dispatched.

No security defect in this diff. No LIVE vulnerability. Two defense-in-depth items below — neither gates;
both are follow-up tickets.

The fact that frames everything else

This PR changes ZERO Rust source files. The four changed files are Cargo.lock, Cargo.toml,
crates/digstore-chain/Cargo.toml and crates/digstore-crypto/Cargo.toml. The BLS downgrade required no
call-site adaptation at all, which independently corroborates API compatibility for the surface actually
used — and means every behavioural security property of the §21.9 signing path is unchanged except as
mediated by the dependency itself. So the dependency delta IS the audit.

Area-by-area

# area verdict
1 version delta across the BLS boundary CLEAR — characterized from source, not changelog
2 golden KAT non-circular + passing CLEAR — verified by execution
3 opaque-newtype / public-surface claim CLEAR — verified exhaustively
4 pin provenance CLEAR — single commit, no surviving rationale
5 lock collapse CLEAR — 4 lines to 3, verified from the resolved lock
6 §3.5 end-to-end under the new line CLEAR — 5/5 e2e + isolated install, both run by me
7 version bump / --locked agreement CLEAR — 6/6 required checks SUCCESS by name
reversion cleanliness CLEAR — 0 deletions, 0 non-comment additions
secrets / custody / injection / authz / amplification no new surface; no source changed

1 — the delta is fully characterized. I diffed the two published crate sources fetched from
static.crates.io rather than trusting a changelog. Six of nine source files are byte-identical
(bls_cache.rs, derive_keys.rs, error.rs, gtelement.rs, lib.rs, parse_hex.rs). Only three
differ, by 49 lines total: public_key.rs and signature.rs differ ONLY in their Arbitrary impls
behind feature = "arbitrary", and secret_key.rs differs in a pyclass attribute (py-bindings only),
an Arbitrary impl, a new and unused as_hex_string(), and the Debug change described in D1.
chia-bls declares NO default features, and this crate declares it bare, so the arbitrary and
py-bindings hunks are dead code here.

Nothing on the keygen / sign / verify / serialize path differs at all. So there is no mechanism by
which output could change — which the KAT then confirms empirically rather than being asked to establish
on its own.

Supporting facts: no RUSTSEC advisory exists for any chia crate (the advisory-db has no chia directory),
so the downgrade walks back no published fix; no chia-bls version in 0.32.0..0.48.0 is yanked; and CI's
supply-chain audit job runs cargo-deny over advisories/yanked/bans/sources and is SUCCESS on this head.
Most tellingly, 0.36.1 is where the rest of the ecosystem already lives — chia-wallet-sdk 0.36.0 and
datalayer-driver 4.0.0 BOTH declare chia-bls ^0.36.1. The old exact pin was the outlier that could
never unify with either.

2 — the KAT recomputes, and I ran it. cargo test -p digstore-crypto --locked gave 43 passed, 0
failed
, including all three named tests. committed_bls_fixture_matches_generated
(crates/digstore-crypto/tests/bls_fixtures.rs:24) reads the committed JSON and SEPARATELY calls
BlsFixtureSet::generate(), which at crates/digstore-crypto/src/fixtures.rs:129-133 runs four fixed
seeds through bls_keygen/bls_sign on the live crate, then compares pubkey and signature hex. It
recomputes; it does not reload.

The evidence is stronger than the PR claims. The committed bls_vectors.json was generated under
0.45.0
— the pin commit is an ancestor of the single commit that ever wrote that file — and it is
byte-unchanged in this diff. So its reproduction under 0.36.1 is a direct byte-level equivalence proof
with 0.45 as the reference side. Stronger still,
chia_aug_scheme_known_vector (crates/digstore-crypto/tests/bls_roundtrip.rs:135) carries its ground
truth as inline hex literals in the test source, not in any file — a regenerated fixture could not
launder that one.

6 — I ran the end-to-end leg rather than taking it on report. bash scripts/local-push-test.sh in a
private worktree: 5/5 passed in 11.38s, exit 0, including clone_rejects_unauthenticated_or_forged_head,
which serves a genuine self-certifying module under a FORGED publisher signature and requires clone to
fail closed. That exercises signature verification through the new BLS line against a real in-process
§21 server, driving the real compiled CLI. Confirmed offline and mainnet-safe: the helper sets
DIGSTORE_ANCHOR_MOCK=1 with the public ABANDON vector
(crates/digstore-cli/tests/common/mod.rs:57-59), so init mints against an in-memory mock. I did not run
the mint command directly.

Separately, cargo install --path crates/digstore-cli --force --locked into an isolated install root
completed exit 0, and the resulting binary reports digs 0.28.1 — matching the workspace bump. I
used an isolated root deliberately rather than the shared cargo bin, which is machine state other lanes
may depend on.

Defense-in-depth (NOT gating — follow-up tickets)

D1 — MEDIUM. The downgrade re-adopts a Debug impl that renders the raw BLS private key. Unreachable
today.
chia-bls 0.45.0 redacts the secret in its Debug impl; 0.36.1, the version this PR adopts,
prints it in full
— 0.36.1 formats <PrivateKey {hex-of-the-secret-key}> where 0.45.0 writes the
constant string <PrivateKey>. This is exactly the class the golden vectors are blind to: the KAT pins
OUTPUT BYTES, and a Debug impl produces none. It is the one hunk in the whole delta that runs in the
dangerous direction, and it is why "confirm no API changed semantics" was worth doing properly.

Checked, not assumed. It is unreachable for three independent reasons, all in
crates/digstore-crypto/src/bls.rs:

  1. The wrapper pub struct SecretKey(ChiaSecretKey); at line 21 has no Debug at all — only
    PublicKey and Signature (lines 25, 29) derive anything, and only Clone. Formatting a
    digstore SecretKey with the debug formatter is a compile error, not a leak.
  2. The inner field is private, so no consumer can reach the underlying key to format it.
  3. A workspace-wide grep for debug-formatting or dbg! near any secret/key identifier returns
    zero hits.

There is no state and no attacker action that reaches the impl, so no exploit scenario exists. I am not
gating on it.
Recommend a follow-up: give digstore_crypto::bls::SecretKey an explicit redacting Debug
so the protection is owned locally instead of silently depending on which chia-bls the workspace happens
to resolve. Neither version zeroizes on drop; that is unchanged by this PR and is a separate pre-existing
item.

D2 — LOW. Nothing mechanical prevents this drift recurring. One correction to the PR's reasoning,
which does not change the verdict. The description says the split could not fail loudly and that only the
golden KAT could have caught it. The first half is right and I verified it. The second half is not:
because 0.36.1 and 0.45.0 emit byte-identical output — now established from the source diff AND from
the vectors reproducing unmodified — the KAT passed at the merge-base under 0.45.0 too. The KAT could
not have detected this split, and still cannot.
It is a drift guard on the cryptographic output, not a
version guard.

The manifest comment is honest about this (it says it pins the cryptographic output), so nothing in the
tree overclaims. But it does mean the detection gap survives this merge. deny.toml:49 sets
multiple-versions = "warn", not deny, and it cannot be raised to deny because chia-wallet-sdk
legitimately resolves several chia-bls lines through clvmr and chialisp — the same reason cargo tree -d
is unusable as a gate here. Recommend a targeted lint asserting every FIRST-PARTY chia-bls declaration
equals the SDK ceiling.

D3 — trivial. crates/digstore-crypto/tests/bls_roundtrip.rs:147 still reads "Real chia-bls 0.45
AugScheme reference values". The values remain correct — that is precisely the finding — but the comment
now names a version the workspace no longer resolves. Fix on next touch; not worth a round.

Supporting verification detail

Point 3 — opaque newtypes, verified exhaustively. chia_bls appears in exactly ONE file,
crates/digstore-crypto/src/bls.rs: the import at line 14 and three tuple structs with private fields at
lines 21, 25 and 29. I grepped every source file in the crate for the three type aliases and the crate
name: zero hits outside bls.rs. No public field, no From, no Deref/AsRef, no public function
returning a chia type. The interchange with every consumer is Bytes48 / Bytes96 / plain byte arrays.
The claim holds — and it is exactly what made the split invisible to the compiler, since a duplicate-crate
error only fires when two versions of a type meet at a signature.

Point 4 — pin provenance. git log -L on the declaration returns exactly one commit in all of
history: 5fe545e, 2026-06-08, the commit that CREATED the manifest. Its full body is the subject plus a
co-authorship trailer — no rationale, no issue reference, no comment, and nothing has touched the line
since. Stale, as judged; I found no surviving reason and no code path needing 0.45-only behaviour (the
only 0.45-only addition, as_hex_string(), is unused here).

Point 5 — lock collapse, verified from the resolved lock. I counted chia-bls package blocks directly
rather than via cargo tree -d: base has 4 (0.28.2, 0.36.1, 0.42.1, 0.45.0); head has 3
(0.28.2, 0.36.1, 0.42.1). The 0.45.0 line and its private 0.45.1 subtree — chia-sha2, chia-traits,
chia_streamable_macro — are removed. The survivors are deep SDK transitives, as claimed.

The reversion is clean and its comment is accurate. crates/digstore-chain/Cargo.toml shows 0
deletions and 0 non-comment additions
in this diff: all 18 added lines are comments, and no version
moved in that file. This is not a half-reverted experiment. Every factual claim the comment makes checks
out against the crates.io index: datalayer-driver 4.0.0 is latest and hard-requires
chia-wallet-sdk ^0.34.0; the SDK ceiling is 0.36.0; dig-wallet-backend 0.31.0, dig-cat / dig-tips
/ dig-offers 0.3.0 and dig-options 0.4.0 all declare ^0.36; and datalayer-driver is indeed the
sole remaining holdout.

One lock rider, benign. tempfile moved between getrandom lines. The getrandom version SET is
identical at both ends (0.2.17, 0.3.4, 0.4.3), the 0.4.3 line survives via chia-ssl, jobserver,
quinn-proto and rand, and digstore-crypto has no RNG dependency at all — its AEAD path uses a
documented fixed nonce owned by digstore_core::crypto. No cryptographic path is fed by this change.

What I could NOT verify, and why

  • The digstore-chain 0.34-to-0.36 compile failure the comment describes was not reproduced — the PR
    reverted it, so there is nothing in the diff to test. I verified the CLAIMS against the crates.io index
    instead (all six confirmed above) and verified the reversion left zero residue in the tree.
  • chia-bls was not fuzzed under adversarial G1/G2 inputs. I relied on the source diff showing the
    parse and verify paths byte-identical between the two versions, plus the passing negative tests
    (from_bytes_rejects_non_canonical_public_key, verify_rejects_malformed_public_key_bytes,
    verify_rejects_wrong_message, verify_rejects_wrong_public_key).
  • I did not re-run the KAT under 0.45.0 to demonstrate D2 empirically. It follows by construction from
    the source diff (identical keygen/sign paths) plus green CI at the merge-base, but it is an inference
    rather than an executed probe.
  • I did not audit whether the §21.9 server enforces nonce-replay and freshness windows. That code is
    unchanged here (no source file moved), so it is outside this diff — but it is the property that makes
    the request signatures meaningful, and it is worth a dedicated pass during a hardening round.

Merge preconditions (observed, not acted on)

6/6 required contexts present and SUCCESS by nameLint commit messages, Check version increment, build & test (ubuntu-latest), build & test (windows-latest), dig-client-wasm (read-crypto npm package), supply-chain audit — with 0 unresolved threads. The PR is still DRAFT,
which is correct: gates had not returned until now.

Process note

Read-only throughout; I made no edit, push, merge or undraft. All probes ran in a private worktree cut
detached at the head SHA under C:\tmp\worktrees\sec-digs-57. The shared checkout at modules/apps/digs
— which is currently sitting ON this PR's branch — and the sibling lane's digs-51 worktree were never
mutated: no checkout, no reset, no stash, no file edit, and the install went to an isolated root rather
than the shared cargo bin. My worktree is removed on completion.

@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review August 26, 2026 18:30
@MichaelTaylor3d
MichaelTaylor3d merged commit 5872240 into main Aug 26, 2026
21 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the chore/3161-chia-ceiling branch August 26, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant