Cipherpost is a self-sovereign, serverless, accountless CLI for cryptographic-material handoff over Mainline DHT via PKARR. Hand off a key, certificate, or secret to someone, end-to-end encrypted, with a signed receipt, without standing up or depending on any server.
- No servers. Rendezvous is Mainline DHT via PKARR. No operator, no account, no subpoena target.
- Key is identity. Ed25519/PKARR keypair, passphrase-wrapped on disk (Argon2id + HKDF-SHA256 +
cipherpost/v1/<context>domain separation). - Ciphertext only on the wire. Payload and metadata both encrypted; the DHT sees only opaque blobs.
- Signed receipts. Recipient publishes a signed receipt to the DHT on pickup; the sender can independently verify delivery without a central log.
- Full PRD v1 scope shipped (v1.1):
- typed payloads (
Material::GenericSecret,Material::X509Cert,Material::PgpKey,Material::SshKey) --pinsecond-factor encryption (Argon2id+HKDF→X25519→age, no direct chacha20poly1305 calls)--burnsingle-consumption mode with emit-before-mark state ledger- non-interactive automation via
--passphrase-file/--passphrase-fd - CAS-protected concurrent receipt publication with retry-and-merge contract
- still unimplemented (deferred past v1.1): TUI wizard, non-interactive PIN input (
--pin-file/--pin-fd), destruction attestation — the post-v1.1 effort went into experimental large-payload support instead
- typed payloads (
cargo build --release
# binary: ./target/release/cipherpostRequires Rust 1.88+ (pinned in rust-toolchain.toml). No tokio dependency at the cipherpost layer — uses pkarr::ClientBlocking. Bootstrap nodes are pkarr defaults (Mainline DHT — router.bittorrent.com:6881 and three peers); no user-tunable bootstrap configuration in v1.1.
cipherpost identity generate # prompts for passphrase twice (confirmed)
cipherpost identity show # prints OpenSSH + z-base-32 fingerprintsIdentity lives at ~/.cipherpost/secret_key (mode 0600). Override with CIPHERPOST_HOME.
echo "my-secret" | cipherpost send --self \
-p "backup signing key" \
--material-file -
# → prints a share URI on stdoutcipherpost send --share <recipient-z32> \
-p "onboarding token" \
--material-file ./key.agecipherpost receive <share-uri>
# Prints an acceptance screen on stderr (sender fingerprints, purpose,
# TTL remaining, payload type + size). Type the sender's z-base-32
# pubkey to confirm, or anything else to decline with exit 7.
# Payload goes to stdout, or -o <path>.Repeat runs on an already-accepted share report the prior acceptance timestamp and do not re-decrypt or publish a duplicate receipt (idempotent via state ledger).
cipherpost receipts --from <recipient-z32>
cipherpost receipts --from <recipient-z32> --share-ref <32-hex>
cipherpost receipts --from <recipient-z32> --jsonFetches the recipient's PKARR packet, filters TXT records by the _cprcpt- prefix, verifies each receipt's Ed25519 signature, and renders a structured summary (or a 10-field audit-detail view when filtering by --share-ref).
Status: experimental preview, partial. This is a self-backup tool today:
--selfonly (cross-identity--shareerrors out), and a large-payloadreceivedoes not publish a signed receipt — so the delivery-attestation guarantee small shares get does not extend to large payloads yet. Off-by-default; the CLI surface and wire details may change before it graduates from-alpha. See the full gap list below.
Behind an off-by-default large-payload cargo feature, cipherpost can hand off
arbitrarily large payloads (directories, workspaces, archives) that blow past the
1000-byte DHT wire budget — without adding any operator you don't control.
cargo build --release --features large-payloadThe model is manifest-on-DHT, ciphertext-blob-on-homeserver: the payload is
tar'd, age-encrypted to the recipient (same envelope crypto as small shares), and
uploaded as an opaque ciphertext blob to a pubky homeserver
(self-hostable, pkarr-addressed — the "relay" is a homeserver you run). A tiny
signed manifest carrying only sha256(ciphertext) + size is published to the DHT
via the normal dual-signed flow.
# homeserver URL comes from CIPHERPOST_HS (your own homeserver)
export CIPHERPOST_HS=https://hs.example.com
cipherpost send-large --self -p "vllm workspace backup" ./workspace
# → prints a share URI
cipherpost receive-large <share-uri> -o ./restored
# acceptance screen shows payload size + SHA-256; on confirm, downloads the blob,
# verifies the hash against the signed manifest, decrypts, and unpacks.Talks to the homeserver over blocking HTTPS (ureq + OS-native TLS — no tokio,
no ring/aws-lc in the default tree; the feature adds them only when enabled).
The blob never leaves the sender's machine as plaintext: the homeserver, like the
DHT, sees only ciphertext, and a mismatched hash aborts receive with exit 3.
v2-alpha scope — what is NOT here yet:
--selfonly. Cross-identity--sharefor large payloads is unimplemented and returns an error; today this is self-backup (encrypt a workspace to your own identity, restore it on another machine you control).- No delivery attestation. Unlike small shares, a large-payload
receivedoes not publish a signed receipt. The signed-receipt loop that lets a small-share sender prove pickup (see Verify receipts for shares you sent above) is not wired for large payloads — there is currently no cryptographic proof that a large blob was received. - Live homeserver flow is manual. The real end-to-end homeserver round-trip is covered
by
#[ignore]'d tests run by hand (tests/homeserver_live.rs); CI exercises only the mock-backed round-trip, so the HTTP/auth path is not continuously regression-tested. - Capability-URL exposure. Blobs live under the homeserver's world-readable
/pub/at an unguessable content-addressed path (pubky-homeserver has no writable private space). Confidentiality rests on the age ciphertext + the unguessable path (which travels only inside the encrypted manifest); someone who knows your identity and homeserver could enumerate/pub/to see blob hashes + sizes (never content). SeeTHREAT-MODEL.md§10.
- Dual signatures verified before any decrypt. Every share carries an outer PKARR-packet signature (SignedPacket) and an inner Ed25519 signature over canonical JSON (RFC 8785 / JCS). Tampering at either layer aborts
cipherpost receivewith exit 3 before age-decrypt runs, and no envelope field (includingpurpose) is displayed prior to that check. - Explicit acceptance required. The receiver is shown a full-fingerprint acceptance screen with the sender-attested purpose and must type the sender's z-base-32 pubkey to continue. Declining returns exit 7 with no material written.
- Tamper-zero-receipts. A receipt is published to the DHT only after outer verify + inner verify + typed-z32 acceptance all succeed. Any byte-flip between outer verify and acceptance causes zero receipts to be published (integration-tested).
- Passphrase contract is non-interactive-first.
CIPHERPOST_PASSPHRASEenv var,--passphrase-file <path>(mode 0600/0400), or--passphrase-fd <fd>. Argv-inline--passphrase <value>is rejected — it would leak viaps. - Signature-failure errors are indistinguishable by design. All outer/inner/canonical-mismatch verification failures share one identical user-facing message and exit 3 (defense against distinguishable-oracle attacks — see
THREAT-MODEL.md).
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Generic / unexpected error |
| 2 | TTL expired |
| 3 | Signature verification failed (any layer) |
| 4 | Passphrase incorrect or missing |
| 5 | Payload size cap exceeded (64 KB plaintext) |
| 6 | Network / DHT error |
| 7 | Acceptance declined |
Full taxonomy in SPEC.md § Exit Codes.
- FAQ.md for common questions and answers.
SPEC.md— Protocol specification (wire format, JCS reference vector, share URI, DHT labels, passphrase contract)THREAT-MODEL.md— Adversary model and mitigations (identity compromise, DHT adversaries, acceptance UX, receipt replay, passphrase-prompt MITM)SECURITY.md— Vulnerability disclosure policy (GitHub Security Advisory, 90-day embargo)cipherpost-prd.md— Original product requirements document
All three protocol documents are kept current, including the v2-alpha large-payload additions (SPEC.md §Pitfall #22, THREAT-MODEL.md §10/§10.1). The v1 wire format is unchanged — v1.0 fixtures byte-identical; v1.1 pin/burn fields preserve v1.0 byte-shape via is_false skip-serializing-if; the v2 Material::LargePayload variant is additive.
Cipherpost is a fork-and-diverge from mothballed cclink focused on keyshare workflows. Crypto and transport primitives (Ed25519/PKARR, age, Mainline DHT, Argon2id KDF, dual signatures) were vendored unchanged; the delta is at the payload and flow layer: typed payload schema, explicit acceptance step, signed receipt.
- Wire-budget ceiling for typed Material. Realistic X.509 / PGP / SSH keys exceed the 1000-byte PKARR BEP44 ceiling;
Material::GenericSecretpayloads above ~550 bytes also exceed it. Round-trip tests for realistic typed inputs are#[ignore]'d behind positiveError::WireBudgetExceededclean-error pins. An experimental two-tier escape hatch now ships behind the off-by-defaultlarge-payloadfeature (ciphertext on a Pubky homeserver, tiny signed manifest on the DHT) — see Large payloads (v2) above; chunking / out-of-band variants remain targeted for later (seeSPEC.md§Pitfall #22). - Real-DHT cross-identity round trip is per-release, not per-commit. The cross-identity Mainline-DHT round trip lives at
tests/real_dht_e2e.rsbehind a triple-gate (#[cfg(feature = "real-dht-e2e")]+#[ignore]+#[serial]). PR + push CI stays mock-only — UDP/NAT variance + the 60-90s DHT long tail make per-commit real-DHT testing structurally unworkable (Pitfall #29). Therelease-acceptanceworkflow at.github/workflows/release-acceptance.ymlruns the same gate on everyv*tag push and uploads the output as a 90-day artifact, so each release publishes its own real-DHT evidence next to the tag. The v1.1.0 evidence run (manual demo + automated test, both PASS against pkarr-default Mainline bootstrap nodes) is checked in atRELEASE-EVIDENCE-v1.1.0.md. - No TUI. CLI + non-interactive automation cover v1.x use cases.
- Non-interactive PIN input deferred. PIN is intentionally a human-in-the-loop second factor.
--pin-file/--pin-fdremain deferred, pending a concrete automation use case. - Destruction attestation not implemented. Originally scoped for PRD v1.1; deferred when v1.1 filled with PRD-closure scope, and still unimplemented.
--burnis local-state-only (DHT ciphertext survives until TTL). - No identity import.
cipherpost identity generateis the only path; importing existing Ed25519 / SSH / age keys (cipherpost identity import) is planned for a future release.
MIT — see LICENSE.