Skip to content

feat(storage): private by default; node operators only ever hold ciphertext - #218

Merged
ralyodio merged 2 commits into
masterfrom
feat/private-storage-e2e
Nov 9, 2026
Merged

ralyodio merged 2 commits into
masterfrom
feat/private-storage-e2e

Conversation

@ralyodio

@ralyodio ralyodio commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

What

Private storage, finished and on by default (DIP-0012 private tier). Node operators now only ever hold ciphertext for anything that is not deliberately public. Public hosted sites are unchanged and stay plaintext.

  • New crate c0mpute-sealed (format c0mpute-private-v1):
    • A random data key per object. AES-256-GCM over 64 KiB chunks in the STREAM construction, with the chunk index and a last-chunk flag in both the nonce and the AAD, so reordering, truncation and extension all fail.
    • An HKDF key commitment, checked before anything is decrypted, which closes the multi-recipient "salamander" gap.
    • Metadata (name, type, exact size, a signed monotonic version) is encrypted inside the stream.
    • Padmé size buckets.
    • The key is wrapped with X25519 into 16 fixed, anonymous, look-alike slots.
    • An owner-only encrypted roster of who the object is wrapped to.
    • A per-object ed25519 owner signature over the content hash and metadata hash, bound to the site id.
    • No convergent encryption: equal files give unrelated ciphertext.
    • Every secret type zeroizes on drop, and Debug never prints keys or plaintext.
  • Keys derive from the user's c0mpute identity seed (the c0mpute dm keyfile: Argon2id, encrypted backup) under their own HKDF labels. First use forces a backup, either written and read back (--backup-to) or confirmed by typing SAVED, with a loud lost-keys-means-lost-data warning.
  • On the network a sealed blob is a hosting site with one opaque file, published under a per-object key and a random name from a throwaway peer id per session. Replication (3 copies), healing, gateway serving and proof-of-storage challenges all run unchanged over ciphertext. Files over 192 MiB are split into sealed parts whose keys live only inside the sealed root.
  • CLI
    • c0mpute storage put encrypts by default. --public keeps the old plaintext path, and --local keeps ciphertext on this node.
    • New storage get c0p1:... (decrypts here; --raw shows what operators hold), share --with c0ps1... (header-only rewrap), revoke --from (re-seals under a new key and kills old links), link, rm c0p1: (tombstone), ls --private (encrypted local index), and key init|show|backup.
    • Rollback guard: the CLI remembers the newest signed version it has seen of each object and refuses replays.
  • Private sites
    • c0mpute hosting publish --private [--rekey]. Gateways serve a fixed loader at /p/<id>/ under a strict CSP.
    • The browser fetches the ciphertext and decrypts it with WebCrypto using the key from the # fragment, which is never sent to the gateway. It verifies the owner's Ed25519 signature and the blake3 site-id binding, then renders in an opaque-origin sandboxed frame.
    • Downloads are always application/octet-stream. Gateways never decrypt.
  • Encrypted volume: c0mpute storage mount DIR is a FUSE filesystem on Linux, behind the fuse cargo feature (on by default). It uses pure-Rust fuser with no libfuse, so the static musl release build has nothing new to link. Each file becomes a private object, and the directory is one more.
  • OpenDisk descriptor advertises accepts.encrypted: true and c0mpute.private_storage.
  • Docs: docs/private-storage.md (usage and threat model: what operators can and cannot see), DIP-0012 marked implemented, CIP-011 noted as implemented differently, and a new "Private storage" section on c0mpute.com/getting-started.

Review

An independent adversarial crypto review ran before this PR, and every finding was fixed:

  • the previous site key is now taken only from a fully verified, owned version, never from an unverified gateway blob;
  • signed versions plus the local rollback guard;
  • the browser verifies signatures;
  • download blobs are forced to octet-stream;
  • Debug is redacted;
  • multipart revoke publishes the new version before tombstoning the old;
  • put_site keeps shares;
  • dummy slots are real curve points;
  • a slot only counts if it matches the key commitment;
  • verify_strict;
  • padding must be zero;
  • popups are disallowed in the sandbox;
  • the doc is honest about session-level linkability.

Tests

  • c0mpute-sealed, 30 tests:
    • round trip at chunk-boundary sizes;
    • wrong key fails;
    • a tampered chunk fails;
    • reordered chunks fail;
    • truncation and extension fail;
    • sharing to a second identity works;
    • revocation by rekey locks out the old key;
    • a substituted object is rejected;
    • a recipient cannot forge content under the owner's signature;
    • an injected slot cannot shadow the real one;
    • the roster is owner-only;
    • padding buckets;
    • no convergent equality.
  • sealed_store: put/get/share/revoke/delete, multipart reassembly with full revoke, private sites keep their link across versions and --rekey kills it, volumes persist and hide names, and the local index is encrypted.
  • tests/private_storage_e2e.rs, 5 hosting nodes on loopback libp2p:
    • exactly 3 nodes take copies;
    • a scan of every file on every node finds zero bytes of the plaintext marker, file name, content type or owner key;
    • a non-holding gateway serves only ciphertext, and the loader page is static with the locked-down CSP;
    • a holder passes a proof-of-storage challenge on the ciphertext;
    • Bob is denied, then shared in, then revoked;
    • a capability link works with no identity;
    • the object is still served after the owner's client leaves.
  • mount::tests::fuse_write_read_round_trip: a real FUSE mount covering mkdir, write, rename, remount from the store alone, and remove, with the store checked for ciphertext only. CI installs fuse3 and sets C0MPUTE_REQUIRE_FUSE=1, so the test cannot skip silently.
  • Browser loader checked by hand in headless Chrome: a multi-file site renders (CSS, images, scripts, link navigation), the parent origin is blocked from the frame, a 3 MB object passes signature verification, and a wrong key is refused.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

13 finding(s)

HIGH/CRITICAL: 1 | MEDIUM: 8 | LOW: 4

Severity Rule Location
HIGH js-ssrf-outbound-request apps/web/public/sw.js:24
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:53
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:89
MEDIUM sh-remote-script-execution scripts/dev-setup.sh:25
MEDIUM sh-eval-expansion scripts/dev-setup.sh:35
MEDIUM sh-remote-script-execution scripts/install.sh:181
MEDIUM sh-remote-script-execution scripts/install.sh:206
MEDIUM sh-remote-script-execution scripts/install.sh:399
MEDIUM sh-remote-script-execution scripts/install.sh:441
LOW py-ssrf-outbound-request examples/business-strategist-agent/chat.py:37
LOW py-ssrf-outbound-request examples/business-strategist-agent/train.py:54
LOW secret-generic-credential node/crates/c0mpute-core/src/webhook.rs:1165
LOW py-path-traversal node/crates/c0mpute-core/tests/fixtures/fake-infernet-text:29

Snippets are redacted; ThreatCrush never prints matched credential material.

@socket-security

socket-security Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​fuser@​0.18.07210093100100

View full report

@socket-security

socket-security Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

ralyodio and others added 2 commits November 9, 2026 12:32
Wires the c0mpute-sealed format into the network and every surface:

- c0mpute-core::sealed_store: sealed blobs published as single-file hosting
  sites under per-object keys and random names from a throwaway peer id, so
  replication, healing, gateway serving and storage challenges run unchanged
  over ciphertext. Multipart over 192 MiB, share (rewrap), revoke (re-seal,
  new parts first), delete (tombstone), private sites with stable links,
  encrypted volumes, encrypted local index, rollback guard on signed
  versions.
- CLI: `storage put` encrypts by default (`--public` opts out); get, share,
  revoke, link, rm, ls --private, key init|show|backup; mandatory encrypted
  identity backup on first use; `hosting publish --private [--rekey]`;
  `storage mount` FUSE volume (Linux, `fuse` feature, no libfuse).
- Gateways serve a static browser decrypt loader at /p/<id>/ (strict CSP);
  the key stays in the URL fragment, the browser verifies the owner's
  Ed25519 signature and renders in an opaque-origin sandbox.
- OpenDisk descriptor advertises encrypted support.
- Docs: docs/private-storage.md with the threat model, DIP-0012 implemented,
  getting-started "Private storage" section.
- Tests: 3-copy replication e2e scanning every node for plaintext, gateway
  serves ciphertext only, challenge on ciphertext, share/revoke over the
  network; FUSE round trip (CI installs fuse3, C0MPUTE_REQUIRE_FUSE=1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(CodeQL)

fuser < 0.16 has an uninitialized memory read and leak; move the FUSE volume
to 0.18 (&self callbacks behind a Mutex, newtype ids). The browser loader
keeps site paths in Maps so an owner-chosen path like __proto__ can never
reach an object prototype.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sh1pt-actions-fleet
sh1pt-actions-fleet Bot force-pushed the feat/private-storage-e2e branch from 9eb39f1 to e8f0ecc Compare November 9, 2026 12:32
@ralyodio
ralyodio merged commit ce8f3dc into master Nov 9, 2026
12 checks passed
ralyodio added a commit that referenced this pull request Nov 9, 2026
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants