Two CRIT findings from the 2026-05-12 Opus audit. Both are
mainnet-blocking against the aldabra-mainnet container.
CRIT-1 — cap-bypass via unsigned-build → sign_partial → submit chain.
Previously `wallet_send` / `wallet_mint` / `wallet_mint_cip68_nft` /
`wallet_script_spend` enforced `max_send_lovelace`, but the unsigned-
build tools + `wallet_sign_partial` + `wallet_submit_signed_tx` did
not. A prompt-injection that walked the cold-signer chain could drain
the wallet past the cap with zero policy enforcement.
Fix:
- `wallet_send_unsigned` / `wallet_mint_unsigned` /
`wallet_plutus_mint_unsigned` now enforce the cap on the user-
supplied destination lovelace, mirroring their signed equivalents.
All three gain a `force: bool` arg with `#[serde(default)]`.
- `wallet_sign_partial` and `wallet_submit_signed_tx` decode the
Conway tx CBOR, sum lovelace across every output whose address is
NOT this wallet's own primary address, and enforce the cap on that
total. Both gain `force: bool`. The chokepoint covers cold-signed
multi-sig flows and any hand-built CBOR the daemon would otherwise
blindly sign or submit.
- New free fn `sum_non_self_lovelace` is the unit-testable core of
the chokepoint logic; `enforce_cap_on_cbor` wraps it.
- The sum uses `try_fold` + `checked_add` (NOT `.sum::<u64>()`) so a
crafted CBOR that overflows `u64::MAX` fails the check instead of
wrapping silently in release builds.
CRIT-2 — path traversal via `reference_script_path` and
`policy_cbor_path`. Previously the tools called `std::fs::read_to_
string(p)` on any path the LLM passed. The MCP daemon runs as the
same user that owns `$ALDABRA_DATA/mnemonic.age` /
`$ALDABRA_DATA/root-xprv.age`. Decode-error messages included the
hex_decode position offset — a small but real information leak about
non-hex file structure.
Fix:
- New `Config::safe_reads_root` field (default `$ALDABRA_DATA/scripts/`,
override via `ALDABRA_SAFE_READS_ROOT` env or TOML).
- New `assert_inside_sandbox` helper canonicalize()s both the root and
the user-supplied path, then enforces `starts_with`. Rejects
outside-root paths, `..`-traversal, and nonexistent paths with
generic messages.
- Hardlink-rejection: post-canonicalize, stat the file and refuse if
`nlink > 1`. `canonicalize` resolves symlinks but NOT hardlinks (a
hardlink IS the file — same inode, different directory entry), so
without this check an attacker with daemon-uid write access could
plant a hardlink to the encrypted key blob inside the sandbox and
exfiltrate bytes through the read path.
- `resolve_ref_script_bytes` + `resolve_policy_cbor_bytes` + the
`resolve_validator_required` wrapper used by all 5 escrow spend
tools take `&Path` and route through the sandbox.
- Error messages on hex_decode failures no longer carry the path
string or byte-offset position — return a constant "contents are
not valid hex" instead.
- `main.rs` creates the sandbox root with 0o700 perms at startup if
missing. chmod errors are surfaced (not swallowed) so a broken
filesystem doesn't silently fall back to umask 0o755.
- README documents the new `ALDABRA_SAFE_READS_ROOT` env var alongside
`ALDABRA_MAX_SEND_LOVELACE` (also previously undocumented).
Tests (243 → 253, +10):
- 5 sandbox tests: accept-inside, reject-outside, reject-dotdot,
reject-nonexistent, reject-hardlink.
- 1 non-hex regression: constant message (no byte-offset leak).
- 3 cap tests: self-send → 0 non-self total, outbound counts,
overflow → Err (regression for the prompt-injection `u64::MAX`
wraparound attempt).
- 1 garbage-CBOR test: clean error.
No new clippy warnings, no new fmt drift, `cargo audit` unchanged
(0 CVEs, 2 transitive unmaintained warnings).
Adversarial review of the first draft (3 Opus reviewers) caught the
u64 overflow, the hardlink bypass, and the swallowed chmod error.
README + supporting docs were written for ourselves (deployment paths,
internal product comparisons, internal task lists, build pipeline
artifacts) instead of for users of the software. This pass refocuses
them on what the software is, how to install, configure, and use it.
- README.md: full rewrite. New shape — What it does / Architecture /
Build / Run / Configuration / MCP tools / Security model / Status /
License / Dependencies. Drops the internal "why we built it"
narrative, drops phase-status claims that drifted stale, drops
internal deployment paths.
- ROADMAP.md: deleted. Was an internal task-list with [x]/[ ] items
showing incremental private development. The README's Status
section now communicates what's actually shipped.
- docs/architecture.md: scrub cross-project comparisons referencing
unrelated internal Sulkta codebases.
- aiken-escrow/README.md: drop reference to a non-existent spec file;
rewrite the Status checklist to reflect what's actually done
rather than what was open at the time of writing.
- audits/2026-05-09-escrow-e2e.md: scrub internal image names +
container paths; the audit findings (chain hashes, validator hash,
what each tx proved) are the public-useful part and stay.
- audits/2026-05-09-escrow-internal-audit.md: drop references to
feature-flag-gated branches that no longer exist.
- Dockerfile: drop the dead `escrow_wip surface` phrase from comments.
- Cargo.toml: drop the cross-project comparison comment that named
an unrelated internal service.
- crates/aldabra-{core,dao}: scrub internal preprod-test naming from
source comments — same technical content, generic phrasing.
- README.md: drop "first Sulkta Rust project — workout for crafting-
table's Rust toolchain" paragraph + the `crafting-table build aldabra`
recipe. Both reference non-public Sulkta-internal infrastructure.
- Dockerfile: drop "Built nightly on Lucy (see lucy-infra/scripts/
nightly-builds.sh)" comment + the `lucy-registry:5000/aldabra/mcp`
internal image-name advertisement.
- Cargo.toml: drop the comment block referencing the deleted
`docs/internal-build-rewrites.md` + `crafting-table + Lucy + dev
hosts` Sulkta-internal-builds note. The patch block stands on its
own.
Removed mentions of Rackham + Sulkta-runs-its-own-Koios claims from
README + module doc-comments + Cargo.toml descriptions. aldabra works
against any Koios endpoint — public api.koios.rest, preprod/preview,
or operator-self-hosted — so the docs now reflect that capability
neutrally instead of advertising our internal infra.
Repo skeleton for sulkta-wallet, the rust-native cardano lite wallet
with MCP server interface. Builds end-to-end, types in place,
real cardano primitives land next pass.
Crates:
wallet-core — pure crypto + types. mnemonic, key derivation,
signing. No I/O. Security boundary.
wallet-chain — pluggable backends. ChainBackend trait, Koios
client (stub for now). Ogmios + submit in phase 2.
wallet-mcp — the binary. stdio MCP transport via rmcp.
Phase plan in ROADMAP.md, threat model in docs/architecture.md.
This is also Cobb's first Rust project + a real-world workout for
crafting-table's rust toolchain.