docs: add README and refresh audit

This commit is contained in:
randogoth 2026-10-05 11:12:23 +03:00
parent 267625b411
commit 21c7cbe4ed
4 changed files with 175 additions and 8 deletions

View file

@ -1,16 +1,17 @@
# smolboid audit
A security and implementation audit of smolboid as of 2026-10-05, covering the `core/` feed format, the `daemon/` binary, and the three end-to-end suites (integration rig, stress, pentest). The audit was conducted against the smolmail protocol (SPEC.md 1.2), PLAN.md, and the actual fumi-core and bunshin codebases it interoperates with. Everything below reflects executed and observed behavior, not intent.
A security and implementation audit of smolboid as of 2026-10-05, covering the `core/` feed format, the `daemon/` binary, the three end-to-end suites (integration rig, stress, pentest) and a dependency vulnerability scan. The audit was conducted against the smolmail protocol (SPEC.md 1.2), PLAN.md, and the actual fumi-core and bunshin codebases it interoperates with. Everything below reflects executed and observed behavior, not intent.
## Verification results
| Suite | Command | Result |
| --- | --- | --- |
| Unit and format tests | `devbox run test` | 19 core + 28 daemon + 3 vector pins, all pass |
| Unit and format tests | `devbox run test` | 20 core + 31 daemon + 3 vector pins, all pass |
| Reference vectors | `devbox run vectors` regenerates `core/vectors.json` | 15 cases pin every verification state; byte-identical regeneration |
| Integration rig | `devbox run rig` | Passes: post, nick, repost, delete, timestamp collision, hard limit, FIFO prune, 90% warning, author rotation — all against real bunshin and fumi binaries |
| Stress | `devbox run stress` | Passes: 40-send burst with integrity held, exact 25-record quota, reply cap exact at 10, 200 parallel HTTP requests in 271ms (~738 req/s lower bound), all-304 conditional load, zero failed reads during concurrent writes |
| Pentest | `devbox run pentest` | Passes: every probe repelled (details below) |
| Dependency scan | `devbox run audit-deps` | Clean: 80 crate dependencies, zero RustSec advisories (cargo-audit 0.22.2, 2026-10-05) |
## Trust model, as implemented
@ -26,6 +27,7 @@ The reader of a feed verifies the author's own §5.3 payload signature, rebuilt
- **Reply floods are bounded.** At most 10 error replies per sender per hour, and never twice for the same rejected payload (state ledger plus dedupe ring). Proven by both suites: 15 and ~39 rejections drew at most 10 answers.
- **Quotas and retention hold under rapid fire.** The daily cap is never exceeded even by a 40-send burst, and paced sends land exactly on it; FIFO prunes only what the owner directed; hard-limit mode never silently deletes. Proven by the stress suite.
- **Slow clients cannot wedge the listener.** Socket read/write timeouts of 10s close half-open connections; the listener stayed healthy during and after a slowloris hold. Proven by the pentest.
- **Connection floods are shed, not absorbed.** At most 64 concurrent connections; beyond that the listener answers 503 without spawning a thread, and recovers as soon as the flood closes. Proven by the pentest's 64-connection flood probe and a unit test that asserts the shed and the recovery.
- **Reads never see torn writes.** Feeds and state are replaced atomically (temp file plus rename); 60 reads during a concurrent delete all succeeded and the feed verified. Proven by the stress suite.
- **Unparseable or hostile input fails closed.** Oversized requests are capped at 8 KiB, malformed bodies become `Ignore`, blocks with unreconstructible fields are rejected with an explanatory reply rather than published. Proven by pentest parsing probes and dispatch unit tests.
@ -39,11 +41,12 @@ Each of these was discovered by a suite or by design review against real clients
4. **Admission required a welcome mail (fixed).** Without the daemon's Accept token an author's mail stays in requests tier forever; `smolboid admit` now sends a welcome mail carrying it.
5. **A `smol:reply_to` metadata binding would break on rotation (fixed by removal).** The author's Reply-To changes with each rotation; per-record derivation replaced the binding entirely.
6. **A stale base32 length in the plan (fixed).** A 64-byte signature is 103 unpadded base32 characters, not 104.
7. **Unbounded thread-per-connection (fixed).** The listener now caps concurrent connections at 64, answering 503 beyond that; an atomic counter with an RAII guard keeps slots leak-free across early returns.
8. **HEAD returned 405 (fixed).** The listener now answers HEAD with exactly the headers GET would send, Content-Length included, minus the body — what pollers expect.
9. **A CR-run round-trip bug in the feed parser (fixed, found by the new property test).** `str::lines` strips only one CR per line, so a line ending in a CR run kept a CR as content that vanished on the next parse — a load/save cycle could silently change bytes. Trailing CRs are now stripped per line at parse, making render a fixpoint on arbitrary input.
## Open findings, accepted for v1
- **Thread-per-connection is unbounded.** Bounded per connection by the 10s timeouts, and the port is operator-exposed, but a connection flood can still spawn many short-lived threads. A concurrent-connection cap is a small future hardening step.
- **HEAD returns 405.** Legitimate pollers may use HEAD; serving it (same headers, no body) is a small compatibility improvement.
- **The HTTP listener is plain HTTP.** The plan's `https://` origin assumes TLS termination upstream (a reverse proxy); a direct deployment serves feeds in cleartext. This is a deployment requirement to document in the README, not a code defect.
- **Bootstrap trust is operator-mediated.** The daemon's own server pin, registration, and each author's first resolution arrive via the fumi CLI and `smolboid admit` with trust-on-first-use semantics; a compromised or lying DNS/SRV path fails closed at the Noise pin mismatch, but the *first* pin is taken on faith by the operator.
- **`smol:author` is adopted from the first publishing mail.** The first Reply-To a feed's owner sends rewrites the metadata's address spelling (empty feeds only). A malicious owner can only rename their own feed's address form, not another user's; cross-feed impact is none, but the rule is worth knowing.
@ -52,18 +55,18 @@ Each of these was discovered by a suite or by design review against real clients
## Not covered by this audit
- fumi-core and bunshin themselves; they are separate projects with their own test suites, and smolboid depends on their correctness (envelope verification, Noise pin enforcement, tier separation).
- No `cargo audit`-style dependency vulnerability scan was run; the dependency tree is small (fumi-core, chacha20poly1305, serde, serde_json) but this check is recommended before any public deployment.
- No fuzzing of the feed parser or HTTP request parser; both are small and hand-tested against hostile inputs, but fuzz targets would raise confidence.
- The fuzzing gap is narrowed but not closed: deterministic property tests now hammer both parsers with thousands of hostile inputs (no panics, render is a fixpoint, every accepted username is path-safe), which already caught the CR-run bug above; what remains is coverage-guided fuzzing and non-UTF-8 input, which would need a nightly toolchain.
- Timing side channels are untested; the crypto layer is all constant-time primitives from fumi-core, but no analysis was performed at this layer.
## Reproducing
```
devbox run test # unit, format and vector tests
devbox run test # unit, property and vector tests
devbox run vectors # regenerate and pin core/vectors.json
devbox run rig # end-to-end against a local bunshin
devbox run stress # flood, quota, reply budget, HTTP load
devbox run pentest # adversarial probes, raw sockets and real mail
devbox run audit-deps # cargo audit against the RustSec advisory database
```
All suites are self-contained: each starts its own bunshin instance, fumi identities and daemon on local ports and cleans up after itself.