Identity & Auth

Identity Layers

Anonymous credentials, JWT identity proofs, and passport verification.

The previous section covered authentication — how passkeys secure accounts and sign transactions. This section explains how Apertrue layers identity on top of authentication: anonymous credentials for rate-limited access, organisation verification via work email, and passport verification via NFC.

Each layer is independent and optional. A user can remain fully anonymous (passkey only), verify their organisation (proves they work at a specific domain), verify their personhood (proves they hold a real passport), or combine all three. Identity layers are never required for uploads — they add trust signals that the user can optionally disclose.

Identity layers architecture: Passkey Account (authentication) → Anonymous Credential (Semaphore ZK proof, all users) → two optional layers: Organisation Verification (Noir JWT proof) and Passport Verification (ZKPassport NFC proof) → User Profile (privacy-preserving trust signals)

Anonymous credentials

Every registered user gets an anonymous credential — a way to prove "I am a registered Apertrue user" without revealing which user. This is a Semaphore-style system built on noir-edwards (an open-source Noir library for Baby Jubjub elliptic curve operations) and zk-kit.noir (by Privacy & Scaling Explorations) for Poseidon2 hashing and binary Merkle tree verification.

Credential derivation

The credential secret is derived deterministically from the Aztec secret key:

Credential secret and commitment
// 1. Derive secret from Aztec key (domain-separated)
credential_secret = Poseidon2([aztec_secret, domain_tag])

// 2. Derive Baby Jubjub public key
pubkey = BJJ_generator * credential_secret

// 3. Compute commitment (this is what gets registered)
commitment = Poseidon2([pubkey.x, pubkey.y])

The commitment is a one-way hash of the public key. It is the only value shared with the backend during registration. The secret key, public key coordinates, and Aztec secret never leave the browser.

Registration

Before registering, the user generates a proof-of-knowledge: a ZK proof that they know the secret behind the declared commitment. This prevents an attacker from registering someone else's commitment. The backend verifies the proof, inserts the commitment into a Merkle tree, and returns the leaf index and Merkle path.

Membership proofs

After registration, the user can prove membership without revealing their commitment:

  1. Scope computation. Each proof has a scope — a hash of the action type (e.g., "upload"), the current epoch day, and a slot counter. Different scopes produce different nullifiers, enabling rate limiting.
  2. Merkle verification. The circuit verifies the user's commitment is in the Merkle tree using the cached path and root. This proves membership without revealing the leaf position.
  3. Nullifier generation. nullifier = Poseidon2([hashed_scope, secret_key]). The nullifier is deterministic per identity and scope — same user, same scope always produces the same nullifier. The backend rejects duplicate nullifiers.

The Merkle path is cached in IndexedDB and only re-fetched when the tree grows. A freshness check endpoint validates the cached root without exposing the commitment.

Note
Rate limiting works through scoped nullifiers. Each scope allows one proof per day. If the backend rejects a nullifier as already-used, the client automatically increments the slot counter and retries with a new scope — transparent to the user.

Organisation verification

Organisation verification proves a user owns an email at a specific domain (e.g., "reuters.com") without revealing the email address itself. It uses OAuth/OIDC with Google Workspace or Microsoft 365, followed by a ZK proof of the JWT's contents.

OAuth flow

  1. Redirect. The user clicks "Verify with Google" or "Verify with Microsoft". The browser generates a random nonce, stores it in sessionStorage, and redirects to the provider's authorization endpoint with scope openid email.
  2. Sign in. The user signs in with their work email at the provider. The provider returns a JWT containing the email claim and an RSA-2048 signature.
  3. Callback. The browser parses the JWT, verifies the nonce (preventing replay attacks), extracts the email domain, and blocks free email providers (gmail.com, outlook.com, etc.).

ZK proof generation

A dedicated worker generates a ZK proof using noir-jwt (by ZKEmail), an open-source Noir library for JWT verification. The proof demonstrates that the JWT contains a verified email at the claimed domain, without revealing the email. Apertrue adds custom domain extraction and matching logic on top of the library's RSA-2048 SHA-256 signature verification:

  • RSA signature verification. The circuit verifies the provider's RSA-2048 SHA-256 signature over the JWT, proving the token is authentic.
  • Email extraction. The circuit extracts the email claim from the JWT body and locates the @ symbol.
  • Domain matching. The circuit extracts the domain (everything after @) and asserts it matches the public input byte-by-byte.
  • Verification flag. The circuit asserts email_verified: true in the JWT claims.
What the proof revealsWhat stays private
Email domain (e.g., "reuters.com")Full email address
OAuth provider (Google or Microsoft)JWT contents and signature
Verification timestampUser's name or profile data
Use case: conflict journalism
A Reuters photojournalist covering a conflict zone uploads verified images. Their post shows "Verified member of reuters.com" — proving the images come from a working journalist at a major news organisation, without revealing their name, email, or bureau location.

The proof and a hash of the domain are committed on-chain as an IdentityNote in the ApertrueVerifier contract. The domain is also stored in the backend, linked to the user's credential commitment.

Passport verification

Passport verification proves personhood — that the user holds a real government-issued passport — using NFC scanning on a mobile phone. This feature is powered by the ZKPassport SDK and mobile app — a third-party service that handles passport NFC reading and ZK proof generation entirely on the user's phone. No passport data leaves the phone. Apertrue integrates the SDK's browser-side API to initiate verification requests and receive proofs via an encrypted WebSocket bridge.

Verification flow

  1. Request creation. The browser creates a verification request via the ZKPassport SDK, specifying what to prove: age (18+), sanctions check, and optionally nationality disclosure.
  2. QR display. The browser displays a QR code containing the request URL and a request ID.
  3. Phone scan. The user scans the QR with the ZKPassport app, which connects to a WebSocket bridge at wss://bridge.zkpassport.id.
  4. NFC read. The user taps their passport against the phone's NFC reader. The app reads the passport chip and generates a ZK proof locally — no passport data leaves the phone.
  5. Proof relay. The phone sends the proof through the encrypted bridge channel to the browser. The browser receives the verified result.
  6. Storage. The browser stores the proof in wallet metadata and commits a hash of the proof and nullifier on-chain as a PassportIdentityNote.

What gets proven

ClaimTypePrivacy
Age >= 18BooleanOnly reveals adult status, not birth date
Not sanctionedBooleanChecked against sanctions lists, reveals only pass/fail
NationalityISO 3166-1 alpha-3Optional — user chooses whether to disclose
NullifierHashUnique per passport, prevents duplicate registrations

The nullifier is the key sybil resistance mechanism. Each passport produces a unique nullifier — one person cannot register multiple identities. The nullifier is derived from the passport's cryptographic data, so it's consistent across scans but reveals nothing about the passport holder.

Use case: anonymous source protection
An anonymous source documenting human rights abuses uploads images with a "Verified Human" badge. Viewers know the content comes from a real person — not a bot farm or state-sponsored troll account — without learning the source's name, nationality, or any identifying detail.
NFC security
Modern passports contain an NFC chip with a signed data group (SOD) that chains to the issuing country's CSCA certificate. The ZKPassport app verifies this chain locally on the phone, then proves in zero knowledge that the verification passed. The browser never sees the passport data, the name, the photo, or the document number — only the boolean results and the nullifier.

How the layers compose

Identity layers are additive. Each layer is independent and optional:

LayerWhat it provesDisclosure optionsSybil resistance
Anonymous (default)Registered Apertrue userNothing revealedOne credential per Aztec account
OrganisationWork email at a specific domain"Verified member of reuters.com" (domain only)Email verified by provider (no passport-level)
PassportHolds a real government passport"Verified Human" badge, optionally nationalityOne nullifier per passport (strongest)

All identity proofs are linked to the same credential commitment — a deterministic hash derived from the user's Aztec secret. This means the backend can associate JWT and passport proofs with the same user, but the commitment reveals nothing about the user's actual identity.

Per-upload disclosure

When uploading, users choose which identity signals to attach to each post:

  • Organisation. Toggle between "hidden" and "domain". If disclosed, the post shows "Verified member of reuters.com" — proving the uploader works at that organisation without revealing their name or email.
  • Nationality. Toggle between "hidden" and "country". If disclosed, the post shows the country flag and name. Requires a passport verification with nationality disclosure enabled.
  • Verified human. Toggle between "hidden" and "verified_human". If disclosed, the post shows a "Verified Human" badge — proving the uploader is a real person with a valid passport, without revealing any personal details.

These choices are stored in the encrypted media index alongside the photo's other disclosure preferences (time, location, device). Different uploads from the same user can have different identity disclosures.

Circuit reference

Anonymous credential circuit

AspectDetail
Private inputsSecret key, Merkle path (20 fields), path indices (20 bits), depth
Public inputsHashed scope, hashed message
Public outputsMerkle root, nullifier
OperationsBJJ scalar multiplication, Poseidon2, binary Merkle verification
Sybil preventionNullifier = Poseidon2([scope, secret]) — deterministic per identity per scope

Credential registration circuit

AspectDetail
Private inputsSecret key
Public inputsCommitment
Public outputsComputed commitment (must match public input)
OperationsBJJ scalar multiplication, Poseidon2
PurposeProof-of-knowledge — proves the registrant knows the secret behind the commitment

JWT identity circuit

AspectDetail
Private inputsJWT bytes (up to 640), partial SHA-256 state, RSA modulus/signature (18 limbs each)
Public inputsRSA modulus, domain bytes (up to 64)
Public outputsNone (assertion-based)
OperationsRSA-2048 SHA-256 verification, UTF-8 email parsing, domain extraction and matching
PrivacyProves email domain without revealing email address
Key Insight
The credential commitment serves as the bridge between all identity layers. It is derived deterministically from the Aztec secret, generated once, and used as the foreign key for JWT proofs, passport proofs, and Merkle tree membership. This unifies the identity system without creating linkability between uploads.
Open-source foundations
The identity system builds on several open-source projects: noir-edwards for Baby Jubjub elliptic curve operations, zk-kit.noir (Privacy & Scaling Explorations) for Poseidon2 and Merkle trees, noir-jwt (ZKEmail) for in-circuit JWT verification, ZKPassport for passport NFC reading and proof generation, and ZKPassport's noir_rsa fork for RSA signature verification in the proof circuits. Apertrue's contributions are the credential registration and membership protocols, scoped nullifier rate limiting, OAuth flow integration, domain extraction logic, and the per-upload disclosure system tying these layers together.

The next section covers media processing and storage — how images are stripped, hashed, encrypted, and stored after verification.