Verifying a published results document
Check a signed InviziPoll results document yourself: rebuild the canonical bytes, match the workspace key, apply its status, and read the floors correctly.
- Security reviewers, IT
- 10 min read
- Updated Oct 14, 2026
- Reviewed by Security
A published results document is a set of aggregate numbers, the anonymity floors that governed them, and a signature made with a key that belongs to the workspace. Anyone holding the document and the workspace's public keys can check that these exact numbers were published by that workspace, with no account, no login, and no InviziPoll code; use this page when a client, a board, a candidate, or an auditor wants to check a finding rather than take it on trust.
Verification proves the numbers weren't altered after the workspace published them. It doesn't prove the workspace ran a fair survey; read the floors, the response count and, where it's given, the roster size in the document for that.
#When to use it
- A security team has been handed a survey finding and wants to confirm it matches what the tool produced.
- A consultant's report cites a signed document and the client wants to check the citation.
- A candidate is looking at an employer's public rating and wants to know the score is what the employer's own staff produced.
- You're comparing a PDF someone sent you against the machine-readable document it claims to come from.
#What is signed
Every document is a JSON body with a fixed shape and a signature envelope around it. The body is the claim. The envelope says who vouched for it.
The body carries:
| Field | What it holds |
|---|---|
docType | invizipoll.published-aggregate |
v | 4 for anything published today |
subject | poll, series, wave, as-of date, template |
scope | kind, label, n, invited |
floors | the three floors, as integers |
questions | one aggregate per question |
cuts | named cuts of the same responses |
inclusionReceiptRoot | reserved, always null |
Two fields are new in version 4:
subject.templatenames InviziPoll's own question set when the poll used it unchanged, for exampleemployee-rating. It'snullwhen the questions were written or edited by the workspace. Two documents with the same template asked the same questions.scope.invitedis the size of the poll's roster when it was published. It's the numbernis a share of. It'snullfor an open link and for any cohort, and it's never smaller thann. The workspace declared the roster, so read it as the workspace's own figure.
Documents published before version 4 keep their shape and keep verifying. A v: 3 document simply has no template and no invited.
The signature envelope wraps the body without changing it:
{
"body": { "docType": "invizipoll.published-aggregate", "v": 4 },
"canonical": "base64url SHA-256 of the canonical bytes",
"signatures": [
{
"kind": "workspace",
"keyId": "base64url SHA-256 of the public key",
"alg": "ML-DSA-65",
"sig": "base64url",
"signedAt": "2026-09-12T09:12:00.000Z"
}
]
}
Three properties matter for a verifier:
- The bytes are canonical. Keys serialize in a fixed order, integers stay integers, other numbers are rounded to four decimal places with no exponent, and the array of cuts is sorted by its id before signing. Two independent implementations produce the same byte string from the same inputs, which is what lets you re-derive and compare.
- Nothing in the body says where it was computed. No browser, no device, no "generated at" instant. The only timestamp is
signedAt, and it lives on the signature, not in the claim. Re-signing the same body with a different key yields the same document. - The signer is the workspace. Each workspace signs with an ML-DSA-65 keypair. The secret is created in an owner's or admin's browser and stored wrapped under the workspace's master key, and our servers don't sign documents. Our servers never receive the signing secret in a form they can open: the only copies we hold are key backups locked in the browser under a password or recovery key. So a document signed by the workspace is a claim we couldn't have forged. See the threat model.
#The workspace's signing keys
A workspace signs with one key at a time, and it can replace that key. So a workspace has a list of public keys, not just one, and each entry says what it's good for:
| Status | What it verifies |
|---|---|
active | Any document it signed. |
retired | Documents dated up to its retirement. |
revoked | Nothing. |
Each entry carries its keyId, the public key, the day it became active, and the day it was retired or revoked.
A key is retired when an owner or admin uses Replace key, for example after an admin leaves. Every stored copy of its secret is deleted in the same step, and the key stops vouching for anything dated after its retirement day. It keeps vouching for documents dated on or before that day, and the date it checks is subject.asOfDate, which the signer writes. So if someone kept a copy of a retired secret, they could still sign a new document with an earlier date and it would verify.
A key is revoked when an owner or admin uses Report key compromised. Nothing it signed counts any more, whatever the date, and the workspace has to republish those results under its new key.
So the choice is about whether a copy of the secret could have left your control. Replace the key when you're rotating routinely and have no reason to think it was copied. Report it compromised if you think it might have been, for example if a departing admin's device or an unlocked browser was out of your control.
The public results page and a shared slice fetch the key list together with the document. A workspace's public rating page links to the same list as a JSON file, so you can check a document without scraping a page.
#What the floors mean
floors is the part most readers skip and the part that makes the numbers meaningful.
| Floor | Value | Meaning |
|---|---|---|
resultsUnlock | 3 | released responses before any result exists |
cohortMin | 5, or authoredK if higher | smallest group in any cut |
authoredK | admin's choice | the poll's Minimum group size setting, recorded for the reader |
A poll-scoped document must have scope.n at or above resultsUnlock. A cohort-scoped document, and every entry in cuts, must have n at or above cohortMin. Where scope.invited is given it must be at least n. The check is against the document's own floors, so a validator with the published shape and no knowledge of InviziPoll can run it.
authoredK is recorded, not checked. Nothing in validation compares n to it, but nothing is released to admins below the larger of three and authoredK, so a poll-scoped n is at least that. It also sets cohortMin when it's above five. Read resultsUnlock and cohortMin as the floors the validator enforces.
scope.n counts the responses released to the workspace when the document was made. While a poll is open, a few responses are always held in reserve, so a document published mid-poll leaves them out; one published after close counts every response. A document published while the poll is collecting has an empty cuts array, because breakdowns only exist after close. Nothing outside the signature adds to or subtracts from n. How batched release works.
The floors are signed alongside the counts. Nobody can widen or narrow a cohort after the fact to change how a number reads, because the conditions of the number's validity are inside the same signature. A group below the floor isn't in the document at all. There is no "suppressed" marker, because a marker would tell you a small group exists, which is itself a disclosure.
#How to verify
You need the document's JSON and the workspace's key list. Any ML-DSA-65 library will do for the check itself.
- Parse the document. And confirm it matches the shape above. Refuse anything that doesn't.
- Rebuild the canonical bytes. From
bodyusing the ordering rules, and hash them with SHA-256. The base64url of that hash must equalcanonical.- Find the key. Take the signature with
kind: "workspace"and look up the entry in the key list whosekeyIdmatches it. Check that the base64url SHA-256 of that entry's public key equals thekeyId. If nothing matches, the check fails. Zero recognised signatures is a failure, not a warning.- Verify.
sigover the canonical bytes with ML-DSA-65 and that public key.- Apply the key's status. An active key is good. A retired key is good only if
subject.asOfDateis on or before its retirement day. A revoked key is never good, however well the signature checks out.- Read the floors. And check every
nagainstresultsUnlockorcohortMin, andinvitedagainstn.authoredKis informational.
#What a failed verification looks like
InviziPoll's own pages say the failure out loud rather than dropping a badge quietly. The public results page, a shared slice and a public rating page show one of these sentences:
- "Signature checks out. These numbers are exactly what the workspace published." A document signed by a key the workspace later replaced reads exactly like this, as long as it was dated before the replacement.
- "We couldn't verify this. Nothing here carries a signature this build knows how to check." Usually the document was published without a signature.
- "We couldn't verify this. The signature didn't match the workspace's published key."
- "We couldn't verify this. The numbers don't match the fingerprint that was signed, so treat them as unconfirmed."
- "We couldn't verify this. It was signed with a key the workspace revoked on a given day, so treat these numbers as unconfirmed." The rating page names the company and the date.
- "We couldn't verify this. It's dated after the key that signed it was retired."
On the results page and a shared slice the charts still render, under the warning. The reader is told what they're looking at, and left to decide what it's worth. A public rating page and its badge go further and show no score at all for a document that doesn't verify.
If you edit one digit in a signed document and run the check yourself, step 2 fails first: the canonical hash no longer matches.
#What the document does not contain
- No written answer. Open-text questions appear as recurring terms with counts.
- No per-response data of any kind, and no submission times. Response records carry no timestamp.
- No group below the floor.
- No respondent identity, and no roster values other than the labels of the cuts that cleared the floor.
invitedis a count, never a list. - No record of the device or person that computed it.
The document is a claim about aggregates. The reason it's worth signing is that the underlying answers were encrypted in each respondent's browser, so the aggregate is the only readable form the data ever takes outside the workspace's own keys. See Post-quantum cryptography for the primitives and Encryption model overview for the key hierarchy the signing key sits inside.
#Limits and gates
- A workspace signs with one active key at a time. Replacing or revoking it is an owner or admin action in workspace settings, and it needs a browser that has unlocked the workspace's encryption.
- A revoked key's documents stay unverified until they're republished, and republishing needs the responses. A wave whose responses were already deleted under its retention setting stays unverified.
- Most documents can be published unsigned, with a warning before the click. A public employee rating can't: the page won't publish one without a signature, and the server checks the signature before storing it.
- A republish overwrites that poll's document. In a recurring series each wave keeps its own document, so an earlier wave isn't replaced by a later one. A copy you saved stays valid on its own bytes.
- Verification says nothing about whether a PDF was produced from the document. Compare the PDF's numbers to the JSON yourself.
See also Share a slice: one team's results for one manager, Give the board results they can verify. Each page covers the adjacent settings and workflow steps.