Skip to main content

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.

Note

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:

FieldWhat it holds
docTypeinvizipoll.published-aggregate
v4 for anything published today
subjectpoll, series, wave, as-of date, template
scopekind, label, n, invited
floorsthe three floors, as integers
questionsone aggregate per question
cutsnamed cuts of the same responses
inclusionReceiptRootreserved, always null

Two fields are new in version 4:

  • subject.template names InviziPoll's own question set when the poll used it unchanged, for example employee-rating. It's null when the questions were written or edited by the workspace. Two documents with the same template asked the same questions.
  • scope.invited is the size of the poll's roster when it was published. It's the number n is a share of. It's null for an open link and for any cohort, and it's never smaller than n. 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:

json
{
  "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:

StatusWhat it verifies
activeAny document it signed.
retiredDocuments dated up to its retirement.
revokedNothing.

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.

FloorValueMeaning
resultsUnlock3released responses before any result exists
cohortMin5, or authoredK if highersmallest group in any cut
authoredKadmin's choicethe 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.

  1. Parse the document. And confirm it matches the shape above. Refuse anything that doesn't.
  2. Rebuild the canonical bytes. From body using the ordering rules, and hash them with SHA-256. The base64url of that hash must equal canonical.
  3. Find the key. Take the signature with kind: "workspace" and look up the entry in the key list whose keyId matches it. Check that the base64url SHA-256 of that entry's public key equals the keyId. If nothing matches, the check fails. Zero recognised signatures is a failure, not a warning.
  4. Verify. sig over the canonical bytes with ML-DSA-65 and that public key.
  5. Apply the key's status. An active key is good. A retired key is good only if subject.asOfDate is on or before its retirement day. A revoked key is never good, however well the signature checks out.
  6. Read the floors. And check every n against resultsUnlock or cohortMin, and invited against n. authoredK is 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. invited is 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.