tessera

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 20, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

tessera-go

Server-side Go SDK for Tessera, Ciphera's zero-knowledge identity system: OPAQUE password login (the password never reaches the server) and a generic client-encrypted vault.

Self-reviewed; not yet independently audited. Apache-2.0. See the security model and self-audit before relying on it.

Install

go get github.com/ciphera-net/tessera-go

Requires Go 1.25+ (the golang.org/x/crypto dependency sets the floor; the vault uses the crypto/hkdf standard-library package).

What this SDK is (and is not)

  • Is: a pooled client for the OPAQUE sidecar, an Argon2id blind index, and a versioned AES-256-GCM vault (Seal/Open).
  • Is not: an OPAQUE client (the browser is, via the shared Rust core), a database, or a JWT issuer. Persistence and sessions live in your application (your identity service).

The actors

Step Browser (TS SDK) Your server (this SDK) Sidecar (Rust) Your DB
blind index computes BlindIndex(email) receives it (never the email) stores it
register OPAQUE client msgs relays via RegisterStart/Finish OPAQUE math stores password file
login OPAQUE client msgs relays via LoginStart/Finish OPAQUE math reads password file
vault seals/opens with export_key (optional) Seal/Open for server-held keys stores ciphertext blob

Zero-knowledge rule: the email blind index and the vault export_key are computed client-side. The server stores only the blind index, the OPAQUE password file, and opaque vault ciphertext. Never call BlindIndex on a plaintext email in the request path — it exists for parity vectors and trusted offline tooling only.

Example (login, server side)

// credID arrives from the browser (the blind index); passwordFile is loaded from your DB.
loginID, credResp, err := client.LoginStart(ctx, credReqFromBrowser, &passwordFileB64, credID)
// ... send credResp to the browser, receive the finalization ...
sessionKey, err := client.LoginFinish(ctx, loginID, finalizationFromBrowser)
// sessionKey now matches the browser's; issue your JWT.

CI

The integration test runs the real sidecar + a Rust OPAQUE client-helper; it needs read access to the private ciphera-net/tessera repo via the CI_REPO_TOKEN secret.

Documentation

Overview

Package tessera is the server-side Go SDK for Tessera, Ciphera's zero-knowledge identity system. It provides three primitives:

  • A pooled Unix-domain-socket client for the OPAQUE sidecar (RegisterStart/Finish, LoginStart/Finish). The SDK RELAYS browser-supplied OPAQUE messages to the sidecar; it never computes OPAQUE client math (the single Rust core guarantees cross-environment interop by construction).
  • A deterministic, privacy-preserving blind index (Argon2id) used as the account lookup key and OPAQUE credential_identifier, so the server never sees a plaintext email.
  • A generic client-encrypted vault (Seal/Open) for arbitrary user-private records, using a versioned DEK/KEK AES-256-GCM envelope.

Persistence and session issuance (JWT) are the caller's responsibility (your identity service), not this SDK's.

Index

Constants

View Source
const MaxFrame = 1 << 20

MaxFrame bounds a single wire frame. It MUST match the sidecar's MAX_FRAME (1 MiB).

Variables

View Source
var (
	// ErrUnsupportedVersion is returned by Open for an envelope whose version byte is not
	// recognized (forward compatibility — the version is not secret).
	ErrUnsupportedVersion = errors.New("tessera: unsupported vault envelope version")
	// ErrMalformedEnvelope is returned for a too-short envelope, a wrong key/context, or any
	// failed authentication tag — deliberately not distinguished, to avoid a decryption oracle.
	ErrMalformedEnvelope = errors.New("tessera: malformed or unauthentic vault envelope")
	// ErrEmptyVaultKey is returned when the supplied vault key is empty.
	ErrEmptyVaultKey = errors.New("tessera: empty vault key")
	// ErrEmptyContext is returned when the record context is empty — callers MUST name the record
	// type (e.g. "address", "totp") so each type gets an independent key.
	ErrEmptyContext = errors.New("tessera: empty record context")
)

Functions

func BlindIndex

func BlindIndex(email string) []byte

BlindIndex derives the deterministic, privacy-preserving account lookup key from an email.

SECURITY: in PRODUCTION this MUST be computed CLIENT-SIDE (in the browser). The server must never receive a plaintext email, so it must never call this on request-path input — doing so would defeat the zero-knowledge property by bringing the email into server memory/logs.

It is ALSO a denial-of-service hazard on the server path: each call allocates 64 MiB held for the full Argon2id computation, so even a single network-reachable handler that calls this can be memory-exhausted by a few tens of concurrent requests. This function must never be reachable from a network-exposed handler. It exists in Go only for (a) cross-language parity vectors and (b) trusted offline tooling such as one-time migration jobs. Memory-hard Argon2id is chosen because the index faces an offline, cross-user-amortized enumeration attack over a low-entropy (email) space; its security rests on that COST, not on the (public, shipped) salt being secret.

func BlindIndexString

func BlindIndexString(email string) string

BlindIndexString returns the base64url (unpadded) encoding, suitable for use as the OPAQUE credential_identifier and the database lookup key.

func NormalizeEmail

func NormalizeEmail(email string) string

NormalizeEmail canonicalizes an email for stable, implementation-independent lookup. The order is PART OF THE PARITY CONTRACT: TrimSpace (strip surrounding whitespace) THEN ToLower — the TS/WASM SDK must apply the same sequence. (No Unicode NFC / IDNA punycode folding in v1; a documented known limitation and part of the contract.)

func Open

func Open(vaultKey []byte, context string, envelope []byte) ([]byte, error)

Open reverses Seal. The caller MUST supply the same context used to Seal. It returns ErrUnsupportedVersion for an unknown version, ErrEmptyVaultKey for an empty key, ErrEmptyContext for an empty context, and ErrMalformedEnvelope for a too-short envelope, a wrong key/context, or any failed tag. The empty-key/empty-context guards signal caller error and are independent of envelope content, so they are not a decryption oracle.

func Seal

func Seal(vaultKey []byte, context string, plaintext []byte) ([]byte, error)

Seal encrypts plaintext under a fresh random data-encryption key (DEK), wraps the DEK under a per-context KEK derived from vaultKey, and returns the versioned envelope. context names the record type (e.g. "address", "totp") and is REQUIRED — it is bound into both the KEK derivation and the AAD, and is NOT stored in the envelope (the caller must supply the same context to Open). It may also encode a record identity (e.g. "address:user-123"). vaultKey is key-agnostic (any non-empty key material): browser-held vault keys, recovery keys, or legitimately server-held keys.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a pooled client for the OPAQUE sidecar over a Unix domain socket. It is safe for concurrent use. The pool REUSES up to poolSize idle connections; under burst load it may open additional connections, bounded only by the sidecar's TESSERA_MAX_CONNECTIONS cap (default 256). Size the pool to steady-state concurrency and keep peak concurrency below that cap.

func NewClient

func NewClient(socketPath string, opts ...Option) *Client

NewClient creates a sidecar client for the given UDS path.

func (*Client) Close

func (c *Client) Close() error

Close shuts the pool, closing all idle connections. It is safe to call concurrently with in-flight requests: once Close marks the client closed (under mu), any connection a still-running roundTrip later hands to put is closed rather than parked, so no connection is leaked. The pool channel is deliberately never closed — that would let a concurrent put panic on send.

func (*Client) LoginFinish

func (c *Client) LoginFinish(ctx context.Context, loginID, finalizationB64 string) (sessionKeyB64 string, err error)

LoginFinish finalizes authentication and returns the server's session key (equal to the client's on success).

func (*Client) LoginStart

func (c *Client) LoginStart(ctx context.Context, requestB64 string, passwordFileB64 *string, credentialID string) (loginID, responseB64 string, err error)

LoginStart begins authentication. passwordFileB64 is the stored record, or nil for an unknown account (the sidecar then returns a timing-safe fake response — DO pass nil, never fabricate a record, to keep account existence unobservable).

func (*Client) RegisterFinish

func (c *Client) RegisterFinish(ctx context.Context, uploadB64 string) (passwordFileB64 string, err error)

RegisterFinish forwards the client's RegistrationUpload and returns the password file to persist for this account (input to every future LoginStart).

func (*Client) RegisterStart

func (c *Client) RegisterStart(ctx context.Context, requestB64, credentialID string) (responseB64 string, err error)

RegisterStart forwards the client's RegistrationRequest; credentialID is the blind index.

type Option

type Option func(*config)

Option configures a Client.

func WithDialTimeout

func WithDialTimeout(d time.Duration) Option

WithDialTimeout sets the per-dial timeout (default 5s).

func WithPoolSize

func WithPoolSize(n int) Option

WithPoolSize sets the max number of pooled connections (default 32).

type SidecarError

type SidecarError struct {
	Code    string
	Message string
}

SidecarError is returned when the sidecar replies with an Error frame. Code is a stable error class — "unknown_login", "invalid_credentials", "bad_request", or "internal" — suitable for control flow (HTTP status mapping); Message is human-readable, for diagnostics only.

func (*SidecarError) Error

func (e *SidecarError) Error() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL