Skip to content

Per-Recipient Blinded Pseudonyms

A Schnorr zero-knowledge proof hides the sender’s secret key, but the proof is bound to a public key Y — and ifY is the sender’s long-term identity, every proof is linkable to that identity. Tessera solves this with per-recipient blinded pseudonyms: each delivery uses a fresh public keyY′ = Y + t·G, so the proof is on Y′, notY, and the long-term identity never appears on the wire.

The problem: ZK hides the key, but reveals the public key

A Schnorr proof of knowledge of x (whereY = x·G) reveals nothing about x. But the proofcontains Y — the verifier must checks·G == R + c·Y, which requires Y. IfY is the sender’s stable identity, then every proof an adversary observes is linkable: this proof is about Y, that proof is also about Y. The adversary has built the social graph without ever learning x. Zero-knowledge of the secret is not enough; you also need unlinkability of the public key.

The solution: per-delivery blinded pseudonyms

For each delivery, the sender derives a fresh blinded pseudonym:

Y′ = Y + t·G,   t = H(shared_seed ‖ session_id) mod q

The sender then proves knowledge of x′ = x + t (notx), because Y′ = (x + t)·G = x·G + t·G = Y + t·G. The proof is a normal Schnorr / Fiat–Shamir proof on Y′; the long-term Y never appears in the proof or on the wire.

The recipient, who holds the sender’s long-term Y and theshared_seed from enrolment, recomputes t from the session_id (carried in the delivery envelope), checks that Y′ == Y + t·G, and only then verifies the Schnorr proof against Y′. If the proof verifies, the recipient knows the delivery came from the contact whose (Y, seed) they hold — without ever having seen Y on the wire.

The blinding scalar

t = H(shared_seed ‖ session_id) mod q is deterministic given the seed and the session id, but pseudorandom to anyone who does not know the seed. It is computed from:

  • shared_seed — a secret established once, pairwise, during enrolment (see below). Different recipient → different seed.
  • session_id — a fresh per-delivery value (e.g. a counter or random nonce). Different delivery → different session_id → different t.

Because t is the output of a hash modeled as a random oracle, Y′ = Y + t·G is uniformly distributed in the group for anyone without the seed: the blinding shifts Y by a pseudorandom group element, producing a fresh, unlinkable point per delivery.

Cross-recipient unlinkability

Each recipient holds a distinct shared_seed. When the sender delivers to recipient A (seed s_A) and recipient B (seed s_B), the two blinded pseudonyms areY′_A = Y + H(s_A ‖ id)·G andY′_B = Y + H(s_B ‖ id)·G. Because s_A ≠ s_B and the hash is a random oracle, Y′_A and Y′_B are independent uniform group elements — an adversary observing both cannot tell they came from the same sender.

Cross-delivery unlinkability

Even to the same recipient, two different deliveries use different session_id values, hence differentt, hence different Y′. An adversary observing two deliveries to the same recipient sees two independent uniform group elements — it cannot link them to the same sender (only the recipient can, by recomputing t).

What each party learns

What an adversary sees

A network observer or a coalition of compromised relays sees, per delivery, a uniformly random Y′, a Schnorr proof bound toY′, and a routing commitment. None of these link back to the sender’s long-term Y, and none link two deliveries to each other. The adversary’s view is fresh random noise per delivery.

What the recipient learns

The recipient uses its stored (Y, seed) for each known contact to try to reconstruct Y′ from thesession_id. For the contact that actually sent the delivery,Y′ == Y + t·G matches and the Schnorr proof verifies. For every other contact, the check fails. The recipient therefore learnswhich of its known contacts sent this delivery — which is the intended authentication — and nothing more.

Enrolment: one-time, pairwise, out-of-band

Before any delivery, the sender and recipient must share ashared_seed and the recipient must store the sender’s long-term Y. This is a one-time, pairwise, out-of-band step — analogous to Signal’s safety number exchange or PGP key signing. In Tessera this is done once per (sender, recipient) pair and never repeated unless the relationship is reset; after enrolment, an unlimited number of deliveries can be made, each with a freshsession_id and hence a fresh Y′.

Enrolment does not require a central authority: it is purely pairwise and can be done over any authenticated channel (in person, via QR code, via a pre-shared passphrase, etc.).

Code example: BlindedSender / BlindedVerifier

from tessera.crypto.blinding import BlindedSender, BlindedVerifier

# --- Enrolment (one-time, out-of-band) ---
# Sender and recipient share `shared_seed` and the recipient stores sender's Y.
# (omitted — application-specific transport)

# --- Sender side: produce a blinded proof for one delivery ---
sender = BlindedSender(secret_key=x, public_key=Y, shared_seed=seed)
envelope = sender.prove(session_id="delivery-7f3a")
# envelope contains: Y', session_id, Schnorr proof, AES-GCM ciphertext

# --- Recipient side: authenticate the delivery ---
# recipient holds (Y, seed) for this contact
verifier = BlindedVerifier(public_key=Y, shared_seed=seed)
contact_id = verifier.authenticate(envelope)
# contact_id is None if this delivery is not from the expected contact,
# otherwise the proof verified against Y' = Y + t·G.

The envelope is routed via bucketed broadcast; the recipient filters by Bloom fingerprint (see Bucketed Broadcast Routing) and runs authenticate on matches.

Comparison: Tessera blinding vs Tor vs Signal

SchemeLinkabilityTrust modelKeysScope
Tessera blindingNo (uniform Y′ per delivery)Pairwise enrolmentPer-delivery Y′ = Y + t·GCross-recipient & cross-delivery
Tor hidden servicesAddress is stableNo enrolmentStable onion addressHides server, not client pattern
Signal ratchet keysPer-message ratchet stepSafety-number exchangeX3DH + Double RatchetForward secrecy, not metadata

Tor hides the server behind a stable onion address but does not hide the client’s traffic pattern. Signal’s ratchet gives forward secrecy but the server still sees who talks to whom. Tessera’s blinding hides the sender’s identity per delivery from everyone except the intended recipient.

Frequently asked questions

If the recipient can recompute Y′, why can't a relay?

Recomputing t = H(shared_seed ‖ session_id) mod q requires the shared_seed, which is established out-of-band between sender and recipient only. A relay sees the session_id (it is in the envelope) but not the seed, so it cannot reconstruct t or Y. Only the recipient, who stored the seed during enrolment, can compute t and check Y′ = Y + t·G.

Does blinding break the zero-knowledge property?

No. The Schnorr proof is still a standard ZK proof — it is simply computed on the blinded key Y′ and proves knowledge of x′ = x + t rather than x. The simulator for the blinded proof is identical to the standard Schnorr simulator applied to Y′. Zero-knowledge, soundness, and completeness all carry over because the blinding is a group homomorphism.

What happens if two recipients collude?

Each recipient holds a distinct shared_seed, so each computes a distinct t and a distinct Y′ for the same delivery. Two colluding recipients see two independent uniform group elements and cannot link them to the same sender without additional information. The cross-recipient unlinkability guarantee holds under the assumption that seeds are distinct and the hash is a random oracle.

See the full blinding lemma

The formal proof of cross-recipient and cross-delivery unlinkability is in the Tessera research notes and the protocol docs.

pip install tessera