Key Transparency Without a Server: Verifying Contacts Offline
Signal built merkle trees and hired two external auditors to detect one specific lie: a server showing different keys to different people. VeilMesh removed the indirection that makes the lie possible — and pays for it in other ways. An honest accounting of what self-authenticating identifiers buy, what they cost, and where Signal is still stronger.
In August 2026, Signal shipped automatic key verification. Behind a single button sits a substantial piece of infrastructure: a transparency log built from a chronological log tree and prefix trees for search, identifiers obscured through a verifiable random function, and two independent auditors — Cloudflare and Trail of Bits — who countersign the log so the server cannot show different data to different people.
It is good engineering, and it solves a real problem. It is also a problem VeilMesh does not have — for a reason worth explaining, because the alternative comes with a bill of its own.
1. What the log is actually for
In Signal, your identifier is a phone number. The mapping from that phone number to your current encryption key lives on the server. That indirection is the vulnerability: a server that can change the mapping can quietly point your contacts at a key it controls, and nothing in the protocol would reveal it.
Everything else follows from that one fact. The merkle trees, the VRF, the two external auditors — all of it exists to make one specific lie detectable: the server told Alice one key and Bob another. Cryptographers call this equivocation.
Signal is candid about the limits. Automatic verification requires knowing your contact's phone number; username-only connections cannot use it. If a contact changes numbers, it stops working. And it does not, in Signal's own words, verify the identity of the user who controls a particular phone number or username. Manual safety number comparison remains necessary.
2. When the identifier is the key
A Veil ID is not a lookup key. It is SHA-256 of your Ed25519 public key. There is no mapping table, on our servers or anyone else's, because there is nothing to map: the identifier is derived from the key itself and can be checked locally by anyone holding both.
So the attack the transparency log detects is structurally absent. A different key produces a different Veil ID — which is a different contact, not the same contact with a new key. No log, no auditors, no network connection required to establish that.
This is the honest version of the "decentralization" claim, and it is narrower than it sounds. Removing the indirection removes exactly one class of attack. It does not remove the rest, and it introduces obligations of its own.
3. The obligation: binding the session key
Here is where the self-authenticating identifier earns its keep — or fails to.
The safety number is computed from Veil IDs, which are hashes of Ed25519 keys. But the X3DH session is built on a separate X25519 key, delivered inside a prekey bundle. In our implementation, that X25519 key is derived from the identity seed directly, not converted from the Ed25519 public key. Neither can be computed from the other.
Which means a recipient cannot verify that the session key belongs to the claimed identity by computation. Only a signature can establish that. If the bundle carries an unsigned session key, the safety number is decorative: the digits match while the session runs on somebody else's key.
VeilMesh now binds them explicitly. The identity_key in every bundle is 64 bytes — X25519 concatenated with Ed25519. On receipt:
- the Ed25519 half is hashed and checked against the claimed Veil ID;
- the signed prekey's Ed25519 signature is verified over a domain separator, the X25519 session key, and the prekey itself.
One signature, both keys. Swap either and verification fails. Bundles that fail are dropped — there is no permissive mode, on any of the three paths that can establish a session: mesh delivery, server fetch, and prekey gossip.
4. What replaces the auditors
Binding proves a key belongs to an identity. It does not tell you whether that identity has been showing the same key to everyone. That is what a transparency log is for, and we do not have one.
What we have instead is the mesh. Every prekey advertisement is Ed25519-signed by its owner and propagates peer to peer. Each device now keeps an append-only record of what it has observed: which key, under which Veil ID, first seen when, last seen when, how many times, and how many hops away.
From that record, one question can be answered locally: have two different keys appeared under the same Veil ID at the same time?
Note the qualifier. A key that stops appearing and is replaced by another is an ordinary rotation — someone reinstalled the app. What is not ordinary is two keys whose observation windows overlap. That means different peers were shown different things, which is precisely the equivocation the log is designed to catch. When it happens, the verification screen stops being informational and starts telling the user not to trust the chat.
5. The layer key transparency does not have
Signal's stated limit is worth repeating: key transparency does not tell you who controls an identifier. It tells you the mapping was not tampered with. Those are different questions, and the second one is the one users actually care about.
VeilMesh answers it from the social graph. Contacts you have personally verified can vouch for others, and the verification screen reports how many of your own verified contacts have vouched for the person in front of you.
This is a heuristic, not a proof, and the interface says so in as many words: vouching is not a substitute for comparing the numbers yourself. A trust signal that overstates itself is worse than none.
6. The digits, and why they changed
The safety number itself needed work. The previous derivation took twelve five-digit groups from four-byte windows advanced two bytes at a time — the windows overlapped, only 26 of the 32 hash bytes were used, and adjacent groups were correlated. There was no version marker and no domain separation.
The current format uses iterated SHA-512 over a domain separator and both sorted identity keys, then twelve non-overlapping five-byte blocks. The iteration count follows Signal's reasoning: in practice people compare the first and last groups rather than all sixty digits, and iteration makes grinding out a key with a plausible-looking number expensive.
The format version is displayed on screen alongside the digits. This matters more than it sounds: when two devices run different versions, the numbers will not match, and a mismatch must be explained by an app version rather than read as an attack.
7. What this costs
Four things this design does not give you, stated plainly.
The first contact is unprotected. Observation history begins when you first meet someone. Nothing in the mesh can vouch for a key you are seeing for the first time. This is trust-on-first-use, and it is a real gap that a transparency log covers better — a log lets a newcomer query history they did not personally witness.
Partitions delay detection. Our evidence of consistency is whatever your device has observed. If an attacker keeps two groups of peers from ever exchanging observations, the contradiction never surfaces. A centrally maintained log with external auditors has no such blind spot: consistency is checked globally, not per-device. On this specific property, Signal's guarantee is stronger than ours.
Identifier exchange still happens out of band. Signal uses phone numbers, which people already know. We use QR codes and pasted IDs, which is more friction and one more step where a person can be fooled.
Vouching is social, not cryptographic. It reflects who your contacts trust, with all the ways that can be wrong.
8. What we ship
Three layers, strongest last, all on one screen:
- Key binding — the session key is provably tied to the Veil ID. Automatic, offline, no server involved.
- Observation history — the key has not changed since a known date, seen across a known number of observations. Automatic; strength grows with how much of the mesh you have met.
- Manual comparison — sixty digits, in person or over a call you trust. The only actual proof.
The automatic layers answer whether a key was swapped. They cannot answer who holds it. The screen says exactly that, because the failure mode of a security feature that overstates itself is a user who stops checking.
Available now on iOS, macOS, and Android.