Open-source zero-knowledge identity: an OPAQUE asymmetric PAKE core plus a client-encrypted vault. Your password never reaches the server — not even hashed — and the key that encrypts your account data is derived on your device and never leaves it.
This repository (ciphera-net/tessera) is the Rust core: the OPAQUE cipher suite, the
tessera-sidecar binary that runs the server side of the handshake, the cross-language
conformance kit, and the specification & audit documents. It powers Ciphera ID, and it is
built to be self-hosted and adopted by anyone.
Note
Security status: self-reviewed; not yet independently audited. Tessera has had a rigorous
internal self-audit (see docs/SELF-AUDIT.md). Read docs/SPEC.md
and docs/THREAT-MODEL.md, and review the code, before relying on it
for anything critical.
- OPAQUE authentication — the password is never sent to the server in any form. Login is a cryptographic proof of knowledge; the server stores only an opaque registration record.
- Client-encrypted vault — account data (e.g. email, profile) is sealed on the device under a Vault Master Key (VMK) the server never holds; the server stores only ciphertext.
- Recovery & re-key — VMK-wrap envelopes (password, recovery phrase, passkey/PRF) let users recover or rotate credentials without the server ever seeing key material.
- Blind index — accounts are looked up by an irreversible keyed hash of the email, so the server can find an account without storing the address.
- Sealed login state — the in-flight server state for a login is handed back to the caller as ciphertext, so any process holding the same ServerSetup can finish a login another one started. The sidecar stays stateless and needs no database of its own.
| Component | Choice |
|---|---|
| OPAQUE | RFC 9807 configuration #1 |
| OPRF | ristretto255-SHA-512 |
| Key exchange | 3DH (TripleDH) |
| KSF (key-stretching) | Argon2id, v0x13, m = 64 MiB, t = 3, p = 1 (pinned, client-side) |
| Vault / VMK-wrap | AES-256-GCM with HKDF-SHA-256, AAD-bound to a version + context |
Parameters are pinned (not library defaults) and verified byte-for-byte across the Rust, Go,
and TypeScript implementations by the conformance kit. All primitives are
delegated to vetted libraries (opaque-ke, curve25519-dalek,
argon2, sha2); the crate is #![forbid(unsafe_code)] and contains no hand-rolled cryptography.
| Repo | What |
|---|---|
ciphera-net/tessera (this repo) |
Rust OPAQUE core + tessera-sidecar + conformance kit + docs |
ciphera-net/tessera-go |
Go server SDK |
ciphera-net/tessera-ts (@ciphera-net/tessera) |
Browser SDK (WASM OPAQUE + WebCrypto vault) |
Published on crates.io:
cargo add ciphera-tesseraThe library is exposed as the tessera crate (use tessera::…); the server-side handshake
binary is tessera-sidecar. To build from source instead, see below.
cargo build --release # builds the library + the tessera-sidecar binary
cargo test # unit + integration (real-socket round-trip) tests
cargo fmt --check && cargo clippy --all-targetstessera-sidecar answers the server side of the OPAQUE handshake over a Unix socket. It requires
a ServerSetup (the long-term OPRF secret), which you generate once per deployment and provide
at startup from your own secrets manager:
# Generate once; store the output in your secrets manager. Keep it 0400 and OFF the repo.
tessera-sidecar gen-setup /path/to/server-setup.bin
# Run the socket server: serve <socket> <setup-path>
tessera-sidecar serve /run/tessera/tessera.sock /path/to/server-setup.binOptional tuning via env vars: TESSERA_FRAME_DEADLINE_MS, TESSERA_LOGIN_TTL_MS,
TESSERA_MAX_CONNECTIONS.
A login is two requests — login_start and login_finish — and the server state between them
has to live somewhere. Historically it lived in the sidecar's memory, which quietly made those two
requests inseparable from one process: a second replica could not finish a login the first one
started, and a restart killed everything in flight.
login_start now also returns state_b64, that state sealed with XChaCha20-Poly1305 under a
key derived (HKDF-SHA512) from your ServerSetup. Store it wherever you already store the
login_id, and pass it back to login_finish:
login_start -> { login_id, response_b64, state_b64 }
store (login_id -> state_b64) in your datastore
login_finish <- { login_id, finalization_b64, state_b64 }
Your datastore holds ciphertext it cannot open, and every replica that loads the same ServerSetup
derives the same key — so any of them can finish any login. This is the only supported way to
run more than one replica. Omitting state_b64 selects the legacy in-memory path, which is
deprecated and only works against the process that served login_start.
Sealed state expires after TESSERA_LOGIN_TTL_MS (default 60s) and is bound to its login_id, so
it cannot be replayed against a different login. It is not single-use on its own — consume
your own login_id record exactly once (e.g. a Redis GETDEL) to prevent replay.
An expired, tampered-with, or mismatched state all return the same unknown_login error code,
deliberately without distinguishing which. Treat that code as "this ceremony is over, start a new
one" — not as a failed password. Counting it as a credential failure means a correct password
can accrue account lockouts.
The ServerSetup is the only long-term server secret. It must never be committed to source control —
.gitignoreexcludes*.bin/server-setup*as a guard. Treat its loss/exposure the way you would any root key.
See docs/: SPEC, THREAT-MODEL,
SELF-AUDIT, PARITY, DEPENDENCIES,
AUDIT-SCOPE, and SECURITY.
Please report vulnerabilities per docs/SECURITY.md (responsible disclosure
to security@ciphera.net). Do not open public issues for security reports.
Licensed under the Apache License, Version 2.0. You may self-host, modify, and redistribute it, including in proprietary products, subject to the license terms.
This distribution includes cryptographic software (OPAQUE / ristretto255 / Argon2id / AES-256-GCM). It is published as open-source, publicly-available software. Under EU Regulation (EU) 2021/821 (the dual-use recast), software that is "in the public domain" / publicly available is generally outside the scope of export controls. Your own local laws may still apply to your use.