Learn
Pollis's whole pitch is simple: don't trust the operator, verify. This section takes you from "what is a key" to "I checked the log myself", in plain language, with animations you can watch and widgets you can try.
- FOUNDATIONSWho can see what
- FOUNDATIONSKeys, encryption, signatures, and hashes
- MLSGroup chats, made efficient
- MLSJoining, leaving, and your history
- MLSA stolen key is a window, not a door
- IDENTITYMaking sure you have the right person's key
- TRANSPARENCYMerkle trees and append-only logs
- TRANSPARENCYProving an entry is really in the log
- TRANSPARENCYPollis's three logs, and what each one protects
- TRANSPARENCYBinary transparency, and reproducible builds
- TRANSPARENCYVerify it yourself
- TRANSPARENCYReading the artifacts and Security pages
Who can see what
On most messaging apps, your privacy rests on a company keeping a promise. The trouble is that a company can be hacked, bought, or handed a court order, and none of those things care what it promised. "We would never read your messages" is a statement about intent, and intent can change.
Pollis is built differently. Reading your messages is not something we choose not to do. It is something we are not able to do. Here is the plain map of who can see what.
Your device sees everything. That is where you type and read, so your messages are unscrambled there and nowhere else. The flip side: if someone gets into your unlocked device, they can read what is on it, and no clever maths can prevent that.
Our servers see the outside of the envelope, never the letter inside. They carry sealed messages we cannot open, because the keys to open them never leave your devices. What they can see is ordinary delivery information: that an account sent something, roughly when, roughly how big it was, and which conversation it belongs to. That is real information, and we would rather tell you about it than pretend it is not there.
Getting the right key to the right person is the part that takes care. Encryption only helps if the key your app uses really belongs to the person you meant. Safety numbers (Topic 6) and our public key logs (Topic 9) are what let you confirm that yourself, rather than take our word for it.
The pattern runs through everything here: we keep shrinking the list of things you have to take on trust, and we publish evidence for whatever is left.
A few things sit outside what any messenger can protect, and it is fairer to name them. Someone holding your unlocked device can read what is on it. The delivery information above, sometimes called metadata, is visible to whoever carries your traffic. And a brand new device starts empty, because there is no history backup (that trade is explained in Topic 4). Everything else, we work to protect, and to let you check.
Full transcript
Most messaging apps ask you to trust a company. Not its promise, its ability. A company that means well can still be hacked, bought, or ordered by a court. So Pollis is built so that reading your messages is not something we choose not to do. It is something we cannot do.
Your message is sealed on your device before it goes anywhere. Our servers pass it along without ever holding the key. It opens on your friend's device.
We cannot see what is inside. We can see the outside: that your account sent something, when, roughly how big, and which conversation it belongs to. That is called metadata, and it can be sensitive on its own, so we are not going to pretend it does not exist.
There is also a subtler point. Encryption only helps if you are using the right key for the right person. If the wrong key ever slipped in, a message could be read in the middle without breaking any of the maths. That is exactly what safety numbers and our public key logs let you check.
Here is the full picture. Green, we cannot see. Amber, we can, and we say so. Red is outside what any messenger can defend: your own device, and plain software bugs.
Everything else in Learn is about shrinking how much of the green depends on trusting us, and publishing evidence for whatever is left.
The rigorous version: docs/security-whitepaper.md §1.1, and the trusted/untrusted split in CLAUDE.md → "Security model".
Keys, encryption, signatures, and hashes
Four words show up on every page that follows. None of them needs any maths to understand.
A key is just a very large number. Large enough that guessing it is not a matter of being patient, it is a matter of there not being enough time in the universe. When we say a key never leaves your device, we mean that number stays in your device's secure storage and is never sent to us.
Encryption is a lock that scrambles. Put your message in with a key and out comes noise. Put the noise back in with the right key and your message returns. With the wrong key you get nothing back, not a blurry version, not most of it. Nothing.
The clever part is that a key comes in two halves that are different from each other. One half is public and one is private. Anything locked with the public half can only be opened with the private half. So you can hand your public half to anyone, even publish it, while the private half stays on your device. Now anyone can send you something only you can open, and you never had to agree on a secret in advance.
Signatures are the same idea in reverse. Lock something with your private half and anyone holding your public half can confirm it really came from you and was not altered. Change a single character and the check fails. Pollis uses signatures constantly, so you can tell that a record genuinely came from the right place.
A hash is a fingerprint for data. Feed in anything, of any size, and get back a short string. The same input always gives the same fingerprint, and changing one character anywhere gives a completely different one. You cannot work backwards from the fingerprint to the original. That is what lets you say "this is exactly the file I meant" without publishing the file. When you see a long root code on our artifacts page, that is a hash.
Strong encryption is not the fragile part, and it almost never is. The parts that matter in practice are where keys are kept, whether a public key really belongs to the person it claims to, and whether the app doing the locking is honest. Those three questions are what the rest of this section is about.
Full transcript
A key is a very large number. Large enough that guessing it is not hard, it is out of reach, because there is not enough time to try.
Encryption is a lock. Your message goes in with a key and out comes noise. Put the noise back in with the right key and your message returns. Wrong key, and you get nothing. Not a blurry version, not most of it. Nothing.
Now the idea that makes this work between strangers. Your key comes in two halves. One is public, you hand it to anyone. One is private, and it never leaves your device. Anything locked with the public half can only be opened by the private half. A stranger can lock a message to you, and then cannot open it themselves.
Run the same idea backwards and you get signatures. Mark something with your private half and anyone with your public half can confirm it came from you and was not changed. Change one word and the check fails.
Last idea: a hash. It is a fingerprint for data. Any file, any size, in, and a short string out. Same file, same fingerprint, every time. Change one character and it is completely different. Try it yourself in the box below.
Key, encryption, signature, hash. Everything else is those four ideas arranged. And the maths is not the weak point. What matters is where the keys are kept, whether a public key really belongs to who it says, and whether the app is honest. That is what the rest of Learn is about.
The rigorous version: the Ed25519 keys and OS-keystore storage in auth.rs, the device-signed writes in ds_client.rs, and the SHA-256 hashing in merkle.rs.
Group chats, made efficient
Locking a message for one person is simple. Groups are where it gets interesting.
The obvious way is to encrypt the message separately for every member. Five people means four copies. A hundred people means ninety-nine copies of every message you send. It works, but it gets slow and expensive fast. And when someone leaves, everyone needs fresh keys, which means everyone coordinating with everyone.
Pollis uses a standard called MLS, short for Messaging Layer Security (RFC 9420, a published internet standard). It arranges the group as a tree.
Picture a pyramid. Every member is a box along the bottom. Boxes pair up into boxes above them, and so on up to a single box at the top. Each box holds a key, and the rule is simple: you know the keys on the straight line from your own box up to the top. The key at the very top is shared by everyone, and that is the key that protects the group's messages.
The payoff comes when something changes. Whoever makes the change only has to send one small update along their line to the top, and each part of the group can unlock the piece it needs from what it already knows. The work goes from one message per member down to about the height of the tree: roughly seven steps for a hundred people, not ninety-nine. For a thousand people, about ten.
Two words you will see throughout Pollis. An epoch is the group's version number; it ticks up every time the keys change. A commit is the update that makes it tick, such as adding someone or refreshing keys. Everyone has to apply commits in the same order, and keeping that order right across every device and reconnection is one of the hardest parts of building Pollis. It is also why every commit is written to a public log you can check (Topic 9).
The tree protects the contents of messages and who is in the group. It does not hide the fact that a group exists, or that messages are flowing, which is the metadata point from Topic 1. And it assumes every device applies updates in the same order; when that slips, the effect is a device that cannot read new messages until it catches up, not a leak.
Full transcript
Locking a message for one person is easy. Groups are where it gets interesting. The obvious approach is to encrypt the message separately for every person. Five people, four copies. A hundred people means ninety-nine copies of every message. And when someone leaves, everyone needs new keys.
MLS arranges the group as a tree instead. Everyone sits along the bottom. Boxes pair up all the way to a single box at the top. Each box holds a key, and you know the keys on the line from your box up to the top. The top key is shared by everyone, and it protects the group's messages.
When something changes, the person making the change sends one small update along their line, and each part of the group unlocks the piece it needs from what it already knows. Ninety-nine messages becomes about seven.
Leaving works the same way. When a member is removed, the keys along their line are replaced. The next message goes out, and for them it stays sealed. They still hold the old key, and it opens nothing.
Two words you will see everywhere. An epoch is the group's version number. A commit is the update that makes it tick.
Everyone applies those updates in the same order. If a device misses one, it falls a step behind and cannot read what comes next until it catches up. Keeping that order right is one of the hardest parts of the app, and it is why every commit is written to a public log, which is where we go next.
The rigorous version: RFC 9420 itself, our OpenMLS usage in mls.rs, and the ordering invariant in docs/transparency.md.
Joining, leaving, and your history
To add you to a group, someone has to lock the group's current keys so that only you can open them, and they need to do it even while you are offline. So every Pollis device leaves a small locked box for itself in advance, called a key package. Think of a self-addressed lockbox at a front desk: anyone can drop something in, only you can open it. When someone adds you, they use that lockbox to seal up the group's current state and send you a welcome. You open it with your private half, and you are in.
Here is the part that surprises people. The welcome gives you the keys as they are right now. It does not give you the keys from before you joined, and it cannot, because the design deliberately makes new keys unable to reveal old ones. That is the same property that makes removing someone actually work.
So messages sent to a group before you joined are not readable by you. Not hidden by the app, genuinely unavailable. Every serious app built on this standard works the same way. It is the price of removal meaning something.
Leaving. When a member is removed, everyone else moves to fresh keys, and from that point on the departed device cannot read anything new. Whatever it already saw stays on it; we cannot reach into someone else's machine. Removal here is a property of the keys, not a setting on a server you have to trust.
Your own devices work the same way. Your phone and your laptop are separate members of the group, each with their own keys. Nothing is copied between them, because a key that travels is a key that can be stolen along the way. Adding your laptop is just like adding a person.
Which leads to the one sharp edge, stated plainly: a new device starts empty. There is no history backup. A new device joins from today forward. Your old messages live on your old device and nowhere else, not on our servers, which only ever held sealed envelopes, and not in a backup, because we do not make one.
We could offer one. The usual approach is to store your history on the server, protected by a PIN or passphrase. But that recreates the exact thing this app exists to avoid: a copy of your conversations sitting on our infrastructure, guarded by something short and guessable. So we do not do it. If you want your history on a new device, keep your old device. That is a real cost, and we would rather say so than hide it.
Two kinds of loss are expected: messages sent before you joined, and a new device starting empty. Everything else should just work, including delivery to every current member, going offline and back, reconnecting, and accepting a message request. If a message goes missing for any other reason, that is a bug and we treat it as one.
Full transcript
To add you to a group, someone needs to lock its keys so only you can open them, even while you are offline. So every device leaves a small lockbox in advance. Anyone can drop something in. Only you can open it.
When you are added, an existing member seals the group's current state into your lockbox and sends it over. You open it, and you are in.
Now the part people do not expect. You received the keys as they are now, not the older ones. New keys cannot be used to work out old ones, and that is the very thing that makes removing someone work.
So messages sent before you joined stay sealed. It is not that the app is hiding them. The key genuinely does not fit.
Removing someone is the same idea. Their line of keys is replaced, and the next message is sealed to them. What they already saw stays on their device. We cannot reach into it.
Your own devices work the same way. Your phone and laptop are separate members, each with their own keys. Nothing is copied between them, because a key that travels can be stolen along the way.
Which brings up the trade we would rather you hear from us. A new device starts empty. There is no history backup. We could store your history on our servers behind a PIN, but that would put a copy of every conversation on our infrastructure behind a short, guessable code, which is exactly what this app exists to avoid. So we do not. If you want your history, keep your device.
What we do promise is narrow and firm. Two kinds of loss are expected: messages from before you joined, and a new device starting empty. Everything else has to arrive, and anything else going missing is a bug.
The rigorous version: the enrollment and GroupInfo flows in the MLS wiki article, and the two-acceptable-losses policy stated verbatim in CLAUDE.md → "Messages must work; history is bounded, not flaky".
A stolen key is a window, not a door
Suppose the worst has happened and someone lifts a key off your device today. What did they actually get? Two questions follow, one pointing backward in time and one pointing forward.
The past: forward secrecy. The answer is nothing. Keys only move forward, through a one-way step. Each new key is made from the previous one by a process that cannot be run in reverse, like mixing two paints and being unable to separate them again. Used keys are thrown away. So a key stolen today opens today's messages, and yesterday's are already out of reach, because the material that made them no longer exists anywhere.
One thing to be precise about: this protects the scrambled traffic, not your screen. If yesterday's messages are still on display in your app, someone holding your unlocked device can read them off the screen. The real guarantee is that someone who quietly recorded your encrypted traffic months ago still cannot open it, even with a key stolen today. That is a strong promise.
The future: post-compromise security. This is the less familiar and more surprising half. Normally a stolen key means you stay compromised until you notice and start over. Here, the group heals on its own. Every ordinary update, such as adding a member or a routine key refresh, replaces keys along part of the tree. As soon as a member the attacker does not control makes an update, the stolen key stops working, and nobody has to notice anything for that to happen.
This is why the tree from Topic 3 was worth the effort. Because rekeying is cheap, it happens often, and the more often it happens, the shorter the window in which a stolen key is any use. A stolen key is a window, not a door: it does not open backward, and it closes on its own.
If someone keeps continuous access to your device, they receive every update you do and stay in, so this recovers from a break that has ended, not one still in progress. Anything already unscrambled and sitting on your device can be read by whoever holds the device. Neither property touches metadata. And healing happens through activity, so a very quiet group refreshes its keys less often. None of this means messages delete themselves; it means a stolen key only opens a limited slice of time.
Full transcript
Suppose the worst has already happened. Someone has a key from your device, today. What did they get? Two questions: what about the past, and what about the future.
The past first. They try yesterday's messages and get nothing. Keys move forward in one-way steps. Each key is made from the one before it, through a process that cannot be reversed, like mixing paint. And once a key is used, it is thrown away. Yesterday's key does not exist anywhere anymore.
One point, because this gets overstated. If your old messages are still on your screen, someone holding your unlocked device can read them. Forward secrecy protects recorded traffic: someone who captured your data months ago still cannot open it with today's key.
Now the future. Normally a stolen key means you are compromised for a long time. Here, someone else in the group, someone the attacker does not control, does something ordinary, like refreshing their keys. New keys flow along the tree, and the stolen key stops working. Nobody had to detect anything. The group healed by carrying on.
Sealed on the left by forward secrecy. Sealed on the right by the next update. A stolen key is a window, not a door, and the more the group talks, the narrower it gets.
The limit: if someone never loses access to your device, they get every update you do and stay in. This recovers from a break that ended, not one still happening.
The rigorous version: the key schedule in RFC 9420 §8, the epoch advance in the MLS wiki article, and the local-storage caveat in CLAUDE.md → "Storage".
Making sure you have the right person's key
Encryption protects your message. On its own, it does not tell you who is on the other end.
When you first message someone, your app fetches their public key from our servers. That is the one moment worth being careful about. If the wrong key were ever handed over, a message could be read in the middle, and both sides would still see a padlock, because the encryption itself is working perfectly, just on the wrong key. Every encrypted messenger has this same gap. Pollis closes it two ways.
First, safety numbers. Your app combines your key and your contact's key into a short code, shown as a set of digit blocks. Both of you compute the same code from the same pair of keys. If the keys ever differed, the codes would differ too. Compare them over any channel an eavesdropper cannot touch, by holding your phones side by side, reading them aloud on a call, or scanning a QR code, and a mismatch shows up immediately. Matching numbers mean you are talking to the key you think you are.
The catch is that this only helps if you actually compare, and most people never do. So there is a second safeguard that works even when nobody checks.
Second, every key we publish is public and permanent. Every identity key Pollis lists for an account is added to a public log that can only grow, never be quietly edited (Topic 9). Your own app keeps checking that the key the world sees for you matches the key you actually hold, and it warns you if they ever disagree. That means a substituted key could not stay hidden: it would sit in a public record for anyone to see, and your own device would flag it. You do not have to trust that promise; it follows from how the log is built.
Trust on first use. The first time you talk to someone, your app remembers their key. After that, a change is what gets flagged. Changes are usually innocent, like a new phone or a reinstall, so a flag is not an accusation, it is a nudge to re-check before you share anything sensitive. Pollis shows a shield next to verified contacts, and a distinct warning when a remembered key has changed.
In short: compare safety numbers once with the people who matter, and let the log handle the rest.
A safety number that nobody compares protects nobody, and your app cannot tell an innocent key change from a real problem, which is exactly why it asks you rather than guessing. The public log narrows the gap by making any swapped key permanent and visible, which is a strong deterrent and a record, though not a physical block. And none of this affects metadata (Topic 1).
Full transcript
Encryption protects the message. It does not tell you who is on the other end. When you first message someone, your app fetches their public key from our servers. That is the gap.
If the wrong key were handed over, a message could be opened in the middle, read, and re-sealed. Both screens would still show a padlock, because nothing is wrong with the encryption. It is working perfectly, on the wrong key. Every encrypted app has this problem. Here are the two safeguards.
First, safety numbers. Your device combines your key with your contact's and produces a short code. They do the same. Same two keys, same code. If the keys differed, the codes would differ. Compare them any way an eavesdropper cannot touch, side by side, read aloud, or by QR code, and a mismatch shows up right away.
The catch is you have to actually check, and most people never do. So there is a second safeguard that works even when nobody checks.
Every identity key we publish goes into a public log that can only grow. A swapped key would have to appear there, in the open, permanently, and it could not be quietly removed later. Your own device reads that log and compares it against the key you hold. If they ever disagree, your app tells you. The swap stops being invisible and becomes a public record.
One last thing. The first time you talk to someone, your app remembers their key. If it changes, you get a note. Usually that is innocent, a new phone or a reinstall. Sometimes it is not. Your app cannot tell the difference, which is why it asks you.
Compare safety numbers once, with the people who matter. The log handles the rest.
The rigorous version: the safety-number derivation and TOFU pin store in the safety wiki article, and the account-key self-audit in transparency.rs.
Merkle trees and append-only logs
Suppose we publish a list, say every identity key we have ever issued, and promise to only ever add to it, never quietly edit or delete. How could you confirm that for yourself, without taking our word for it?
Downloading the whole list every day and comparing does not scale, and it only works if you never miss a day. The real answer is a structure called a Merkle tree, and once you see it, it is hard to unsee.
Start with hashes, the fingerprints from the last topic. Take your list of entries and hash each one. Take those hashes in pairs and hash each pair together, giving half as many. Pair those and hash again. Keep going until a single hash is left.
That final hash is the root. On our artifacts page right now it looks like this, a single fingerprint of the entire binaries log:
root: 37945cb2f61ee43782259a3893336b8ba8b8679d3af1612742deeec75e46cc0c
Here is the property that makes it worth the trouble: the root depends on every single entry. Change one character anywhere, and its hash changes, and so does its parent's, all the way up. The root becomes completely different. There is no way to alter an entry and keep the root the same, because that would need two different pieces of data with the same fingerprint, which is exactly what hash functions are built to prevent.
So the root is a fingerprint of the whole list, in sixty-four characters. That is the leverage. Instead of downloading a list that grows forever, you compare one short root. If you know the correct root, any claimed version of the list either matches or it does not.
A Merkle tree does not decide whether an entry is truthful; it only makes the whole list tamper-evident. What it removes is the ability to show one version of the list to you and a different version to someone else, or to change the story later. That is a smaller claim than "trust us", and a much more useful one, because you can confirm it yourself instead of believing it.
Full transcript
Say we publish a list, every key we have ever issued, and we promise to only ever add to it. Never edit, never delete. How could you confirm that for yourself?
You could download the whole list every day and compare. But it grows forever, and it only works if you never miss a day.
Here is the real answer. Take every entry and hash it. Take those fingerprints in pairs and hash each pair together. Half as many. Do it again, and again, until one is left. That is the root.
This is the actual root of one of our live logs right now. Watch what happens if one character in one entry changes. Its fingerprint changes, which changes its parent's, which changes the next one up, and the root is completely different.
There is no way to edit an entry and keep the root the same. That would mean two different things with the same fingerprint, which these functions are built to prevent. So the root is a fingerprint of the entire list, in sixty-four characters. You do not download fifty thousand entries to check the list; you compare sixty-four characters.
A Merkle tree does not decide whether an entry is truthful. What it removes is the ability to show one version of the list to one person and a different version to another, or to change it later. Everyone comparing roots gets the same answer. Try it: change any entry and watch the root move.
The rigorous version: docs/transparency.md and the RFC 6962 hashing in verifiable-log/src/merkle.rs. See the live roots on the artifacts page.
Proving an entry is really in the log
You have a root: sixty-four characters standing in for an entire list. Two useful questions follow.
Is my entry actually in there? This is the inclusion proof. You do not need the whole list. You only need the fingerprints of the neighbours along the path from your entry up to the root. Hash your entry, combine it with the neighbour you were given, hash that, combine with the next neighbour, and keep climbing. If you land on the published root, your entry is in the tree. If you land on anything else, it is not.
For a list of a thousand entries, that is about ten hashes. For a million, about twenty. The proof grows very slowly: double the list, add one step. That is why this keeps working for logs that never stop growing.
Is this the same list as before, only longer? This is the consistency proof, and it is what makes "append-only" real. You saw a root last week and there is a new root today. A consistency proof is a small set of fingerprints showing that last week's list sits unchanged inside today's, with new entries only added on the end. If an old entry had been edited or removed, no such proof could exist. Append-only stops being a policy we state and becomes something you can confirm.
Who says which root is the real one? We sign it. Periodically we publish a signed statement, called a signed tree head, that ties together the root, the number of entries, a timestamp, and a signature over all of it. The signature uses one specific key, and the Pollis apps and the verifier tool carry that key's public half built in:
pinned key: 175ebfef98fc6b20c67c4cba9d4a36a4f85f05afa4e31f707e7d7e3c02227148
Building the key into the app matters. If the app simply downloaded the key from a server, a bad server could hand over its own key alongside a self-consistent fake list, and everything would appear to check out. Because the key is compiled in, a substituted key is spotted immediately.
One question remains: could different people be shown different lists? This is called equivocation, and it is the case the whole design is built to catch. Even a perfectly signed, append-only log could in principle show one clean list to auditors and a slightly different one to a single person, each internally consistent. The defence is refreshingly simple. Any two people who compare the signed heads they were shown will spot the difference at once, and because each head is signed, that difference is permanent, undeniable proof of a problem. Nobody has to trust anybody; they just have to compare. Our own publishing job checks what is actually being served for exactly this.
So the chain is: a root fingerprints the list, an inclusion proof puts your entry in it, a consistency proof stops it being rewritten, a signature under a built-in key says it is ours, and comparison between people stops two different versions being served.
None of this makes serving two different lists impossible; it makes it undeniable the moment anyone compares. If literally nobody ever checks their signed head against anyone else's, two versions could sit side by side unnoticed. The maths guarantees the comparison would settle it instantly; it cannot force the comparison to happen. Today that routine check is done by our own publishing job and by anyone running the verifier, and more independent checkers is always better.
Full transcript
You have a root, sixty-four characters standing in for an entire list. Two questions. First: is my entry actually in there?
You do not need the whole list. You need the neighbours along the path from your entry to the top. Hash yours, combine it with a neighbour, hash, combine with the next, and climb. If you land on the published root, your entry is in the tree. If you land anywhere else, it is not. A million entries needs about twenty hashes. Double the list, add one step.
Second question: is this the same list as last week, only longer? A consistency proof shows last week's tree sitting inside today's, unchanged, with the new entries added on the end. If an old entry had been edited, no such proof could exist. So append-only is not a promise, it is something you check.
Who says which root is real? We sign it: the root, the count, and the time, sealed together. That is a signed tree head. And the key we sign with is built into the app and the verifier, not downloaded. If it were downloaded, a bad server could hand over its own key with a matching fake list, and everything would appear to check out. Because it is built in, that is caught right away.
Which leaves one question: could different people be shown different lists? A clean one for the auditors, and a slightly different one for a single person. Each on its own would check out. Neither person could tell alone.
So they compare. Two signed heads, side by side. They differ, and both carry a signature, so there is no explaining it away. That is why this is published rather than promised: comparing makes serving two versions impossible to get away with.
Root. Inclusion. Consistency. Signature. Comparison. Each one closes a door.
The rigorous version: verifiable-log/src/proof.rs for both proof checks, sth.rs for signed heads and the equivocation test, and docs/transparency.md → "Trust model". The live signed head is on the artifacts page.
Pollis's three logs, and what each one protects
Three separate append-only logs, one signing key, one public site at verify.pollis.com. Each is easiest to understand by the problem it protects against.
Log 1, account keys. It protects against the wrong person's key being used for you. Every identity key we list for an account goes in here, in order, with a version number. For a swapped key to be accepted, it would have to be published here, in public, in a record your own device audits. The rule is that versions only move forward, so an older key cannot be slipped back in and history cannot be renumbered.
Log 2, the message-order log. It protects against a group's history being quietly rewritten. A group moves forward in epochs, and everyone applies the same updates in the same order. That order is exactly what a bad server might try to reorder or split. So every update is logged, and the rule is that a conversation is a single straight line: each epoch appears once, and two different updates can never claim the same epoch. A split is refused when it is written.
Note what this log holds: sealed updates. Publishing them reveals the shape of a conversation, never its content. The server never had the keys, and neither does anyone reading the log.
Log 3, binaries. It protects against a special, tampered app being shipped to one person. The fingerprints of every released installer go in here. The rule is that the same release and platform cannot come to mean different files later. If a version were ever built for a single person, its fingerprint would either sit in a public log everyone can read, or be conspicuously missing from it. Either way it is detectable.
Why three logs and not one. A signed statement about one log must never be reusable as a statement about another. So each log signs under its own separate label, and a statement from one log fails to verify against another by design. Different questions, different logs, non-interchangeable proofs. They share one key and one site, which keeps the thing you have to trust down to a single built-in key.
These logs make tampering permanent and public rather than physically impossible, and they say nothing about ordinary software bugs: a log proves what was published, not that it was correct. There is also a timing point. The logs are rebuilt and republished on a daily schedule, so a brand-new release or a just-published key can genuinely not be in them yet. "Not in the log yet" and "not in the log" are different, and the app shows the first as pending rather than an alarm.
Full transcript
Three logs. One signing key. One address: verify dot pollis dot com. Each one protects against a different problem.
The first holds every identity key we have published. For a swapped key to be used, it would have to be written here, in public, where the real owner's app is checking. And the rule is that versions only move forward. An old key cannot be slipped back in.
The second holds every update in every conversation. Groups move forward in epochs, and everyone applies the same updates in the same order. If a server tried to split the history, showing two different updates as the same epoch, the rule would refuse it: one epoch, one update. And what is stored is sealed. The log shows the order of a conversation, never a word of its content.
The third holds the fingerprint of every app we release. If a special version were built for one person, its fingerprint would either be in this public log, where anyone can see it, or missing from it, where that person's own app would notice. There is no third option.
Why three logs instead of one? Because a signed statement about one must never work for another. A valid statement from the message log does not fit the binaries log. Each log signs under its own label, so the signature says which log it belongs to.
And the limits, plainly. These make tampering permanent and public, not impossible. They also say nothing about bugs. A log proves what was published, not that it is correct.
One practical note. The logs rebuild once a day, so something published an hour ago may genuinely not be in them yet. That is why your app says pending instead of raising an alarm.
The rigorous version: docs/transparency.md → "Three domain-separated trees" and "The commit-log invariant", plus the per-tree contexts in verifiable-log-builder/src.
Binary transparency, and reproducible builds
Everything else on these pages assumes one thing: that the app running on your machine is the app whose source you can read. Nothing survives a tampered app, so this is the part to be most careful about.
Code signing, the check your operating system runs before letting an app start, proves less than people assume. An Apple or Windows signature proves that the holder of the company's signing key produced these exact bytes. It says nothing about what those bytes do. A tampered build that was still signed with the right key would pass every check your system makes.
So we publish the fingerprint of every release into a public log. The question stops being "did Pollis sign this?" and becomes "is this exact app the same one Pollis published to everyone, or a different one?"
Each release is recorded at three levels, because "the app" is not one file. The payload is the program and its resources before any signature; this is the level an independent person can rebuild from source and compare. The signed level is what you actually download, after Apple or Windows wraps it; signing deliberately mixes in unique data, so the signed file cannot be identical between two builds, by design. The installed program level records the fingerprint of the main program exactly as it sits on your machine, which is the one thing a running app can measure about itself. The app's built-in "verify this build" check compares the program you are running against that recorded fingerprint.
What this buys you. Suppose two people download the app. One gets the public build, whose fingerprint is in the log. If a different build were ever made for one person, its fingerprint would either be missing from the log, which that person's own app would notice, or present in the log, sitting in public next to the normal release for everyone to see. There is no quiet third option.
Reproducible builds, and where we actually are. A logged fingerprint proves we published these bytes. It does not by itself prove those bytes came from the source code you can read. Closing that gap needs reproducible builds, where anyone compiling the source at a given version gets a byte-for-byte identical result. That is hard, because ordinary builds bake in timestamps, file paths, and ordering that differ from machine to machine. Where we stand today:
Linux is reproducible. An independent rebuild, run from a public copy of the source with none of our secrets, produces the same fingerprint we logged. macOS and Windows are not there yet. This is the largest open gap in our verification story, and we would rather state it than gloss over it. The signing wrapper is never reproducible on any platform, by design.
There is also a second, independent record. Every release carries a build provenance record, stored in a public log that is not ours, proving which automated workflow produced the bytes, with no Pollis key involved in checking it. That answers a different question from reproducibility: provenance says who built it, reproducibility says whether it matches the source. Having both means a single compromised key does not undo the whole story.
On macOS and Windows you currently have "these are the bytes Pollis published, and this workflow built them," but not yet "these bytes match the source you can read." Only the Linux build closes that last loop today. We state it here, in your path rather than in a footnote, because a verification story you have to squint at is not one. The signing wrapper will never close it on any platform, because signatures mix in unique data on purpose.
Full transcript
Your operating system checks that an app is signed before it runs it. That check proves less than you would think. A signature proves the company's key produced these bytes. It says nothing about what the bytes do. A tampered build that was still signed with the right key would pass every check your computer makes.
So we publish the fingerprint of every release, in public. The question changes. Not "did they sign it?" but "is this the app they gave everyone, or a different one?"
Each release is recorded at three levels, because the app is not one file. The payload is the program before any signature; that is the level someone else can rebuild from source. The signed level is what you download, after notarization, which mixes in unique data on purpose, so it is never identical twice. And the installed-program level is the fingerprint of the main program as it sits on your machine, which is what a running app can measure about itself.
Here is what it buys you. Two people download the app. One gets the public build, whose fingerprint is in the log. If a different build were made for one person, its fingerprint is either missing from the log, which their own app notices, or in the log, in public next to the real release. There is no quiet option.
Now the part to be careful about. A fingerprint proves we published these bytes. It does not prove they came from the source code you can read. For that you need reproducible builds: anyone compiling the source gets an identical result.
On Linux, we have that. An independent rebuild, with none of our secrets, produces the same fingerprint we logged. On macOS and Windows, not yet. That is the biggest open gap, and we would rather you hear it from us. And the signing wrapper is never reproducible anywhere, by design.
There is one more record. Every release carries a build provenance record in a public log that is not ours, proving which workflow produced the bytes, with no key of ours involved in checking it. A different question from reproducibility, and a useful one when the worry is a compromised key.
The rigorous version: docs/verifiable-builds-design.md, the residual list in reproducible-builds-residuals.md, and the independent rebuild-verify workflow you can run from a fork. Live release hashes are on the artifacts page.
Verify it yourself
Everything above explains what could be checked. This topic has you actually check it, on your own machine, in about two minutes. There are two ways in: reading the files directly, and running the verifier.
The verification data is just files. There is no clever
service. verify.pollis.com serves a folder of plain JSON
files, with no server-side logic and no database lookup, so nothing can
behave differently depending on who is asking. That is deliberate: a
service that computes an answer could compute a different answer for
different people, while a folder of files written in advance cannot, and
our daily job re-reads what is actually being served to confirm it
matches what was signed.
/v1/<log>/sth/latest.json is the current signed
statement; /v1/<log>/entries.json is the log's
entries; /verify/release/<tag> is a ready-made report
for one release. And /v1/<log>/public_key.json is the
key the statements are signed with, which you should not simply trust: it
is served for convenience, and the whole point (Topic 8) is that real
verification compares it against a key you already have.
Fetch the current statement yourself, in any terminal, on any system:
curl -s https://verify.pollis.com/v1/binaries/sth/latest.json
The verifier does the actual checking. Reading the JSON
tells you what is claimed. pollis-verify is a small
standalone program that checks the claims: it re-hashes entries, rebuilds
the tree, checks the proofs against the signed statement, checks the
signature against a key built into the program, and re-runs the same
rules the log enforces. It is the same checking code the app uses, not a
separate version that might happen to agree by accident.
Four commands, four questions. Arguments come in this order:
# is the whole log consistent and append-only?
pollis-verify remote https://verify.pollis.com
# what keys have we published for this user, in what order?
pollis-verify account https://verify.pollis.com <user-id>
# is this conversation's history a single unbroken line?
pollis-verify group https://verify.pollis.com <conversation-id>
# are this release's files in the log, with valid proofs?
pollis-verify release https://verify.pollis.com v1.5.3
Here is a real run of that last command against the live log, captured 2026-07-23, not mocked up:
$ pollis-verify release https://verify.pollis.com v1.5.3
Release: v1.5.3
Found: yes
STH: tree_size 60 root 37945cb2f61ee43782259a3893336b8ba8b8679d3af1612742deeec75e46cc0c
Artifacts (publish order):
darwin aarch64 dmg payload payload 774c8f…ea92 artifact 774c8f…ea92 [included ✓]
darwin aarch64 dmg signed payload 774c8f…ea92 artifact eef7ae…c155 [included ✓]
darwin aarch64 dmg exe payload 774c8f…ea92 artifact 77fc24…bea5 [included ✓]
windows x86_64 nsis payload payload 88547f…7926 artifact 88547f…7926 [included ✓]
windows x86_64 nsis signed payload 88547f…7926 artifact 16edeb…e92b [included ✓]
windows x86_64 nsis exe payload 88547f…7926 artifact 0f4c0f…73a6 [included ✓]
linux x86_64 appimage payload payload 9b1ab1…e653 artifact 9b1ab1…e653 [included ✓]
linux x86_64 deb payload payload 7f19ec…1a5a artifact 7f19ec…1a5a [included ✓]
linux x86_64 deb exe payload 7f19ec…1a5a artifact 5d9281…55b5 [included ✓]
linux x86_64 rpm payload payload 586cfa…cbaa artifact 586cfa…cbaa [included ✓]
linux x86_64 rpm exe payload 586cfa…cbaa artifact e45d0c…400b [included ✓]
PASS: release binaries tree is valid
Read that against what you now know. tree_size 60 and the
root are the signed statement from Topic 8, the same one the
curl above returns. Each [included ✓] is
a proof that actually checked out, a computation that succeeded on your
machine, not a status someone typed in. The
payload / signed / exe levels are
from Topic 10. And PASS means the whole tree replayed cleanly
under the built-in key, with every rule holding.
What a failure looks like. A FAIL is not
necessarily a sign of an attack. It can simply mean the log is
mid-rebuild, or that a release is newer than the last daily rebuild
(Topic 9). The result that would genuinely matter is a valid signature
over a tree that disagrees with one someone else was served, which is the
two-versions case from Topic 8. That is why the most valuable thing you
can do with this tool is compare results with someone else running it.
That comparison is the whole basis of a transparency log.
Where it comes from. pollis-verify ships as
a standalone program with checksums, and its built-in key is printed in
the release notes. It is also buildable from source, and if you are
seriously auditing us, build it, so you can read exactly what it does
first. A verifier being open and rebuildable is what keeps it from being
one more thing you have to take on trust.
A PASS on your machine proves the log you were served is consistent, append-only, and signed by the built-in key. It cannot tell you whether someone else was served the same log; that is the one question a single run cannot answer, which is why comparing with other people matters. And a program we hand you, checking a key we built in, is only as strong as your choice to build it from source yourself. Both gaps close the same way: more people running it, comparing results.
Full transcript
Everything we have talked about is checkable, in about two minutes. First, the data, which is barely a service at all. It is a folder of plain files. No database, no logic, nothing that can behave differently depending on who is asking. A service that computes an answer could give different people different answers. A folder of files written in advance cannot.
You can fetch any of them right now. Here is the current signed statement: the root, the entry count, and a signature. And here is the same root on our website. Same sixty-four characters, two different places.
But reading the files only tells you what is claimed. To check the claims, use the verifier. It is a small program you download, or build yourself, and if you are seriously auditing us, build it. It re-hashes every entry, rebuilds the tree, checks every proof, and checks the signature against a key built into it. Not downloaded. Built in. One command.
Watch what comes back. Tree size, sixty. The root, the same one you just fetched. Then every file in the release, each with a tick. Those ticks are not a status someone wrote down. Each one is a proof your machine just recomputed. And PASS means the whole tree checked out under a key that cannot be swapped on you.
A failure does not always mean an attack. It can mean the log is mid-rebuild, or a release is newer than the last daily rebuild. The result that would matter is two valid signatures over two different trees, which is the two-versions case, and the only one you cannot spot alone.
So run it, and compare with someone.
The rigorous version: pollis-verify.rs (the CLI you just ran), the shared verification path in verifiable-log-serve, and the static API layout in docs/transparency.md.
Reading the artifacts page and the Security page
By now you have every idea you need to read our public dashboards. Here is the tour, which is really just the key to the map.
On the website: the artifacts page. Latest releases shows the current version of each thing we ship, with download links.
Release proofs is the heart of it: for the current release, was it found in the binaries log, did the tree check out, and what is inside. You will see a row per file with the fields from Topic 10: platform, package, level, and two fingerprints, each with a tick. Read a tick as "a proof was checked", not "a box was ticked". The tree size and root are the signed statement from Topic 8, and the root is the same string you would get from the command line or the verifier tool. If a row says not in log yet, that is usually the daily rebuild (Topic 9), not a problem.
Daily transparency self-audit shows the current signed statement for all three logs, republished each day. And the one key you trust is the built-in signing key that everything above reduces to. The page also notes something worth reading: the key check it runs in your browser is a convenience, not the real safeguard. Real verification uses a key you obtained independently (Topic 11).
In the app: the Security page. This build gives you a version, a commit, and a verdict from the in-app check (Topic 10). There are four possible results. Build publicly verified means this exact app's fingerprint is in the public log for this release. Build publication pending means the release is not in the rebuilt log yet, which is normal right after a release. Verification unavailable means the check could not run, because the log was unreachable or this release predates the installed-program record; this is not an accusation. Build not in public log is the loud one: the release is published, a comparable fingerprint exists, and this one is not among them.
The difference between those last two is the difference between "could not check" and "checked, and something is wrong." It is worth keeping straight.
Below that, account key is your own device confirming that the key the world sees for you matches the key you hold (Topics 6 and 9), quiet when all is well. Devices and security events is a running record of enrollments and resets, worth a look if something feels off. And safety numbers per contact carry the shield states from Topic 6, including a distinct warning when a remembered key has changed.
Green across these surfaces means the things Pollis published are consistent, permanent, and include the app you are running. It does not mean the code is free of bugs, that your device is uncompromised, or that metadata is invisible to whoever carries your traffic. The point of this whole section is that we keep shrinking what you have to take on trust, and publish the evidence for what remains.
Full transcript
You have every idea you need to read our dashboards. Latest releases shows what we currently ship, and where to get it.
Release proofs is the one that matters. For the current version: is it in the binaries log, did the tree check out, and what is inside.
Each row is one part of a release. Platform. Package. Level, which is one of the three from earlier. Two fingerprints. And a tick. Read that tick carefully. It does not mean someone ticked a box. It means a proof was recomputed and it held. The tree size and root are the signed statement, and this root is the same string you would get from the command line or the verifier. If it says not in log yet, that is almost always the daily rebuild, not a problem.
Below that, the daily self-audit shows the current statement for all three logs, and at the bottom, the one key everything reduces to. The page also notes that its in-browser key check is a convenience, not the real safeguard. Real verification uses a key you obtained independently.
Now in the app. This build tells you whether the exact app you are running is in the public log. Four possible results. Pending means the log has not rebuilt yet. Unavailable means the check could not run, because the log was unreachable or the release is older than the record it compares against. And not in public log is the loud one: the release is published, there is something to compare against, and this one is not it.
Unavailable versus not in the log is the difference between "I could not check" and "I checked, and something is wrong." It is worth keeping straight.
Below that: your account key, checked against the public log. Your devices. Your security events. And safety numbers for each contact.
Green across all of it means the things we published are consistent, permanent, and include the app you are running. It does not mean the code has no bugs, that your device is safe, or that metadata has gone anywhere. We keep shrinking what you take on trust, and publish the evidence for what is left. Now you can check it.
The rigorous version: the page itself, the artifacts page, plus artifacts.js for how the proofs are rendered and BuildVerifyLine.tsx for the four verdicts.