Skip to content

Commit c434cdf

Browse files
docs: SPEC 6.1.1/6.2/6.3/6.4 + README + CHANGELOG for the owned-approval shape
Delete the two SPEC carve-outs that stop being true (enforcement is opt-in; the authorization is not bound to the signed bytes) and the closed "undeclared intent is not escalatable" shape gap. SPEC 6.2 states the enforcement point 6.2's MUST previously lacked; 6.3 states the three outcomes; new 6.3.1 records the normative crate-outcome -> wire-code mapping including SPEND_POLICY_INDETERMINATE -33056, which a host must add to its own SPEND_* taxonomy (this crate carries none). Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b44f7b4 commit c434cdf

54 files changed

Lines changed: 20273 additions & 78 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,59 @@ All notable changes to this project are documented here.
44
This project adheres to [Semantic Versioning](https://semver.org) and
55
[Conventional Commits](https://www.conventionalcommits.org).
66

7+
## [0.2.0] - 2026-07-27
8+
9+
### BREAKING CHANGES
10+
11+
The custody gate now returns a RULING and mints an approval that OWNS the coin spends it
12+
authorized, and that approval is the only thing the signer accepts. A `0.x` MINOR is
13+
semver-breaking, so every consumer must move: `^0.1` cannot resolve `0.2.0`.
14+
15+
- `PolicyAuthorizer::authorize_op` now takes `&[CoinSpend]` (not `&SpendSummary`) and returns
16+
`Result<SpendRuling>` (not `Result<()>`). It derives the summary itself, so there is no
17+
caller-supplied description for it to disagree with.
18+
- **`MoneySigner::sign_coin_spends` is REMOVED.** `sign_approved(SpendApproval) -> SpendBundle`
19+
is the only signing entry point; no function anywhere turns `&[CoinSpend]` into a signature.
20+
- **The `SpendAuthorizer` trait is REMOVED.** A custody gate must not be an interface whose
21+
simplest implementation approves everything — the crate's only consumer had shipped exactly
22+
that (a default authorizer whose entire body was `Ok(())`), so every bound this crate
23+
advertised was absent from the running application.
24+
- **`AccountError::RequireAuth` is REMOVED.** "Not yet — ask the human" is now
25+
`SpendRuling::RequiresConfirmation`, an `Ok` value. As an error it forced consumers to collapse
26+
it into a refusal, which made the confirm ceremony unreachable for exactly the `Confirm` and
27+
`Vault` tiers that exist to require it.
28+
- `LocalMoneySigner::sign_unsigned` is now `pub(crate)`: it accepted a caller-supplied summary,
29+
which was the same defect one layer over.
30+
31+
Migration: replace `authorize(&summary)` / `authorize_op(&summary, class)` with
32+
`authorize_op(&coin_spends, class)`; match the ruling — `Approved(approval)``sign_approved`,
33+
`RequiresConfirmation(pending)` → render `pending.summary()`, then
34+
`pending.confirmed(decision)`. Delete any fake authorizer and drive the real gate with a test
35+
policy plus the public `FixedClock`.
36+
37+
### Features
38+
- **wallet:** The gate mints an owned `SpendApproval`; the signer accepts nothing else
39+
- **wallet:** `SpendRuling` carries the escalatable third outcome, so a `Confirm`/`Vault` spend
40+
reaches the confirm ceremony
41+
- **wallet:** `SpendOpClass::Undeclared` escalates to the human rather than dead-ending, so a
42+
request from outside the process is confirmable rather than unspendable
43+
- **wallet:** `AutoSendPolicy::configured_limits` — the `Option` form of the op-class lookup
44+
45+
### Fixes
46+
- **wallet:** Refuse a spend whose input coin amounts do not sum in a `u64`. `dig-wallet-backend`
47+
0.16 accumulates them with an unchecked `+=` (`client/verify.rs:153`), and those amounts arrive
48+
in a caller-supplied skeleton, so an unsummable total panicked in a debug build and wrapped in a
49+
release build — after which the wrapped figure was what value conservation was checked against.
50+
- **wallet:** Charge the rolling period cap the spend's real value, and merge the cap's two
51+
overflow checks into the one that is reachable.
52+
53+
### Tests
54+
- The #1698 exploit is now UNWRITABLE rather than merely rejected: compile-fail proofs for the
55+
exploit itself, for minting an approval outside the crate, and for using either token twice,
56+
plus trait-absence assertions (`Clone`/`Serialize`/`DeserializeOwned`/`Debug`) and structural
57+
invariants (one minter, one field-assembly site, one `derive_summary` call site, no signing
58+
function over `&[CoinSpend]`).
59+
760
## [0.1.2] - 2026-07-27
861

962
### Features

README.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,25 @@ This crate owns the object model, unlock policy + keystore crypto, the in-proces
1111
signer, per-profile key/DEK derivation, the DID+dig-store mint, and all wallet ops. It never draws
1212
UI — the host harness (dig-app) injects a UI/auth provider that this crate calls back through.
1313

14-
`PolicyAuthorizer` is the custody gate a host puts in front of signing. It enforces the two-tier
15-
custody policy and the user's auto-send policy: a vault-tier spend always requires a full
16-
authorization ceremony and may only ever pay the profile's own hot wallet (via `VaultMove`, a
17-
24-hour clawback the user can cancel); a hot-wallet spend auto-signs only within its op class, its
18-
per-transaction limit, and a rolling period cap. Every default refuses.
19-
20-
**The gate is something a host must USE, not something this crate applies for you.** dig-account
21-
does not compose a send path: the money signer is reachable without the gate, and nothing binds an
22-
authorization to the coin spends that get signed. `SPEC.md` §6.1.1 states the obligations a host
23-
takes on, and exactly which of them this crate can and cannot check.
14+
`PolicyAuthorizer` is the custody gate, and **it is not optional.** It enforces the two-tier custody
15+
policy and the user's auto-send policy: a vault-tier spend always requires a full authorization
16+
ceremony and may only ever pay the profile's own hot wallet (via `VaultMove`, a 24-hour clawback the
17+
user can cancel); a hot-wallet spend auto-signs only within its op class, its per-transaction limit,
18+
and a rolling period cap. Every default refuses.
19+
20+
**The gate cannot be bypassed, and the thing it approved is the thing that gets signed.** You hand
21+
`authorize_op` the coin spends — never a description of them — and it derives the summary itself. What
22+
it returns is a `SpendApproval` that *owns those exact coin spends*, and `sign_approved` is the only
23+
signing entry point in the crate. So there is no unauthorized route to a signature, and nothing to
24+
compare that could compare the wrong bytes. Single-use, unmintable outside the gate, and unclonable —
25+
each held by the type system rather than by a runtime check.
26+
27+
The ruling has three outcomes, not two: approved, **requires confirmation** (run the ceremony, then
28+
`confirmed`), or refused. That third state is why a spend needing a human reaches the human instead of
29+
being silently declined.
30+
31+
`SPEC.md` §6.2 is the normative statement, including the limits this layer still does NOT provide
32+
(§6.1.1).
2433

2534
See [`SPEC.md`](./SPEC.md) for the normative contract. Consumed by `dig-app`.
2635

0 commit comments

Comments
 (0)