Assurance
What Pollis checks about its own code, and what those checks actually found — including the times they found something.
Everything below is a link to a machine-generated record, not a summary you have to trust. The artifacts page is the companion: it covers the transparency log and the commands that check a download.
1 · Independent rebuild of every release
After each release, a workflow rebuilds the Linux AppImage payload from the public source at that tag, on a clean runner, and asserts that the hash it gets is the hash already recorded in the public transparency log. It uses no Pollis secrets — that is deliberate and load-bearing, so that anyone can fork it and run the identical check with no credentials from us. It trusts exactly one pinned ML-DSA-44 key.
Until now that verdict went to a job log and a CI artifact that expires after 90 days. Below is the durable record.
Loading rebuild verdicts…
It covers the Linux AppImage payload only. Not the .deb, not the
.rpm, not macOS, not Windows. Those platforms have published-bytes-match and build
provenance, but nothing yet re-derives them from source.
Reproduction needs the published build recipe, not source alone, because one build-time value is compiled in. A third party with no recipe can still prove log inclusion — that half needs no build inputs at all — but not bit-for-bit reproduction.
Two releases were never checked. The rebuild only runs when the release workflow concludes successfully, so v1.9.0 and v1.9.1 were skipped entirely and no verdict exists for them. They are in the table, marked as such, rather than quietly omitted.
Some historical red runs were not real failures. Before commit
564353ae (16 August 2026) the workflow could report "did not reproduce" for a release
that was merely not yet published into the log — the underlying check exits cleanly over an empty
artifact set, so the wait loop gave up immediately and compared the rebuild against nothing. Two runs
failed exactly that way (v1.9.4 and v1.9.5) and both in fact reproduced byte-identically.
The reds are left in the table rather than deleted.
v1.9.3 reproduces. Its August 13 rebuild did not, and sat unexplained. Re-running
exactly the same check on 17 August — same tag, same public source, no other change — landed on the
same CI machine image the release had used and produced a payload
byte-identical to the one in the log
(07bccca8…,
run 32036282718).
One variable was different in August; holding it constant makes the bytes match.
What that variable was. A Linux AppImage is not purely a function of our source:
the bundler copies the app's system libraries off the build machine into the bundle. GitHub rebuilds
the machine image behind its ubuntu-22.04 label every few weeks and rolls it out
gradually, so the release build and its rebuild — 36 minutes apart — landed on different
images, and seven vendored libraries (Kerberos, SQLite, systemd, udev) came out different. Nothing
built from Pollis source changed except the order of a list of content hashes inside the executable,
which a compile-time dependency emits in filesystem order and which carries no meaning.
The August run stays in the table, still red, rather than being deleted or quietly recoloured: it
genuinely could not perform the comparison it reported on. What changed is that the workflow now
reads the machine image the release recorded, compares it against its own, and says which situation
it is in — did not reproduce, build environment drifted, or no recorded
recipe. All three still fail, because an inconclusive rebuild is not a passing one; only the
first is a claim about the shipped binary. The full write-up, with the byte evidence, is in
docs/reproducible-builds-residuals.md.
2 · Machine-checked correctness
Pollis's hard promise is messages must work: delivery to every current member, decryption, offline and online cycles, reconnects. The places where that promise can break are small, pure decision functions — when a delivery cursor may advance, when a commit may be accepted at an epoch, when old data may be deleted. Those are checked by machine, not by testing a happy path.
TLA+ — the design
Two specifications are checked exhaustively by TLC on every relevant pull request. Exhaustive means every reachable state of the model is enumerated, not sampled.
| Spec | Checks | Model bound | States | Result |
|---|---|---|---|---|
CommitLog |
OnePerEpoch, Gapless, OpeningClosesTheHead,
NoForeignAdopt, LineageMonotone
|
3 clients, max 2 commits, max epoch 3 | 72,228 distinct depth 13 |
all hold |
Delivery |
NoLossForCurrentMember, CursorMonotone,
AcceptedLossesOnly, CursorLeqClock
|
2 devices, max 3 messages, max epoch 2 | 130,484 distinct depth 19 |
all hold |
A passing check is worthless if the invariant has quietly stopped being able to fail, so three
deliberately broken variants run alongside and each must produce a counterexample.
Dropping the commit-log head guard violates OnePerEpoch; dropping the migration
compare-and-swap violates OpeningClosesTheHead; guarding retention on the
fastest member rather than the slowest violates NoLossForCurrentMember. If any of
those ever passes cleanly, the gate fails.
Kani — the actual Rust
TLA+ checks the design; the design is not the shipped code. 35 Kani harnesses — 19 proofs and 16 teeth — run bounded model checking over the real functions, not a model of them: the delivery watermark, the rejoin gate that keeps a revoked device out of the tree, own-commit canonicalization, gap detection, the Delivery Service's head arithmetic, and the retention floor.
The same teeth discipline applies. Each proof is paired with a mutant — an off-by-one in the watermark, a fail-open rejoin gate, a retention floor that adds slack instead of subtracting it — that Kani is required to refute.
Both results are now published as a run summary and a downloadable artifact on every run: TLA+ runs · Kani runs · design doc · the specs themselves.
Both are bounded. TLC checks a small model — three clients, a handful of epochs — not an unbounded network, and Kani checks small input domains, not all inputs. A bug that only appears at larger scale is outside both. This is the standard trade: exhaustive over a small space beats sampled over a large one, but it is not a proof about the running system.
They cover the decision functions, not the program. The proofs range over pure functions deliberately extracted for the purpose. The database calls, the network, the MLS library and the UI around them are not verified — they are covered by ordinary tests and a nightly fuzzing soak.
One invariant has no machine-checked defence at all — that the test schema equals the production schema. It is enforced by a mechanical equality check instead, and the design doc says so.
3 · Supply chain
Pollis's Rust lockfile resolves to 1,094 crates. Two gates run on every pull request
and every push to main, both against pinned tool versions
(cargo-deny 0.19.9, cargo-vet 0.10.2) so the gate itself cannot drift.
cargo-deny — advisories, licences, sources
-
Advisories. Every RustSec advisory against the graph fails the build unless it has a
named exemption with a written reason. There are 29 exemptions and
no blanket suppression — each names a specific
RUSTSEC-id. Twenty-one are unmaintained-crate notices with no vulnerability (ten of them the GTK3 bindings Tauri's WebKit backend requires, for which no GTK4 Tauri backend exists). Eight are real vulnerabilities with no reachable fix; those are listed individually below. - Licences. An allow-list of 17 identifiers, each one verified as actually present in the graph rather than allowed speculatively. MPL-2.0 is permitted with the reasoning written down (file-level copyleft, no reach across a module boundary).
-
Sources.
crates.ioonly, plus exactly one git dependency: anopenmlscommit pinned by revision, needed for the post-quantum MLS ciphersuite that no published release carries yet. The entry matches a repository URL, and the revision is pinned inCargo.toml, so it cannot silently follow upstreammain.
| Advisory | Crate | Why it is exempt |
|---|---|---|
RUSTSEC-2023-0071 | rsa |
No upstream fix. Transitive via LiveKit's JWT library; the decryption-timing oracle is not reachable on the signing path we use. |
RUSTSEC-2026-0049RUSTSEC-2026-0098RUSTSEC-2026-0099RUSTSEC-2026-0104 |
rustls-webpki 0.101 / 0.102 |
Certificate-validation issues fixed only on the 0.103 line, which our own direct TLS already uses. The old lines are pinned by LiveKit and libSQL upstream; waiting on their rustls bump. |
RUSTSEC-2026-0258 | h2 0.3 |
Empty-DATA-frame denial of service, fixed only on the 0.4 line with no 0.3 backport. Reachable only as libSQL's outbound client to Turso, never on the Delivery Service's inbound listener, which is hyper 1.9 and does not use it. |
RUSTSEC-2026-0194RUSTSEC-2026-0195 |
quick-xml |
Every vulnerable instance is build-time code generation or dev-only. No runtime path parses untrusted XML. |
cargo-vet — review provenance
cargo-vet records, per crate version, who has reviewed it. Pollis imports
517 audits from six organisations that publish theirs — Mozilla (206), Google (117),
Bytecode Alliance (84), ISRG (52), Zcash (42) and Embark (16) — plus 35 crates trusted by publisher
identity. The remainder sit in an exemption baseline.
Pollis has audited zero crates itself. supply-chain/audits.toml is
empty. 858 exemption entries cover 716 crates, which is the dependency graph as it stood when the
gate was switched on. What the gate actually enforces is a ratchet: no new or bumped crate
can enter without a deliberate, reviewable change to that file. It is not, and should not be read as,
"every dependency has been reviewed".
Duplicate-version and wildcard checks warn rather than fail. A large native tree carries transitive duplicates we do not control, so those are visible in the job log but do not block.
There is no scheduled re-scan and no automated dependency-update bot. Advisories are
checked when a pull request runs or on a push to main. A newly published advisory against
an unchanged dependency is not caught until the next such run.
Neither gate covers the JavaScript half or vendored C/C++. The renderer's
pnpm tree appears in the per-release SBOM but is not vetted by these tools, and the C and
C++ compiled inside build scripts (notably the WebRTC crates) is in neither.
Read the policy directly:
deny.toml ·
supply-chain/ ·
gate runs.
4 · Signing keys, custody and rotation
Every claim on the artifacts page reduces to one pinned
ML-DSA-44 key, key id 6cbd4b2aed5c4bf1. This section is about who holds
it, what happens if it is stolen, and what has already been done to it.
| Key | Signs | Custody |
|---|---|---|
| Transparency-log signing key ML-DSA-44 |
Signed tree heads for all three logs | A GitHub Actions secret, backed up in a secrets manager |
| Offline root key ML-DSA-44 |
Nothing but key-set statements: "these keys may sign these logs, until this date" | Offline. Deliberately absent from CI, from the secrets manager and from the repository |
| Release code-signing | macOS (Apple Developer ID) and Windows (Azure Trusted Signing) installers | GitHub Actions secrets |
| Sigstore identity | cosign signatures and SLSA provenance | No held key at all — a short-lived certificate bound to the release workflow's OIDC identity |
A real rotation has been executed. In August 2026 the log signing key was replaced with fresh material and all three trees were republished from source data under new domain-separation contexts. It was not a formality: the previous key had been derived from the same 32-byte seed as its Ed25519 predecessor, so the earlier post-quantum migration had changed the format without changing the secret. The rotation was rehearsed against production with the old key still installed first, which caught two bugs that would each have made it a silent no-op. The result was verified independently, from a machine that had not performed the rotation.
A compromise does not require shipping a new build. Because clients pin a key set authorised by the offline root, the response to a stolen signing key is to sign a new key set that omits the compromised key entirely and republish — no client update, no flag day. The runbook is explicit that a compromised key is dropped rather than given a short expiry, because an attacker who can delay a client's fetch keeps it alive for exactly that long. It is equally explicit that you act on suspicion: you do not need proof.
GitHub Actions is inside the publishing trust boundary, and the whitepaper says so. The log signing key lives in Actions secrets and the trees are built and signed in CI, so a compromise of that environment could sign a tree. The post-publish self-audit and the equivocation tripwire detect that after the fact; they cannot prevent it. The same is true of the release-signing keys. Pollis operates the only log, which is why the independent verifier is released — so a third party can audit without asking us for anything.
The rotation shipped without an overlap window. The custody design calls for a minimum 90-day period in which both keys are pinned; the rotation that was actually performed did not have one. An auditor holding cached pre-rotation tree heads sees the change as equivocation and has to re-pin from the announcement rather than verify the transition.
Root compromise has no in-band recovery. If the offline root is lost or stolen, the fix does require a client release: mint a new root, ship a build pinning both, wait out the overlap, then publish under the new root alone.
The pinned key is copied by hand into eight places — the Rust core, this website, two workflows, the key-set statement, and three documents — with no check that they agree. One deployed copy going stale has already shown a false tamper alarm to every visitor once.
Compromise runbook · Custody & rotation design · The signed key set.
Reporting a vulnerability
Report privately through a
GitHub security advisory, not a public issue. The machine-readable
policy is at /.well-known/security.txt and the
terms are in the
disclosure policy.
Pollis is source-available, not open source: PolyForm Noncommercial 1.0.0. You may read, build, audit and modify it for noncommercial purposes; commercial use needs a separate licence. Auditing the code and publishing what you find is explicitly permitted.