WP-02 — The Invite Protocol.
Roughly 12 minutes.
How Lattice replaces "share your Bullet ID via WhatsApp" with a properly cryptographic invite scheme that survives ordinary network adversaries. The math, the trade-offs, and the threat model specifically for the invite flow.
Authors: Lattice project · Version: 1.0 (draft) · License: CC-BY-4.0 · Status: draft, frozen at v1.0.
1. Why we need this.
The simplest way to bootstrap a contact relationship in Lattice is in person: scan a QR code from each other's phones, compare the eight fingerprint words aloud, and you have a mutually-verified link. This is what we'd recommend for the people you live with.
For everyone else — the relative across the country, the friend overseas, the colleague on a different continent — physical co-presence is impossible. Some non-physical channel has to do the bootstrap. The naive approach is "send each other your Bullet IDs over WhatsApp," but that has two problems:
- Lattice's wallet-style identity model means the Bullet ID itself is a 32-byte hash of public keys, displayed to humans as
XXXX-XXXX-XXXX. An attacker on WhatsApp who substitutes that ID for a different one — also a valid format — and persuades the user to add the wrong contact has succeeded; the user has no easy way to detect the substitution without a separate channel. - The handshake afterwards is unauthenticated unless the user manually compares fingerprints on a different channel, which is an error-prone manual step that most users will skip.
Lattice Invites fix both: the link is cryptographically self-authenticating against tampering, and the verification step is automatic and visible (four words on each screen) rather than manual and hidden.
2. The construction.
2.1 Components.
On the inviter's device:
────────────────────────
- long-term Ed25519 intro key (signing)
- fresh ephemeral X25519 keypair, generated for THIS invite
On the invite token (on the wire):
──────────────────────────────────
- inviter's Ed25519 public key (32 B)
- ephemeral X25519 public key (32 B)
- inviter's short Bullet ID (cosmetic — auth path is the keys)
- inviter's display name
- issued_at (unix secs)
- expires_at (unix secs)
- optional label (for inviter's own reference)
- family_pack_id (empty for single-contact invites)
- 64-byte Ed25519 signature over the canonical CBOR of all the above
In the URL:
───────────
lattice://invite/
Computed on both sides (NEVER transmitted with the token):
──────────────────────────────────────────────────────────
four_words = wordlist[ SHA256(eph_pub || "lattice-invite-v1")[:4] ]
(one byte per word)
2.2 The two-channel principle.
The link travels on channel A — WhatsApp, SMS, email, Signal, Slack, anything that can deliver a URL. The four-word verification phrase is computed on the inviter's screen and on the invitee's screen independently, never transmitted alongside the link. The inviter speaks (or types into a different app entirely) channel B, typically a phone call or a face-to-face conversation, and asks "what four words do you see?". If the words match on both sides, no man-in-the-middle is mounting an attack; if they don't, abort.
This is the SAS (Short Authentication String) pattern, the same one used in ZRTP voice encryption, in Signal's safety numbers, in WhatsApp's security codes. Lattice's contribution is bringing it to a frictionless drop-link scenario by deriving the SAS deterministically from the link content rather than from a session.
3. Entropy of the four-word phrase.
Four words from a 256-word list = 8 bits per word × 4 = 32 bits. ~4.29 × 10⁹ possible phrases. The probability of two random ephemeral keys producing colliding phrases is 2⁻³² ≈ 2.3 × 10⁻¹⁰.
An attacker mounting a real-time MITM has to find a substitute ephemeral pubkey that produces the same four words as the legitimate one. By the birthday bound, that requires ~2¹⁶ ≈ 65,000 attempts on average — and each attempt is a fresh keypair generation and a SHA-256, costing ~milliseconds on a phone. So an attacker with a phone can compute a colliding key in ~minutes if they can produce arbitrary keys.
But producing arbitrary keys isn't enough — the attacker also needs to sign the substituted invite token with the inviter's long-term Ed25519 intro key, which they don't have. They have one of two paths:
- Compromise the inviter's intro key. This is end-of-game; nothing in the invite layer protects against it.
- Strip the signature, generate their own fresh inviter identity that happens to have a colliding ephemeral phrase, and substitute the entire invite with their own. This produces a different long-term Ed25519 inviter pubkey, which the invitee will display on their screen, and which the inviter can ask about ("what's the contact name on your screen?"). If the inviter pre-warned the invitee that they'd be sent an invite from this Bullet ID, the substitution is caught.
So the four-word phrase is one defence among several. The protocol layered defences:
- Ed25519 signature on the token by the inviter's intro key — defeats invite-substitution by anyone without that key.
- Four-word phrase from the ephemeral pubkey — defeats invite-substitution where the attacker has stripped and regenerated the entire token.
- Display-name and short-Bullet-ID display on the invitee's screen — gives a second cross-check if the inviter mentioned them out of band.
Honestly: against a casual interceptor (a network attacker who can read the WhatsApp message but cannot mint colliding keys quickly), the four-word phrase alone is sufficient. Against a dedicated active attacker controlling the link channel and willing to spend computation, the layered defences hold. Against an attacker controlling both the link channel and the verification-phrase channel (e.g. WhatsApp and the phone call), the scheme is defeated; the user is told this in onboarding and the docs.
4. The invite-specific threat model.
4.1 Adversary capabilities and outcomes.
| Adversary | What they can do | Outcome |
|---|---|---|
| Passive observer of channel A | Reads the link in transit (e.g. seeing a WhatsApp message that contains it) | Learns the inviter's intro pubkey + Bullet ID + display name. Cannot impersonate. Cannot decrypt subsequent messages because the ephemeral X25519 + the recipient's static keys both contribute to session derivation. |
| Active modifier of channel A | Reads + modifies + replays the link | Modification produces an invalid Ed25519 signature; rejected at verify. Replay is bounded by single-use semantics — once redeemed, the receiver records the ephemeral pubkey and rejects re-use. Time-bounded by the expires_at field. |
| Active modifier of channel A, willing to mint colliding keys | As above + willing to spend computation finding a colliding ephemeral pubkey | Caught by the inviter Ed25519 signature in 99.999% of cases — the attacker doesn't have the inviter's intro key. Substitution requires either stealing that key (out of scope of this layer) or substituting the whole inviter identity (caught by short-ID / display-name cross-check via channel B). |
| Compromise of channel B | Reads + modifies the verification call | Defeats the four-word check. Mitigated by the user's choice of channel B — a face-to-face conversation, a long-standing voice call to the same number, etc. We tell the user explicitly: if both channels are compromised, meet in person. |
| Compromise of inviter's intro key | Can sign anything as the inviter | End-of-game; the protocol cannot help. |
4.2 Family Pack threat model.
A Family Pack is N invitations that share the same four-word verification phrase. The phrase is derived from a single shared ephemeral pubkey; each invite references the same ephemeral_x25519_pub. The reasoning: a prepper communicating four words once at Sunday lunch covers an entire family in one go.
This sacrifices per-invitee independence — if the four-word phrase is compromised once, it's compromised for the whole pack. But the alternative ("call each family member separately and recite a different phrase") is the friction that prevents mass adoption. We trade some theoretical security for actual deployment.
Mitigation: family_pack invites are bounded to a smaller TTL (default 72h, max 7 days) and are explicitly labelled as "shared verification phrase" in the inviter's UI so they understand the property. Invitees see the same explanation when they accept.
5. Wire format and CBOR canonicalisation.
The signed body uses CBOR with deterministic encoding rules from RFC 8949 §4.2 (canonical encoding). All field names are well-known strings; integer encoding uses the smallest representation; floating-point is not used. The signature covers the body bytes prefixed by the domain separator "lattice-invite-sig-v1" to prevent cross-protocol signature reuse.
bytes_to_sign = "lattice-invite-sig-v1" || canonical_cbor(InviteTbs)
InviteTbs = {
version: 1,
ephemeral_x25519_pub: bstr 32,
inviter_intro_pub: bstr 32,
inviter_short_id: tstr,
inviter_display_name: tstr,
issued_at_unix_secs: uint,
expires_at_unix_secs: uint,
label: tstr,
family_pack_id: tstr,
}
signature = Ed25519.sign(inviter_intro_priv, bytes_to_sign)
token = InviteTbs ⊕ { signature: bstr 64 }
url = "lattice://invite/" || base64url_no_padding(canonical_cbor(token))
6. Web fallback.
An invitee who taps a lattice://invite/... link without Lattice installed is bounced to lattice.fyi/invite/ which detects platform and shows install links. The full invite token sits in the URL fragment (after the #), which browsers do not send to servers. The web page's static JavaScript reads the fragment in the user's browser, displays the inviter's name and short ID, and asks the user to install Lattice and re-tap the link afterwards.
The server hosting lattice.fyi/invite/ never sees the secret payload. This is verifiable by inspecting network requests in the browser's developer tools.
7. Single-use and revocation.
Single-use is enforced by the receiver, not by the protocol — the receiver records the ephemeral_x25519_pub of every accepted invite and rejects any subsequent invite with the same key. There is no central revocation list because there is no server.
The inviter can also revoke a pending invite locally, before redemption, by removing it from their pending-invites list. The token already in transit is then orphaned: the receiver who taps it gets a "this invite has been cancelled" message because no inviter is listening for the corresponding ephemeral key.
8. References.
- RFC 7748 — X25519 / Curve25519 ECDH.
- RFC 8032 — Ed25519 signatures.
- RFC 8949 — CBOR (canonical encoding).
- RFC 6347 / 8446 — DTLS / TLS, for comparison of two-channel patterns.
- Signal X3DH protocol — comparable bootstrap pattern, server-mediated.
- RFC 6189 — ZRTP §6 — SAS authentication.
- WP-01 — Threat Model. Where the invite flow sits in the larger Lattice threat model.
- WP-05 — What Lattice Doesn't Do.