cipherpost: Serverless Cryptographic Material Handoff¶
Moving an SSH key, a certificate, or a secret between machines is painful without a server in the loop. cipherpost removes the server entirely — end-to-end encrypted handoff over the Mainline DHT, with a signed receipt so the sender can verify delivery independently.
Why I Built This¶
Moving cryptographic material between parties — SSH keys, X.509 certificates, PGP keys, generic secrets — is a solved problem in theory and a painful one in practice. The common solutions require standing up infrastructure: a shared S3 bucket, an email attachment, a Slack DM, or a secrets manager with accounts, credentials, and a subpoena surface.
The goal was a tool that makes the transfer accountless, serverless, and verifiable without any central operator in the loop. No SaaS. No relay. No logins. The Mainline DHT serves as the rendezvous layer — a globally distributed network with no operator to subpoena. An Ed25519/PKARR keypair is the user's only identity. The recipient publishes a signed receipt to the DHT on pickup, so the sender can independently verify delivery without a central log.
cipherpost is a fork-and-diverge from cclink, focused on generic cryptographic material handoff rather than Claude Code session continuity. The typed payload schema (GenericSecret, X509Cert, PgpKey, SshKey), explicit acceptance step, and signed receipts are the additions built on top of the shared PKARR/age/Argon2id cryptographic stack.
How It Works¶
cipherpost uses a dual-signature model: every share carries an outer PKARR-packet signature and an inner Ed25519 signature over canonical JSON (RFC 8785 / JCS). Tampering at either layer aborts the receive operation before age-decrypt runs. No envelope field is displayed prior to that verification check.
Send / Receive Flow:
Sender → encrypts payload → signs inner JSON → publishes PKARR SignedPacket
↓
Mainline DHT (no operator)
↓
Recipient → resolves packet → verifies outer PKARR sig → verifies inner Ed25519
→ shown acceptance screen (sender fingerprints, purpose, TTL, payload type)
→ types sender's z-base-32 pubkey to confirm
→ payload decrypted → receipt published to DHT
The passphrase contract is non-interactive-first: credentials come from an environment variable, a file, or a file descriptor. The --passphrase <value> argument is intentionally rejected because it would leak the passphrase via ps output. Single-consumption --burn mode exists — the recipient's acceptance is recorded in a local state ledger before the DHT ciphertext is marked consumed, so a crash between those two steps does not silently lose the payload.
Signature-failure errors are indistinguishable by design: all outer, inner, and canonical-mismatch verification failures share one identical user-facing message and exit code 3, defending against distinguishable-oracle attacks. A full seven-code exit taxonomy covers success, TTL expiry, signature failure, passphrase errors, payload size cap, network errors, and acceptance declined.
What I Learned¶
The PKARR BEP44 wire budget is a hard constraint that shaped the entire payload design. Realistic X.509 or PGP keys — and even GenericSecret payloads above roughly 550 bytes — exceed the 1000-byte ceiling. Round-trip tests for realistic typed inputs are skipped in CI behind positive Error::WireBudgetExceeded clean-error pins. Two-tier storage or chunking is targeted for a future release; the v1.1 scope shipped the protocol correctly within the constraint rather than papering over it.
Real-DHT cross-identity testing is structurally incompatible with per-commit CI. UDP and NAT variance combined with a 60–90 second DHT propagation tail make per-commit real-DHT testing unworkable. The solution was a release-acceptance workflow that runs the full cross-identity round trip on every version tag push — so each release publishes its own real-DHT evidence alongside the tag. The v1.1.0 evidence run passed against pkarr-default Mainline bootstrap nodes.
The explicit acceptance step — requiring the recipient to type the sender's full z-base-32 pubkey to confirm — was the hardest UX decision. It creates friction. It also prevents a class of attacks where a malicious DHT packet tricks a recipient into auto-accepting material from an unexpected sender.
This section is intentionally thin — there is more to say here once the two-tier storage work and broader ecosystem experience lands. John will expand this on publish.
Links¶
No server, no account, no operator — just cryptography and a global DHT.