ZKAuth: How We Turn Your Email into a ZK Wallet

Feature image
LightLink
LightLink9 min read

ZKAuth turns your email into a self-sovereign Web3 wallet using zero-knowledge proofs. No seed phrases, no extensions. Just log in with Google or Apple and go. In this post, our core developer Kass explains how it all works under the hood.

In this guide, LightLink core developer Kass breaks down how ZKAuth transforms a simple Google login into a self-sovereign Web3 wallet using zero-knowledge proofs. From onboarding flows to smart contract design, Kass walks through the full system, explaining how ZKAuth preserves privacy, removes the need for seed phrases, and sets the stage for seamless Web3 authentication.

Disclaimer: ZKAuth is currently experimental and available as a proof-of-concept only. The library and deployment are not yet publicly available.

The Problem: Web3's Onboarding Nightmare

Getting normies into Web3 is hard. Onboarding feels like IKEA instructions, except without the cute cartoon guy to help you. The current experience is brutal:

“Hey, want to try this cool NFT game?

First, download MetaMask. Then write down these 12 random words on a piece of paper (don’t lose them or you’ll lose everything!). Fund your wallet with some ETH. Then confirm a transaction in a UI that looks like it was designed by aliens.”

Normie immediately nopes out.

The mnemonic seed phrase system, while cryptographically elegant, is a UX disaster. Users either:

  • Screenshot their seed phrase (security nightmare)
  • Lose their seed phrase (funds gone forever)
  • Get overwhelmed and never start using crypto

Meanwhile, custodial solutions like centralized exchanges solve some UX pain; but introduce new ones: your crypto isn’t really yours, you’re trusting a third party with your funds, and you still have to KYC which might be worse UX than the seed phrase thing.

Enter ZKAuth: OAuth Meets Zero-Knowledge

What if we could combine the simplicity of “Login with Google” with the security and self-sovereignty of a non-custodial wallet? That’s what ZKAuth does.

You get a full Web3 wallet that:

  • You control
  • Isn’t linked to your social account
  • Preserves your privacy
  • Can’t be tied back to you

So how is this different from Privy?

We love Privy; we use it! But this is a different approach.

Privy gives you embedded wallets with social login via MPC (multi-party computation), meaning they hold a piece of your key. Often, your wallet is also linked to your email behind the scenes and that info can be shared with DApps. This setup is convenient, but it comes at the cost of privacy. It can open the door to spam emails, phishing attempts, and potential tracking across platforms.

ZKAuth takes a different route. It uses zero-knowledge proofs to deterministically generate a self-sovereign wallet from your OAuth login, without ever storing or exposing your private keys. You’re not tied to any service provider beyond the auth source.

Zero-knowledge is your new middleman...did we just solve onboarding?

A Quick Primer on Zero-Knowledge Proofs

Let’s first cover why ZK proofs are so powerful. A zero-knowledge proof is like a mathematical magic trick: it lets you prove you know something without revealing what that something is.

Example:

Alice wants to prove to Bob that she knows the password to a system without telling him the password. ZK proofs let her prove that knowledge mathematically, and Bob can verify it without learning the actual password.

For ZKAuth, these proofs let you say:

  • “I have a valid Google account”
  • “This ephemeral key belongs to me”
  • “I should control this wallet”

All without revealing your email, user ID, or any personal information.

The exact details of how ZK proofs work are happily ignored in this article. It’s complex stuff, but if you’re a sadist like us, start here: https://rareskills.io/zk-book 

Turning Google Login Into a Web3 Wallet

Here’s what the login flow looks like:

  1. User visits a ZKAuth-integrated app → "Login with Google" button appears
  2. Ephemeral key generation → A temporary key pair is generated in your browser (secure iframe, dapp cant access)
  3. OAuth dance → Standard Google login, but we include the ephemeral public key in the JWT claims
  4. ZK proof generation → Your JWT gets processed through our circuits to generate proofs
  5. Wallet deployment → Your ZK wallet gets deployed (or accessed if it exists)
  6. Transaction execution → Your ephemeral key can now control your ZK wallet

The real magic lives in Steps 4–5, where zero-knowledge bridges Web2 authentication with Web3 ownership.

How it Works: A System Overview

First let's break down the key components in the system:

Component Description
ZK Wallet A smart contract wallet deployed deterministically via CREATE2. Its address is derived from your OAuth identity (privately). Think of it as your permanent crypto identity tied to your Google account.
Ephemeral Key A temporary key pair generated in your browser session. This is what actually signs transactions, but it's cryptographically linked to your ZK wallet through zero-knowledge proofs.
Address Seed Your unique identifier derived from poseidon([iss, sub, salt]) where:
  • iss = OAuth issuer (like "accounts.google.com")
  • sub = Your unique user ID from Google
  • salt = A random value to prevent rainbow table attacks
ZK Circuits The mathematical heart of the system. These circuits prove three things:
  • You have a valid JWT from a trusted issuer
  • Your ephemeral key is included in the JWT claims
  • Your address seed is correctly derived from your OAuth identity
Proving Service Currently handles the computationally intensive proof generation (more on this later).

Circuit Breakdown

The ZK circuits are where it gets spicy. They take secret inputs, do some logic, and produce public outputs

Let's break down the flow into digestible chunks:

Circuit Inputs

All the inputs are passed to the circuit as signals or arrays of signals. Signals are essentially 255 bit numbers.

INPUTS:

  • jwt_messagesignal[]
    The raw JWT payload from Google
  • payload_startsignal
    The start position of the JWT payload
  • field_positions: Start/end positions for extracting specific JWT fields
    • Identity JWT fields: iss_startiss_end, sub_startsub_end
    • Ephemeral key: eph_starteph_end
  • address_saltsignal
    Random value for deterministic address generation

The circuit first validates that all inputs are properly formatted and within expected ranges.

1. Decoding the JWT

The JWT is a base64 encoded byte array. The circuit needs to decode it.

// pseudocode
// Decode the JWT
payload: signal[] = base64_decode(jwt_message[payload_start:])

Decoding the JWT is a computationally expensive operation … and it was a pain to implement.

2. Extract OAuth Issuer and User

These fields identify you as a specific user of a specific OAuth provider.

// pseudocode
issuer: signal = extract_field(jwt_message, iss_start, iss_end)
// Example: "accounts.google.com"

subject: signal = extract_field(jwt_message, sub_start, sub_end)
// Example: "108234567890123456789"

This step proves you're using a JWT from a trusted OAuth provider. The circuit extracts the iss (issuer) field from the JWT payload to verify it matches expected values like "accounts.google.com".

The sub (subject) field contains your unique user ID from Google. This is what makes your wallet deterministic; the same Google account always generates the same wallet address.

3. Derive Deterministic Address

// pseudocode
address_seed = poseidon_hash(iss, sub, address_salt)

This creates your unique wallet identifier. The Poseidon hash ensures that:

  • Same OAuth account = same wallet address
  • Different salt = different wallet (for privacy)
  • Output is cryptographically secure

4. Extract Ephemeral Address

// pseudocode
ephemeral_addr = extract_field(jwt_message, eph_start, eph_end)
// Example: "0x742d35Cc6634C0532925a3b8D0e6d03341e58b37"

This proves that your ephemeral key (the one actually signing transactions) is cryptographically linked to your OAuth identity by being included in the JWT claims.

5. Generate Message Hash

sha256_hash = sha256(jwt_message)

Instead of verifying the RSA signature inside the circuit (which would be computationally expensive), we output the SHA256 hash of the entire JWT. The smart contract then verifies this hash against Google's public key.

6. Generate Proof Outputs

OUTPUTS:

- jwt_hash: For on-chain signature verification

- ephemeral_address: The temporary key that can control your wallet

- address_seed: Your deterministic wallet identifier

- validity_flag: Confirms all constraints were satisfied

The circuit outputs these values along with a zero-knowledge proof that all the extraction and derivation steps were performed correctly.

Important detail: We don't verify the JWT signature inside the circuit (RSA verification is computationally expensive in ZK). Instead, we output the SHA256 hash of the JWT message, and the smart contract verifies the signature against known issuer public keys.

The Proving Problem: Speed vs. Privacy

ZK proof generation is computationally intensive, like, really intensive. Running these circuits on your phone would take minutes, which is unacceptable for UX. So for now, we use a centralized proving service using RapidSNARK that gets it done in <10s.

Here’s the flow:

1. Format the inputs

We preprocess the inputs turning them into an array of signals (255 bit numbers).

inputs, _, err := GenInputs(jwt, ephemeralAddress)

This is fast enough to run on the client side.

2. Generate witness from inputs

The witness is the complete set of all intermediate values that satisfy every constraint in the circuit. Think of it as "showing your work" in a math problem; it contains the value of every signal/variable that makes all the circuit equations true.

witness, err := z.prover.CalculateWitness(ctx, inputs.ToMap())

This step runs through the entire circuit logic with your specific inputs, computing all the intermediate values (like the extracted issuer, subject, address seed, etc.). The witness proves that a valid solution exists, but it stays private; the ZK proof will later prove the witness. The witness contains all the values, including ones we want to keep private.

3. Generate ZK proof

proof, publicInputs, err := z.prover.GenerateProof(ctx, witness)

This step generates the actual ZK proof. It's a cryptographic commitment to the witness, proving that a valid solution exists without revealing the witness itself.

But Is This Safe?

Kind of. The proving service sees your JWT (signed by Google), which contains the ephemeral key. It can’t access your funds or spoof the proof. But yes, it could correlate your wallet with your OAuth identity.

The long-term solution: We're working on smaller, more efficient circuits that can run on-device. The goal is to eliminate the need for the proving service entirely while maintaining reasonable proof generation times. 

The Smart Contracts

The second part of the system lives on-chain.

The Birds and the Bees: How baby wallets are made

ZkAuthFactory is the contract that deploys your ZK wallet. It's a simple factory contract that takes the ZK proof as input and deploys a new ZK wallet using CREATE2.

Simplified pseudo code:

contract ZkAuthFactory {
    function createWallet(
        // These params are the zkproof
        uint[2] calldata _pA,
        uint[2][2] calldata _pB,
        uint[2] calldata _pC,
        uint[71] calldata _pubSignals
    ) public {
        // Verify the ZK proof
        (bytes20 addressSeed, address ephemeralWallet) = verifier
            .LinkEphemeralAddress(_pA, _pB, _pC, _pubSignals); // verifier contract verifies and links the Ephemeral Address to the address seed.

        // Deploy wallet using CREATE2 for deterministic addresses
        address walletAddress = deployer.deployAndCall(
            abi.encodePacked(
                type(ZKAuthWallet).creationCode,
                abi.encode(address(verifier), addressSeed)
            ),
            addressSeed, // CREATE2 salt
            abi.encodeWithSelector(
                Ownable.transferOwnership.selector,
                ephemeralWallet
            )
        );
    }
}

This means the wallet address is deterministic and can be derived from the OAuth identity and the ephemeral key. This also improves UX, as we can show users their wallet address even before the proof is generated and the wallet is created. The beauty of CREATE2 determinism is that the address remains the same across all networks, giving you a consistent crypto identity.

Personal ZK Nightclub Bouncer: The Wallet Contract

Your ZK wallet is a simple contract that allows ONLY the ephemeral keys to execute transactions on its behalf.

Verifier is a contract that both verifies ZK Proofs, and also contains a mapping of address seeds to ephemeral keys. It's the bouncer at the nightclub, checking your (ZK) ID and letting you in.

// Simplified pseudo code
contract ZKAuthWallet is Ownable {
    ZkAuthVerifier public verifier;
    bytes20 public addressSeed;

    function execute(
        address to,
        uint256 amount,
        bytes calldata data
    ) public payable {
        require(
            verifier.isLinked(msg.sender, addressSeed) || // the zkproof linked this address seed to the ephemeral key in the previous step.
            msg.sender == owner(),
            "Not authorized"
        );

        (bool success, ) = to.call{value: amount}(data);
        require(success, "Execution failed");
    }
}

So, what comes after this?

We still need to trust that Google, Twitter, and other providers aren’t malicious and won’t misrepresent user data. While that’s not ideal from a decentralization perspective, ZKAuth marks a shift in how we approach Web3 onboarding. By leveraging zero-knowledge proofs, we can create truly self-sovereign wallets that feel as seamless as any Web2 login.

The current implementation is experimental and limited to proof-of-concept deployments. We're actively working on:

  • Smaller, faster circuits for on-device proving
  • Additional OAuth providers (GitHub, Discord, etc.)
  • Fine-grained permission controls for ephemeral keys
  • Multi-network deployment tooling
  • Adding fallback control in case the Auth provider's (e.g. Google) is malicious, down, or just deletes your account.

I don't think private keys are going away, nor should they, but the future of crypto is reaching a wider audience who aren't Web3-savy. ZKAuth is a step in the right direction that doesn’t compromise on what crypto stands for.

Ready to reimagine Web3 onboarding?


ZKAuth is just the beginning. Dive into Kass’s work to explore how zero-knowledge proofs and OAuth can unlock a new wave of privacy-first, self-sovereign wallet creation; no extensions, seed phrases, or clunky flows required.

Whether you're building seamless login experiences, experimenting with ZK circuits, or pushing the boundaries of user-friendly crypto infrastructure, LightLink gives you the tools to make it happen.

Join our community of developers, builders, and ZK tinkerers on X, Discord, Telegram, and LinkedIn. Help shape the future of private, scalable Web3 apps today.

Join our 200,000+ Strong Community

Be part of a thriving network where you can connect, collaborate, and earn rewards through exciting on-chain and off-chain campaigns

RoyDanDanRoy