Commit Graph
9 Commits
Author SHA1 Message Date
jwinterm a7c144b11f docs: MIT LICENSE (Such Software LLC) + README for v0.3.0
- LICENSE: MIT, © Such Software LLC (published under the company umbrella).
- Cargo.toml: license = "MIT".
- README: rewritten for the feature-complete release — non-custodial guarantee,
  chains, public API + operator surface, running it, security posture, testing.
2026-06-28 08:38:57 -04:00
jwinterm 12e4a54c63 feat(admin): smirk-admin break-glass CLI [operator §1.6]
A binary that talks to Postgres directly, bypassing the HTTP admin plane and all
network rate limits — the recovery path when the network plane is unreachable or
a solo admin key is compromised. Authority = shell + DB creds +
ADMIN_KEY_INTEGRITY_SECRET. Every mutation writes a hash-chained audit row
(actor_kind=cli).

Commands:
- list-keys
- add-key --pubkey <64hex> [--label]
- revoke-key --id <uuid>            — MAY revoke the last key (keep_min_live=false;
  the network DELETE refuses that, the CLI is the authority)
- replace-key --old <uuid> --pubkey <64hex> [--revoke-all] — atomic rotate; with
  --revoke-all, the global break-glass that revokes every other live key
- create-admin-wallet --out <path> — generate a schnorr keypair from OS entropy,
  write the secret hex to a NEW 0600 file (create_new, never clobber/stdout/log),
  register only the pubkey; seed + secret buffers zeroized (zeroize crate)
- doctor — DB reachability, live-key count, audit-chain verification

Preflight refuses to run if a present .env is group/other-readable. No new dep
beyond zeroize; bin auto-discovered as `smirk-admin`. Smoke-tested end to end
(wallet file is 0600; bad pubkey rejected; doctor detects a wrong-secret chain).

Deferred (documented): remac-keys (integrity-secret rotation must also re-chain
the audit log) and setup/reset-setup (first-run bootstrap = Subsystem 3).
2026-06-26 11:04:05 -04:00
jwinterm 707613c92b feat: router middleware (CORS, body limit, timeout, compression, tracing)
Layer global middleware onto build_router (applies to every route, shared by
main + the test harness):
- CORS: configurable allowed origins via CORS_ALLOWED_ORIGINS; empty -> allow
  any origin, which is safe here because auth is a Bearer token (no cookies, no
  ambient credentials). Credentials are never allowed.
- Body limit: 1 MiB global backstop (handlers already validate tighter per
  field); /wallet/lws/submit_tx overrides to ~2 MiB+ for raw Monero tx hex.
- Timeout: 60s request backstop (above the chain clients' ~30s timeouts, so
  their errors surface first; bounds any slow handler). Uses the http-aware
  tower_http TimeoutLayer (-> 408), not tower::timeout (fallible, rejected by
  axum's Infallible Router).
- gzip compression + request tracing (outermost).

Adds cors_allowed_origins config + the tower-http "timeout" feature. Per-IP
rate limiting is deliberately separate (needs ConnectInfo) — next commit.

All gates green; integration tests pass through the layered router (CORS/timeout/
compression/body-limit are oneshot-safe). openapi unchanged.
2026-06-23 18:25:10 -04:00
jwinterm 7ddb586cbe feat: LWS client for Monero/Wownero (hardened, generic)
Reworked clean from the chain-audit playbook into one generic client over
CryptoNoteNetwork (Monero + Wownero share the LWS API). The LWS may be
remote/community-run, so every response is treated as hostile.

Security (closes the audit CRITICAL/HIGH/MEDIUM findings):
- The two `&str` byte-slice panics are GONE: responses are parsed via a
  generic post_json -> read_capped -> serde_json::from_slice path. No
  byte-indexing of an untrusted body anywhere; the JSON-error-column
  slice is deleted.
- Streaming size cap (8 MiB) enforced as bytes arrive (content-length is
  attacker-asserted) + shared reqwest client with request/connect
  timeouts. No unbounded read, no slowloris.
- Admin key held in a redacting `Secret` (Debug -> "***"); secret-bearing
  request structs drop `Debug`; #[instrument] skips view_key.
- Non-success -> generic NodeError tagged with a static endpoint label +
  status code only; the upstream body is never interpolated into a log or
  error. Transport (connect) errors are logged privately, redacted publicly.
- Amounts are u64 end-to-end via the fail-closed string_or_* deserializers
  (garbage/overflow -> Err, never panic/silent-zero); mempool sum and
  confirmation math use saturating ops; future-block confirmation -> 0.
- Ring/decoy count clamped (MAX_DECOY_COUNT) + request/parsed fan-out
  capped (MAX_PARSED_VEC).

0-conf architecture: default is REST-poll (compute pending on-demand from
get_address_txs, which includes mempool on current monero-lws). HTTP
webhooks are NOT brought over (redundant + fragile per docs/lws-update.md);
ZMQ-push stays an optional, feature-flagged unit for single/own backends
wanting instant confirms. Kept the account-lifecycle calls
(modify_account_status/rescan/account_scan_height) — those are the
deep-sleep machinery REST-poll uses to wake/sleep accounts for scale.
Dropped the /login pending flow + all dead/tip surface.

Adds a shared redacting `Secret` (core::secret, grin reuses) and the
reqwest `stream` feature for bounded reads. types.rs kept (the
string_or_* fail-closed helpers) with secret request structs de-Debug'd
and error-not-panic tests. 15 L2 tests. Adversarially reviewed (7/7
audit findings closed; future-block confirmation hardened).
2026-06-22 23:18:58 -04:00
jwinterm 4c71af15e0 refactor: BTC/LTC are Electrum/Fulcrum-only (drop Core-RPC client + dep)
Electrum/Fulcrum already covers everything the backend needs for Bitcoin
and Litecoin — balance, UTXOs, history, tip, verbose tx, fee estimation,
and broadcast (blockchain.transaction.broadcast). A self-hosted Fulcrum
relays broadcasts to its own backing node, so a separate Bitcoin Core
RPC path buys nothing in the self-host model while adding the
bitcoincore-rpc + bitcoin + secp256k1 + jsonrpc dependency tree and a
duplicate broadcast surface.

- Remove the `bitcoincore-rpc` dependency (and its transitive tree).
- Slim `UtxoConfig` to pure Electrum config: drop use_local_node /
  rpc_url / rpc_user / rpc_pass; keep network + electrum_primary +
  electrum_fallbacks.

A future LTC-MWEB or direct-node provider is a distinct capability
(MWEB uses a scan-key model, not canonical scriptPubKeys) and would slot
in at the provider seam rather than extending this client — so nothing is
foreclosed by keeping the surface minimal now.
2026-06-22 21:44:17 -04:00
jwinterm e838565ed7 feat: electrum client (webpki TLS, bech32, checked sats)
BTC/LTC Electrum/Fulcrum client, reworked clean from the chain-audit
playbook (not ported). Every byte from a server is treated as hostile.

Security:
- TLS verified against the webpki root store WITH hostname checking; the
  accept-any-certificate verifier is gone (closes the MITM hole). The
  connector is built once and reused.
- Addresses decoded via the `bech32` crate (full checksum + bech32m +
  witness version) and base58check (checksum); the hand-rolled bech32
  underflow panic is removed. Coin/network enforced (reject ltc-on-btc,
  testnet-on-mainnet) — no byte-indexing of untrusted input.
- Amounts pass a fallible checked f64->sat converter (rejects
  non-finite/negative/>2^53) with checked_add summation; no silent
  saturation to 0 / u64::MAX. estimatefee requires finite, clamps.
- Bounded response read + per-exchange timeout (no OOM, no slowloris).
- JSON-RPC response id correlated with the request.
- Untrusted upstream text is control-stripped + length-capped before it
  reaches a (privately-logged) NodeError.

Single `call` primitive centralizes failover + id + envelope mapping;
public methods (balance/utxos/history/tip/verbose-tx/broadcast/fee) are
thin wrappers. Tip-attribution helpers dropped; total_sent_from now
declines (None) without resolved prevouts instead of mislabeling a
receive-with-change as a send.

20 L2 tests: address->script KATs (P2WPKH/P2WSH/taproot/legacy), network
rejection, panic-safety on short/garbage input, checked-sat bounds,
envelope id correlation, bounded/oversized read, duplex round-trip.
2026-06-22 20:57:08 -04:00
jwinterm 99cfc49cb2 feat: AppState + server wiring (config, pool, migrations, session manager)
- AppState (lib.rs): Config + Database + SessionManager + in-memory web-challenge
  store; injected into handlers via State<Arc<AppState>>
- main.rs: fail-closed Config::from_env, pg pool, run embedded migrations on
  start, build state, serve /health (auth/wallet routes added as handlers land)
- Cargo.toml: sqlx 'migrate' feature for embedded migrations
2026-06-22 14:36:51 -04:00
jwinterm 9ececb03af feat: NIP-98 auth + wallet-signature verification (hardened)
core/crypto/nip98.rs:
- verify_nip98 (login-grade): binds url+method+freshness
- verify_signed_action (state-change): adds single-use nonce + purpose +
  canonical request-descriptor hash + optional target-id/instance-id, each an
  exactly-one tag, constant-time compared, all checked on the signature-
  authenticated event (id recomputed + schnorr-verified before any tag trusted)
- hardening from adversarial review: length-guard pubkey/id/sig before k256
  construction (no panic on short input), saturating freshness window (no
  overflow wrap); regressions for all three

core/crypto/signatures.rs: BIP-137 ECDSA + Ed25519 verification returning
Result<(),AppError> (Ok only on valid; malformed->400, mismatch->401) — closes
the Ok(false) fail-open.

Cargo.toml: overflow-checks=true in release (defense-in-depth for auth paths).
2026-06-22 13:47:20 -04:00
jwinterm 143f00701b chore: initial foundation — server skeleton, autodocs, CI
- minimal Axum server exposing GET /health
- OpenAPI contract generated from handlers (gen_openapi -> openapi.json),
  drift-gated in CI so the spec can never silently diverge from the code
- CI: secret-scan + gitleaks, openapi drift gate, fmt/clippy/build/test/doc-test, cargo-deny
- Cargo.lock tracked for reproducible builds; publish=false; pinned toolchain
- README, SECURITY.md, deny.toml, .env.example
2026-06-22 06:18:57 -04:00