How Veranon Works
How Veranon Works: Cryptographic Anonymity for Organizational Surveys
Veranon is a verifiable anonymous survey platform. The core problem it solves is straightforward: organizations need honest feedback, but employees won’t give honest feedback if they think their responses can be traced back to them. Promises of anonymity aren’t enough—people need cryptographic guarantees.
This post walks through the technical architecture that makes this possible, focusing on two protocols: Semaphore for anonymous group membership proofs, and RSA blind signatures for unlinkable identity claims.
The Problem with Traditional Anonymous Surveys
Most “anonymous” survey tools operate on trust. The survey administrator promises not to look at individual response metadata, but technically they could. IP addresses, submission timestamps, response patterns, and session identifiers all create linkability risks. Even if the administrator is trustworthy, data breaches or subpoenas could expose respondent identities.
The goal with veranon is to make deanonymization technically infeasible, not just against policy.
Architecture Overview
Veranon combines two cryptographic primitives:
- Semaphore Protocol — Enables users to prove group membership and submit responses without revealing which group member they are
- RSA Blind Signatures — Prevents the server from linking an authenticated user to the anonymous identity they claim
The separation matters. Semaphore alone would allow a malicious server to track which user claimed which identity. Blind signatures break that link.
┌─────────────────────────────────────────────────────────────────┐
│ Survey Lifecycle │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. SETUP (Admin) │
│ └─► Create survey, add questions, designate respondents │
│ │
│ 2. ACTIVATION │
│ └─► Server generates N Semaphore identities │
│ └─► Builds Merkle tree of identity commitments │
│ │
│ 3. IDENTITY CLAIM (Respondent) │
│ └─► Blind signature protocol (see below) │
│ └─► Respondent obtains one identity, server can't tell │
│ which one │
│ │
│ 4. RESPONSE SUBMISSION │
│ └─► Generate ZK proof of group membership │
│ └─► Submit response + proof + nullifier │
│ └─► Server verifies proof, records response anonymously │
│ │
└─────────────────────────────────────────────────────────────────┘
Semaphore: Anonymous Group Membership
Semaphore is a zero-knowledge protocol that lets users prove they belong to a group without revealing which member they are. The core data structures are:
Identity: A Semaphore identity consists of two secret values—trapdoor and nullifier—and one public value, the commitment. The commitment is derived via Poseidon hash:
commitment = poseidon(nullifier, trapdoor)
Poseidon is a ZK-friendly hash function. It requires roughly 200 constraints in a circuit, compared to ~25,000 for SHA-256. This matters for proof generation time and verification gas costs.
Group: A collection of identity commitments stored in a Merkle tree. The Merkle root serves as a compact representation of group membership.
Proof: To prove membership, a user provides:
- A ZK proof that their commitment exists in the tree (via a Merkle inclusion proof)
- A nullifier derived from
poseidon(identityNullifier, externalNullifier)where the external nullifier is chosen by the application (e.g., the survey ID)
The nullifier prevents double-voting: same identity + same survey = same nullifier. The server rejects duplicate nullifiers. But critically, different surveys produce different nullifiers, so you can’t link a user’s actions across contexts.
What the proof reveals: The Merkle root (which group), the external nullifier (which survey), and the derived nullifier (which anonymous member, for this survey only). It does not reveal which commitment in the tree belongs to the prover.
The Identity Claim Problem
Here’s where it gets interesting. In veranon, the server pre-generates Semaphore identities when a survey is activated. This is a deliberate architectural choice—we wanted to avoid requiring respondents to manage cryptographic key material or install browser extensions.
But this creates a problem: if a user authenticates (say, via email/OAuth) and then the server hands them an identity, the server can trivially link user → identity. The server would know exactly who submitted each response.
This is where blind signatures come in.
RSA Blind Signatures: Breaking the Link
Blind signatures allow a server to sign a message without seeing its contents. The mathematical flow:
- Client generates a random token
m - Client blinds it:
m' = m · r^e mod nwhereris a random blinding factor and(e, n)is the server’s public key - Server signs the blinded message:
s' = (m')^d mod n - Client unblinds:
s = s' · r^(-1) mod n
The result is a valid RSA signature on m that the server produced but cannot recognize. When the client later presents (m, s), the server can verify the signature is valid but cannot link it to the blind signing request.
Application in veranon:
┌────────────────────────────────────────────────────────────────┐
│ Blind Signature Flow │
├────────────────────────────────────────────────────────────────┤
│ │
│ RESPONDENT (authenticated) SERVER │
│ ───────────────────────── ────── │
│ │
│ 1. Generate random token m │
│ Generate blinding factor r │
│ Compute m' = blind(m, r) │
│ │
│ 2. ─────── POST /blind-sign ────────► │
│ { blinded: m' } 3. Verify user is │
│ eligible respondent │
│ Mark as "claimed" │
│ Sign: s' = sign(m') │
│ ◄────── { signature: s' } ───────── │
│ │
│ 4. Unblind: s = unblind(s', r) │
│ Now have valid (m, s) pair │
│ Discard r (critical!) │
│ │
│ ═══════════ UNLINKABLE BOUNDARY ═══════════════════════════ │
│ │
│ 5. ─────── POST /claim-identity ─────► │
│ { token: m, sig: s } 6. Verify signature │
│ (valid, but no idea │
│ who requested it) │
│ Return random │
│ unclaimed identity │
│ ◄────── { identity: {...} } ─────── │
│ │
│ 7. Store identity locally │
│ Use for anonymous proof generation │
│ │
└────────────────────────────────────────────────────────────────┘
The server knows that the authenticated user (say, alice@company.com) requested a blind signature. It knows that someone later redeemed a valid signature for an identity. But it cannot determine that these are the same person. The blinding factor r mathematically severs the connection.
Preventing double-claims: Before blind signing, the server marks the user as having claimed (without knowing which identity they’ll get). This prevents one user from obtaining multiple identities.
Response Submission: No Session, No Identity
This is where the anonymity guarantee actually materializes. The response submission endpoint is completely unauthenticated. No session cookie, no JWT, no OAuth token, no user ID. The ZK proof is the only credential.
This isn’t a shortcut—it’s the entire point. Traditional anonymous surveys still require authentication to submit, which means the server logs show “user X submitted at timestamp Y.” Even if the response content isn’t linked, the metadata is. Veranon eliminates this by making the proof itself the authorization mechanism.
What the client sends:
POST /api/surveys/:id/respond
{
"answers": [...],
"proof": <ZK proof bytes>,
"nullifier": "0x7a3b...",
"merkleRoot": "0x9f2c..."
}
// Notably absent: session token, user ID, any identifier
What the server does:
-
Verify the Merkle root: Confirm it matches the survey’s current group root (prevents proofs generated against stale or tampered groups)
-
Verify the ZK proof: The Semaphore verifier checks that:
- The prover knows the secret values (trapdoor, nullifier) for some commitment in the tree
- The nullifier was correctly derived from those secrets + the survey’s external nullifier
- The proof was generated against the claimed Merkle root
-
Check nullifier uniqueness: If this nullifier has been seen before, reject (prevents double-voting)
-
Store the response: Just the answers and nullifier. No user reference, no session ID, no IP address logged.
What the server learns: A valid group member submitted this response. That’s it. The server has no way to determine which member, because the proof reveals nothing about which leaf in the Merkle tree corresponds to the prover.
┌─────────────────────────────────────────────────────────────────┐
│ Authentication vs. Proof-Based Submission │
├─────────────────────────────────────────────────────────────────┤
│ │
│ TRADITIONAL "ANONYMOUS" SURVEY │
│ ─────────────────────────────── │
│ Request: POST /submit │
│ Cookie: session=abc123 │
│ Body: { answers: [...] } │
│ │
│ Server: ✓ Verify session → user_id = 47 │
│ ✓ Log: "user 47 submitted response" │
│ ✓ Store response (maybe without user_id, but...) │
│ │
│ Reality: Server knows exactly who submitted. "Anonymity" │
│ is a policy decision, not a technical guarantee. │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ VERANON SUBMISSION │
│ ───────────────────── │
│ Request: POST /respond │
│ Body: { answers: [...], proof: <bytes>, │
│ nullifier: "0x...", merkleRoot: "0x..." } │
│ │
│ Server: ✓ Verify proof (is this a group member?) │
│ ✓ Check nullifier not reused │
│ ✓ Store response │
│ │
│ Reality: Server cannot determine who submitted. │
│ No session = no identity linkage. │
│ Proof alone authorizes the action. │
│ │
└─────────────────────────────────────────────────────────────────┘
Why this works: The ZK proof substitutes for authentication. It answers the question “should this submission be accepted?” without answering “who is submitting?” Traditional auth systems conflate these two questions. Semaphore separates them.
The nullifier as anonymous receipt: After submission, the respondent can verify their response was recorded by checking the nullifier list. They can prove to themselves (and only themselves, since they hold the identity secrets) that their response exists. But they cannot prove this to anyone else without revealing their identity—which is actually a feature, as it prevents vote-selling and coercion.
Security Considerations
What veranon protects against:
- Administrator deanonymization of responses
- Database breaches exposing respondent identities
- Collusion between administrators and infrastructure operators
- Single-user coercion (proving you voted a particular way is difficult since you don’t control which identity you get)
What veranon does not protect against:
- Small group attacks: With only 3 respondents, responses are pseudonymous at best
- Response content analysis: If you’re the only person who would know X, writing about X deanonymizes you
- Compromised client devices: If your browser is compromised, all bets are off
- Timing correlation with very small pools: If only one person is online at submission time, that’s a leak
Trust assumptions:
- The Semaphore proof system is sound (relies on elliptic curve assumptions)
- The server correctly implements the blind signature protocol
- The client correctly discards the blinding factor after unblinding
Implementation Notes
The current implementation uses:
@semaphore-protocol/corefor identity and proof management- Node.js native crypto module for RSA blind signatures (RFC 9474 compliant)
- Poseidon hash throughout (ZK-native, ~200 constraints)
- PostgreSQL for persistence with identity commitments stored in a Merkle tree structure
Proof generation happens entirely client-side. The WASM-based prover takes 1-3 seconds on modern hardware. Verification is fast (~50ms on the server).
Why This Architecture
The combination of Semaphore + blind signatures isn’t novel—it’s a standard pattern for anonymous credentials. But the specific application to organizational surveys hits a useful sweet spot:
- Organizations can track participation rates (who claimed vs who didn’t) without seeing responses
- Respondents get cryptographic guarantees, not just policy promises
- No wallet or key management required from respondents
- Server-generated identities simplify UX at the cost of trusting the server to generate them honestly
The last point deserves emphasis: respondents must trust that the server generated proper random identities and didn’t backdoor the key generation. In a higher-stakes deployment, client-side key generation with commitment schemes would be preferable, but for organizational surveys where the admin is usually somewhat trusted, the UX tradeoff is reasonable.
Veranon is a product of Rank One Labs. For technical questions or consulting inquiries about zero-knowledge implementations, reach out at rankonelabs.com.