StealthNote - ZK Proof of JWT

October 22, 2024

This is a technical explanation on how StealthNote works. StealthNote is inspired by Blind, Nozee, and similar anonymous messaging apps. Thanks to Aayush and everyone at Aztec for testing and brainstorming.

Introduction

StealthNote is an app that allows people in an organization to anonymously broadcast messages. Anyone using the app can verify that a message came from someone within the company, without learning the identity of the sender.

StealthNote uses Zero Knowledge Proofs to prove ownership of an email address from a company domain, without revealing the email address or any other personal information.

How it works

StealthNote uses ZK proof of JWT obtained from Google when users "Sign in with Google" using their company account (this currently only works for teams using Google Workspace).

StealthNote's implementation is more complex than simple JWT verification. Let's start with the basic flow and then build upon it.

"Sign in with Google" returns JWT containing the company domain

When you "Sign in with Google" in an app, Google returns something called JWT to the app (if the app requests it) - which is some information about the user, with a digital signature from Google. Technically speaking, this is the id_token of the OAuth 2.0 OpenID protocol.

Traditional applications use this to authenticate the user information before creating an account.

JWT is simply a string with the structure $headers.$payload.$signature where headers and payload are Base64-encoded. Here is an example of what the data in Google's JWT token looks like when decoded:

headers: {
  ...details on the signature scheme used (eg: sha256 + rsa)
}
payload: {
  name: "Saleel P",
  hd: "aztecprotocol.com",
  email: "abc@aztecprotocol.com",
  email_verified: true,
  nonce: "v27702e128d0eea0731caeaa7873507",
  iss: "https://accounts.google.com",
  iat: 1726663035,
  exp: 1726666635,
  ...
}
signature: V7QYQ98PqoeoE89uwmueaKxEGh8Ed...

Anyone can verify the authenticity of the above data by verifying the JWT signature (RSA signature) with Google's public key.

Use nonce to sign arbitrary data

We can set a nonce parameter (with any value) during the sign-in process, and Google will include that in the signed JWT token payload.

This way, we can have Google sign arbitrary information on behalf of the user. For example, if we set the message (hash of the message) as the nonce, then the JWT becomes a digitally signed proof that a particular user sent that message.

Verify JWT in a ZK circuit

You can share the message and the JWT with someone and convince them the message was sent by someone in the company. They can verify that the JWT was signed by Google using their public key, that the payload contains an email claim with the value xxxx@domain.com, and that the nonce is the hash of the message.

This would require sharing the whole JWT, which includes your name and email. Of course, this is not what we want. We want to hide your personal information yet convince someone the message was sent by someone in your organization. For this, we use Zero Knowledge Proofs.

Using ZK circuits, you can prove that you have executed a program correctly without revealing the inputs to the program - or revealing only selected inputs. Note that the ZK circuit (program/code) itself is public.

For StealthNote, we have built a ZK circuit using Noir that verifies the RSA signature of the JWT using Google's public key and extracts the nonce claim and the domain from the email claim.

Here, the payload and the signature are private inputs to the circuit, while nonce, domain, and JWT public key are public inputs (or outputs). The proof is generated in the browser, and no data ever leaves the user's device.

Now the verifier can verify the ZK proof generated from the above circuit and be convinced that the message was sent by someone in the organization without the need to know the original JWT payload.

Ephemeral key pair to sign messages

In the minimal implementation above, we set nonce as the hash of the message. This means that we need to do the whole OAuth sign-in process and ZK proof generation for every message - which takes about 30 seconds, leading to a poor user experience.

To solve this, we generate an ephemeral key pair (EdDSA in this case) and save it in the user's browser. We set the public key of this key pair as the nonce during the auth process and generate the ZK proof as usual. Now, anyone verifying this ZK proof is convinced that this public key belongs to someone who owns a valid Google account from the given domain.

The user can now sign any number of messages using their ephemeral private key, and share the message, the signature, the ephemeral public key, and the ZK proof to convince a verifier.

The verifier (StealthNote readers) can verify that the message was signed using some public key (standard EdDSA signature verification) and also verify the ZK proof to ensure that the ephemeral public key belongs to someone in that organization.

Note that the messages sent by the same ephemeral key can be connected. To solve this, StealthNote provides an option to rotate the ephemeral key pair.

Privacy from Google

Google can link messages to a user in the above design. They can search their OAuth logs to see which user authenticated with a particular ephemeral public key as the nonce (Note that the ephemeral public key is publicly shared for verification).

To solve this, instead of setting the ephemeral public key as the nonce, we set hash(ephemeralPublicKey, salt) as the nonce.

In the ZK circuit, we add ephemeralPublicKey as a public input and the salt as a private input. The circuit calculates hash(ephemeralPublicKey, salt) and compares it with the nonce in the JWT.

Now, Google can only see a hash of the ephemeral public key and a salt, not the actual public key.

The verifier is now verifying that the message sender knows a secret that, when hashed with the ephemeral public key, creates a `nonce` that is present on the JWT signed by Google corresponding to the domain in the `email` claim, and the message was signed using the same ephemeral key.


Below is a diagram of the ZK circuit. Some minor details are omitted for brevity.

User flow

When a new user comes in:

  • We generate a new ephemeral key pair (and the salt), complete the OAuth flow, and generate a ZK proof.
  • The ephemeral public key and the ZK proof are sent to a server.
  • The server verifies the proof and stores the ephemeral public key in the database against the company domain.

When sending a message:

  • The user signs the message using their ephemeral private key and sends the message, signature of the message, and the ephemeral public key to the server.
  • The server validates that the pubkey exists in the database, verifies the message signature, and then stores the message in the database.

When a reader wants to verify a message:

  • The server sends the message, the signature, the ephemeral public key, and the ZK proof corresponding to that message's public key.
  • The reader verifies the message signature and ZK proof in the browser.

Beyond JWT proofs

StealthNote app is designed to be a forum for anonymous messages from different anon-groups. Membership under a domain using JWT proof is only one scenario.

We can configure more groups easily as long as there is a way for users to prove membership in that group. Potential examples:

  • People living in a country or a city using zkPassport
  • People who attended a particular event using ZK Email
  • People who hold a token or an NFT using proof of Ethereum storage
  • Holders of a POAP

Trust assumptions / Potential attacks

  • Even though we hide the ephemeral public key from Google, they might be able to "probabilistically" de-anonymize users:

    • If a user sends a message immediately after registration, Google might be able to do a timing attack by finding users who completed OAuth flow around the same time.
    • If only one (or a few) users from a domain are using the app, Google can probabilistically link a message to a user.
    • This is a difficult problem to solve.
    • Note that Google may also log all logins and IP addresses, allowing them to know which people signed into the site.
  • Google can also create fake messages on behalf of any company using Google Workspace. We assume Google will not do this.

  • The server cannot forge fake messages from a domain they don't control - since the server cannot produce a valid JWT proof from that domain.

  • There is a liveness assumption on the server though. i.e., the server can censor messages.

  • Stealthnote has a likes functionality, but this only restricts one like per pubkey on a message. But the user is free to generate any number of pubkeys. We could derive a nullifier in our circuit but this might lead to a de-anonymization risk if the server colludes with Google.

  • StealthNote's ZK circuit, Noir, and Barretenberg are not audited yet, so the circuit may still have bugs.

Open problems

Below are some open problems we have. If you have any clever ideas that solve them, please let me know!

  • Privacy from the JWT issuer (timing attacks, weak anonymity set).
  • We have an internal message board for a team, but this works at the courtesy of the server where the messages are simply hidden from the public feed. It could be replaced by some cryptography (witness encryption?) where you can only decrypt the message if you can produce a ZK proof of JWT from the same domain.
  • Google rotates their JWT signing keys frequently. This means we cannot verify messages older than a few weeks. We can set up a registry to store history JWT keys, but this adds a trust assumption on the server.

Next steps

Some of the potential features that could be added:

  • Integration with Microsoft and other SSO providers.
  • Other anon-groups like the ones mentioned above.
  • Slack bridge for the internal message board (a private feed where only people in the organization can read messages).
  • Launch and L2 /s

Building similar apps

You can build similar ZK apps using Noir. Here are some resources: