LogoLogo
LogoLogo
  • Introduction
    • About Us
    • Notations & Definitions
      • MPC
      • ZK
    • Contribute to this Site!
  • Primitives
    • Multiplication
      • Karatsuba Multiplication
      • Toom-Cook Multiplication
    • NAF (Non-adjacent form)
    • Chinese Remainder Theorem (CRT)
    • Euclidean Algorithm
      • Extended Euclidean Algorithm
      • Binary Euclidean Algorithm
      • Extended Binary Euclidean Algorithm
    • Coding Theory
      • Linear Code
    • Number Theoretic Transform
    • Abstract Algebra
      • Group
        • -Morphisms
        • Batch Inverse
      • Elliptic Curve
        • Weierstrass Curve
          • Coordinate Forms
          • Fast Elliptic Curve Arithmetic and Improved WEIL Pairing Evaluation
        • Edwards Curve
          • Coordinate Forms
          • Twisted Edwards ↔ Short Weierstrass Transformation
        • Batch Inverse for Batch Point Additions
        • Scalar Multiplication
          • Double-and-add
          • GLV Decomposition
        • MSM
          • Pippenger's Algorithm
          • Signed Bucket Index
          • CycloneMSM
          • EdMSM
          • cuZK
        • 2-Chain and 2-Cycle of Elliptic Curves
    • Encryption Scheme
      • ElGamal Encryption
    • Modular Arithmetic
      • Modular Reduction
        • Barrett Reduction
        • Montgomery Reduction
      • Modular Inverse
        • Bernstein-Yang's Inverse
    • Multiset Check
    • Sumcheck
    • Commitment Scheme
      • Fflonk
      • SHPlonk
      • Zeromorph
  • MPC
    • Yao's Garbled Circuits
    • GMW
    • BMR
  • ZK
    • Arithmetization
      • R1CS
      • PLONK
      • AIR
    • Folding
      • LatticeFold
      • Nova
        • Nova over Cycles of Curves
    • Lookup
      • Lasso
      • LogUp-GKR
    • SNARK
      • Groth16
      • HyperPlonk
      • Spartan
        • SPARK
    • STARK
      • Additive NTT
      • Basefold
      • Binius
      • Brakedown
      • CircleSTARK
      • FRI
        • FRI Security Features and Optimizations
      • DEEP FRI
      • STIR
      • WHIR
    • Distributed ZK
      • Ryan's Trick for Distributed Groth16
  • Application
    • zkLogin
    • zkHoldem
    • zkTLS
      • DECO
      • Proxying is enough
  • zkVM
Powered by GitBook
On this page
  • Introduction
  • Background
  • OIDC (OpenID Connect)
  • JWT Example
  • Protocol Explanation
  • Transaction Signature with JWT
  • Transaction Signature with nonce-less JWT
  • Circuit Implementation
  • Proving Scheme
  • Prover Service
  • Conclusion
  • References
Export as PDF
  1. Application

zkLogin

Presentation: https://www.youtube.com/watch?v=vVMS1HbDoEM

PreviousRyan's Trick for Distributed Groth16NextzkHoldem

Last updated 1 month ago

Introduction

In the traditional Web3 environment, users must manage their mnemonics directly, which poses a challenge since losing or having them stolen makes recovery difficult. Additionally, when users switch devices, they must restore their accounts using their self-managed mnemonics. In contrast, introduces an OAuth authentication method leveraging ZK to overcome the limitations of conventional Web3 login mechanisms and enhance the user experience. Notably, operates in a similar manner.

Background

OIDC (OpenID Connect)

is a protocol that grants applications permission to access a user's data on their behalf. For example, to obtain file access permissions for Google Drive with OAuth2, the following steps are taken:

  1. The user attempts to access Google Drive files through an application (MyApp).

  2. MyApp requests user authorization from Google.

  3. The user logs in to Google and grants permission.

  4. Google issues an Access Token to MyApp.

  5. MyApp sends the Access Token to the Google Drive API to access the files.

On the other hand, is a protocol based on OAuth2 that verifies a user's identity. In other words, it determines, "Is this person really who they claim to be?" For instance, when logging in using a Google account, the following steps occur:

  1. The user clicks the "Login with Google" button on MyApp.

  2. MyApp sends a login request to Google.

  3. The user completes the Google login process.

  4. Google issues an ID Token to MyApp.

  5. MyApp verifies the ID Token and processes the user's login.

Entities such as Google, Facebook, and GitHub that provide this protocol are called OIDC Providers. The issued ID Token is known as a JWT (JSON Web Token).

JWT Example

To run the following script, you need to install the required packages locally:

pip install cryptography pyjwt 
import jwt
import base64
import json
import time
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

# Example JWT payload with nonce
payload = {
    "iss": "https://accounts.google.com",
    "sub": "1234567890",
    "aud": "4074087",
    "iat": int(time.time()),
    "exp": int(time.time()) + 3600,  
    "name": "Ryan Kim",
    "email": "ryan@a41.io",
    "nonce": "random_nonce_value"  
}

# Generate a new RSA private key
private_key_obj = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048
)

# Serialize the private key to PEM format
private_key_pem = private_key_obj.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.TraditionalOpenSSL,
    encryption_algorithm=serialization.NoEncryption()
).decode()

# Serialize the public key to PEM format
public_key_pem = private_key_obj.public_key().public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode()

# Generate JWT with RS256 signature using the new private key
jwt_signed = jwt.encode(payload, private_key_pem, algorithm="RS256")

print(jwt_signed)

Each field in the JWT has the following meaning:

  • iss (issuer): Represents the entity (Identity Provider, IdP) that issued the JWT. In this case, it indicates that Google issued the token.

  • sub (subject): The unique ID of the authenticated user. Providers like Google, Twitch, and Slack use the same sub across all aud values. However, Apple, Facebook, and Microsoft generate a unique sub for each aud.

  • aud (audience): Specifies the target service (client ID) for which this JWT is intended.

  • iat (issued at): The timestamp (Unix timestamp in seconds) when the JWT was issued.

  • exp (expiration): The expiration timestamp (Unix timestamp in seconds) after which the JWT becomes invalid.

  • nonce: A random value used to prevent replay attacks by ensuring request and response integrity.

Then, the OIDC Provider signs the JSON data using the following method:

This verification process can be performed by a blockchain validator. Here, the nonce can be determined by the audience, i.e., the wallet application, and we aim to leverage this mechanism to improve the existing login process.

Protocol Explanation

Transaction Signature with JWT

To reiterate, our goal is to ensure that users do not need to remember anything. This means we need to replace the traditional method of signing transactions with a private key derived from a mnemonic and the way we generate an on-chain address from it. The new approach should be both efficient and secure, ensuring that the OIDC Provider does not introduce security risks.

Method 1: Signing Transactions Using OIDC

The first approach is to embed the transaction inside the nonce. By doing so, users can sign transactions via Google login (or any OIDC protocol) instead of generating a private key from a mnemonic. The user's on-chain address can then be derived as follows:

Method 2: Introducing ZK

To address the issue from Method 1, we can design a ZK circuit that hides sub while still ensuring its validity.

Method 3: Adding a salt to Address Derivation

TODO(chokobole): add how to ensure that only the user can derive their salt.

Next, we modify the ZK circuit accordingly:

However, this approach presents two major challenges:

  1. Performance Overhead: A new ZK proof must be generated for every transaction, and validators must verify these proofs, which adds computational cost.

  2. Potential for Abuse: Since the OIDC Provider holds the signing private key for transactions, it could be exploited maliciously.

Method 4: Signing with an Ephemeral Key

Flow Overview

This approach has several advantages:

  • Reduced Proof Overhead: A ZK proof only needs to be generated when updating the ephemeral key pair, reducing computation costs.

  • Enhanced Security: Users create the signing private key themselves, preventing abuse by the OIDC Provider.

Method 5: Adding Randomness to the nonce

Now, the OIDC Provider can no longer determine which transactions a given sub has signed, ensuring stronger privacy protection.

Method 6: Updatable Ephemeral Key Expiration

Typically, JWTs are valid for one hour, which is too short in the blockchain domain and inconvenient due to its reliance on real-time expiration. Instead, we extend the validity based on block height rather than real-world time.

For example, if we want the key to be valid for 10 hours and the blockchain's block time is 10 minutes, we can set:

We then update the nonce with:

Additionally, the validator must check that:

The updated ZK circuit is as follows:

Method 7: Adding iss to Public Input

The ZK circuit is further modified as:

Additionally, the validator must check that:

Through these methods, we successfully achieve the initial goal of passwordless Web3 authentication while maintaining security and privacy.

Transaction Signature with nonce-less JWT

This raises an important question: how can we implement zkLogin without a nonce in the JWT?

Security Concern: Transaction Signature Manipulation

One key issue arises with this approach.

Circuit Implementation

When an OIDC Provider signs a JWT, it typically follows this process:

  • SHA-2 computation – 74%

  • JSON parsing, Poseidon hashing, Base64 encoding, and additional constraints – 11%

JWT parsing

One novel aspect of zkLogin is JWT parsing, with two key optimizations:

  1. Header Parsing Optimization:

    • Since JWT headers are public, they are decoded outside the circuit to reduce computational overhead.

  2. Selective Payload Parsing:

    • Based on the following assumptions:

      • The OIDC Provider adheres to the JSON specification.

      • The required fields (e.g., sub, iss, aud, nonce) are located at the top level of the JSON structure.

      • All JSON values are either strings (sub, iss, aud, nonce) or booleans (email_verified).

      • JSON keys do not contain escaped quotes (e.g., \"sub\" is not allowed).

Example JSON:

{"sub":"123","aud":"mywallet","nonce":"ajshda"}

Selective Payload Parsing Algorithm

How to implement indexing operator in Circuit

Optimization via Packing

  • JWT elements are 8-bit values.

  • BN254 scalar field has a width of 253 bits.

  • This allows us to pack 16 elements at once, reducing constraints.

Proving Scheme

In addition, ZK proof verification can be optimized through engineering techniques, such as caching frequently used proofs.

Prover Service

Beyond performance, there are security concerns as well. If there were bugs in circom or the ZK circuit, users’ funds could be at risk. By centralizing proof generation on a server, such risks can be mitigated.

However, this approach introduces several risks:

  • Liveness Issue: If the prover service goes down, users will be unable to send transactions.

  • Cost Issue: If the prover service generates a large number of proofs, it incurs significant computational costs.

  • Centralization Issue: Users must trust the prover service to be honest and reliable.

Conclusion

Take a look at this timestamped video to see zkLogin in action.

zkLogin significantly improves UX by leveraging OIDC, allowing users to avoid managing mnemonics and other sensitive information. This innovation is a step forward toward mass adoption of Web3.

However, there are trade-offs:

  • Web3 was initially created to move beyond Web2. Yet, zkLogin relies on Web2 services to enhance the Web3 experience, which feels somewhat contradictory.

  • Client-side proof generation is crucial for fully decentralized authentication, but due to performance constraints, zkLogin currently relies on server-side proof generation, which is not ideal.

  • Validators must verify numerous ZK proofs, which is why Groth16 was chosen—it requires only three pairing operations per proof. However, instead of optimizing proof generation, both zkLogin and Keyless Wallet optimize circuit design for performance. This leads to an inconvenience in development since each circuit update requires a new trusted setup, posing a DevEx challenge.

Despite these limitations, zkLogin represents a meaningful step toward improving Web3 usability, and with further optimizations, it could strike a better balance between decentralization, security, and user experience.

References

You can decode the JWT at , which will display the following structure:

JWT.Issue(skOIDC,claim)→jwt\mathsf{JWT.Issue}(\mathsf{sk_{OIDC}, claim}) \rightarrow \mathsf{jwt}JWT.Issue(skOIDC​,claim)→jwt

where skOIDC\mathsf{sk_{OIDC}}skOIDC​ is the secret key of the OIDC Provider, claim={iss,sub,aud,iat,exp,nonce,…}\mathsf{claim} = \{\mathsf{iss, sub, aud, iat, exp, nonce, \dots}\}claim={iss,sub,aud,iat,exp,nonce,…} represents the claims included in the jwt\mathsf{jwt}jwt, and jwt\mathsf{jwt}jwt is the signed token that contains both claim\mathsf{claim}claim and the signature σOIDC\sigma_{\mathsf{OIDC}}σOIDC​.

On the client side, the generated jwt\mathsf{jwt}jwt can be verified as follows:

JWT.Verify(pkOIDC,jwt)→b∈{0,1}\mathsf{JWT.Verify}(\mathsf{pk_{OIDC}, jwt}) \rightarrow b \in \{0, 1\}JWT.Verify(pkOIDC​,jwt)→b∈{0,1}

For further details on why OIDC was chosen over alternatives such as Passkey, MPC, and HSM, interested readers can refer to .

We explore various approaches to achieve this goal. The following explanation is inspired by , as it provides a clear and intuitive understanding.

addr=H(iss∥sub∥aud)\mathsf{addr} = H(\mathsf{iss \| sub \| aud})addr=H(iss∥sub∥aud)

Refer to of the for the reason why aud is included.

However, this approach has a drawback: the validator can infer the relationship between sub and addr\mathsf{addr}addr, potentially exposing user identity information.

C(x:{addr,tx,pkOIDC},w:{jwt}):H(jwt.iss∥jwt.sub∥jwt.aud)=?addr∧JWT.Verify(pkOIDC,jwt)=?1∧jwt.nonce=?txC(x: \{\mathsf{addr, tx, pk_{OIDC}}\}, w: \{ \mathsf{jwt} \}): \\H(\mathsf{jwt.iss \| jwt.sub \| jwt.aud}) \stackrel{?}= \mathsf{addr} \land \mathsf{JWT.Verify}(\mathsf{pk_{OIDC}, jwt}) \stackrel{?}=1 \land \mathsf{jwt.nonce} \stackrel{?}= \mathsf{tx}C(x:{addr,tx,pkOIDC​},w:{jwt}):H(jwt.iss∥jwt.sub∥jwt.aud)=?addr∧JWT.Verify(pkOIDC​,jwt)=?1∧jwt.nonce=?tx

Here, we use the notation from .

However, this approach still has a flaw: the OIDC Provider can infer the relationship between sub and addr\mathsf{addr}addr, which could compromise user privacy.

To further improve privacy, we introduce a salt\mathsf{salt}salt when deriving the user’s on-chain address:

addr=H(iss∥sub∥aud∥salt)\mathsf{addr} = H(\mathsf{iss \| sub \| aud \| \textcolor{red}{salt}})addr=H(iss∥sub∥aud∥salt)

With this modification, unless the OIDC Provider knows the salt\mathsf{salt}salt, it cannot link the user's sub to their on-chain activity. The only requirement is ensuring that the user does not lose their salt\mathsf{salt}salt. However, since our goal is for users to remember nothing, we need a mechanism that allows only the user to generate their salt\mathsf{salt}salt securely. (This document does not cover how to achieve this—refer to or of the for details.)

C(x:{addr,tx,pkOIDC},w:{jwt,salt}):H(jwt.iss∥jwt.sub∥jwt.aud∥salt)=?addr∧JWT.Verify(pkOIDC,jwt)=?1∧jwt.nonce=?txC(x: \{\mathsf{addr, tx, pk_{OIDC}} \}, w: \{ \mathsf{jwt, \textcolor{red}{salt}} \}): \\H(\mathsf{jwt.iss \| jwt.sub \| jwt.aud \| \textcolor{red}{salt}}) \stackrel{?}= \mathsf{addr} \land \mathsf{JWT.Verify}(\mathsf{pk_{OIDC}, jwt}) \stackrel{?}=1 \land \mathsf{jwt.nonce} \stackrel{?}= \mathsf{tx}C(x:{addr,tx,pkOIDC​},w:{jwt,salt}):H(jwt.iss∥jwt.sub∥jwt.aud∥salt)=?addr∧JWT.Verify(pkOIDC​,jwt)=?1∧jwt.nonce=?tx

Now, we introduce an ephemeral key pair (esk,epk)(\mathsf{esk}, \mathsf{epk})(esk,epk). The transaction is signed using this ephemeral key pair, allowing users to regenerate their key pair via Google login or another OIDC provider if they ever lose it. This ensures that users do not need to remember anything.

Additionally, by sending the proof that includes the new ephemeral public key epk\mathsf{epk}epk, the user updates their ephemeral key on-chain, ensuring the system recognizes the new key for future transactions.

Sig.Sign(esk,tx)→σtxSig.Verify(epk,tx,σtx)→b∈{0,1}\mathsf{Sig.Sign}(\mathsf{esk, tx}) \rightarrow \sigma_{\mathsf{tx}} \\ \mathsf{Sig.Verify}(\mathsf{epk, tx}, \sigma_{\mathsf{tx}}) \rightarrow b \in \{0, 1\}Sig.Sign(esk,tx)→σtx​Sig.Verify(epk,tx,σtx​)→b∈{0,1}

Next, we embed epk\mathsf{epk}epk into the nonce and modify the ZK circuit accordingly:

C(x:{addr,epk,pkOIDC},w:{jwt,salt}):H(jwt.iss∥jwt.sub∥jwt.aud∥salt)=?addr∧JWT.Verify(pkOIDC,jwt)=?1∧jwt.nonce=?epkC(x: \{\mathsf{addr, \textcolor{red}{epk}, pk_{OIDC}} \}, w: \{ \mathsf{jwt, salt} \}): \\ H(\mathsf{jwt.iss \| jwt.sub \| jwt.aud \| salt}) \stackrel{?}= \mathsf{addr} \land \mathsf{JWT.Verify}(\mathsf{pk_{OIDC}, jwt}) \stackrel{?}=1 \land \mathsf{jwt.nonce} \stackrel{?}= \textcolor{red}{\mathsf{epk}}C(x:{addr,epk,pkOIDC​},w:{jwt,salt}):H(jwt.iss∥jwt.sub∥jwt.aud∥salt)=?addr∧JWT.Verify(pkOIDC​,jwt)=?1∧jwt.nonce=?epk

Key Generation: The user generates an ephemeral key pair (esk,epk)(\mathsf{esk}, \mathsf{epk})(esk,epk).

Proof Creation: The user constructs a ZK proof π\piπ proving ownership of their identity.

Tx Submission: The user signs a transaction tx\mathsf{tx}tx and signature using esk\mathsf{esk}esk, producing σtx\sigma_{\mathsf{tx}}σtx​.

Submission & Key Update: The user submits (π,epk,tx,σtx)(\pi, \mathsf{epk}, \mathsf{tx}, \sigma_{\mathsf{tx}})(π,epk,tx,σtx​), to the validator. Upon verification, the validator may update the user's ephemeral public key (epk\mathsf{epk}epk) on-chain.

Subsequent Transactions: Until the ephemeral key expires, the user only needs to submit a new transaction tx′\mathsf{tx}'tx′ and its signature σtx′\sigma_{\mathsf{tx'}}σtx′​ to the validator, without regenerating a new proof.

However, one issue remains: the OIDC Provider can still infer which transactions were signed by a given sub through the epk\mathsf{epk}epk.

A simple solution, such as hashing epk\mathsf{epk}epk before embedding it into nonce, is insufficient because epk\mathsf{epk}epk is public. An adversary could still brute-force the mapping between epk\mathsf{epk}epk and sub.

To prevent this, we introduce randomness r\mathsf{r}r and modify the nonce by hashing both epk\mathsf{epk}epk and r\mathsf{r}r:

C(x:{addr,epk,pkOIDC},w:{jwt,salt,r}):H(jwt.iss∥jwt.sub∥jwt.aud∥salt)=?addr∧JWT.Verify(pkOIDC,jwt)=?1∧jwt.nonce=?H(epk∥r)C(x: \{\mathsf{addr, epk, pk_{OIDC}} \}, w: \{ \mathsf{jwt, salt, \textcolor{red}{r}} \}): \\ H(\mathsf{jwt.iss \| jwt.sub \| jwt.aud \| salt}) \stackrel{?}= \mathsf{addr} \land \mathsf{JWT.Verify}(\mathsf{pk_{OIDC}, jwt}) \stackrel{?}=1 \land \mathsf{jwt.nonce} \stackrel{?}= \textcolor{red}{H(\mathsf{epk \| r})}C(x:{addr,epk,pkOIDC​},w:{jwt,salt,r}):H(jwt.iss∥jwt.sub∥jwt.aud∥salt)=?addr∧JWT.Verify(pkOIDC​,jwt)=?1∧jwt.nonce=?H(epk∥r)
exp=block.cur+60\mathsf{exp} = \mathsf{block.cur} + 60exp=block.cur+60
nonce=H(epk∥exp∥r)\mathsf{nonce} =H(\mathsf{epk \| exp \| r})nonce=H(epk∥exp∥r)
block.cur<exp\mathsf{block.cur} < \mathsf{exp}block.cur<exp
C(x:{addr,epk,exp,pkOIDC},w:{jwt,salt,r}):H(jwt.iss∥jwt.sub∥jwt.aud∥salt)=?addr∧JWT.Verify(pkOIDC,jwt)=?1∧jwt.nonce=?H(epk∥exp∥r)C(x: \{\mathsf{addr, epk, \textcolor{red}{exp}, pk_{OIDC}} \}, w: \{ \mathsf{jwt, salt, r} \}): \\ H(\mathsf{jwt.iss \| jwt.sub \| jwt.aud \| salt}) \stackrel{?}= \mathsf{addr} \land \mathsf{JWT.Verify}(\mathsf{pk_{OIDC}, jwt}) \stackrel{?}=1 \land \mathsf{jwt.nonce} \stackrel{?}= H(\mathsf{epk \| \textcolor{red}{exp} \| r})C(x:{addr,epk,exp,pkOIDC​},w:{jwt,salt,r}):H(jwt.iss∥jwt.sub∥jwt.aud∥salt)=?addr∧JWT.Verify(pkOIDC​,jwt)=?1∧jwt.nonce=?H(epk∥exp∥r)

Since pkOIDC\mathsf{pk_{OIDC}}pkOIDC​ (OIDC Provider's public key) changes periodically, we add iss to the public input. The validator must then verify that pkOIDC\mathsf{pk_{OIDC}}pkOIDC​ indeed corresponds to the public key of iss.

C(x:{addr,epk,exp,iss,pkOIDC},w:{jwt,salt,r}):H(jwt.iss∥jwt.sub∥jwt.aud∥salt)=?addr∧JWT.Verify(pkOIDC,jwt)=?1∧jwt.nonce=?H(epk∥exp∥r)∧jwt.iss=?issC(x: \{\mathsf{addr, epk, exp, \textcolor{red}{iss}, pk_{OIDC}} \}, w: \{ \mathsf{jwt, salt, r} \}): \\ H(\mathsf{jwt.iss \| jwt.sub \| jwt.aud \| salt}) \stackrel{?}= \mathsf{addr} \land \mathsf{JWT.Verify}(\mathsf{pk_{OIDC}, jwt}) \stackrel{?}=1 \land \mathsf{jwt.nonce} \stackrel{?}= H(\mathsf{epk \| exp \| r}) \land \textcolor{red}{\mathsf{jwt.iss \stackrel{?}= iss}}C(x:{addr,epk,exp,iss,pkOIDC​},w:{jwt,salt,r}):H(jwt.iss∥jwt.sub∥jwt.aud∥salt)=?addr∧JWT.Verify(pkOIDC​,jwt)=?1∧jwt.nonce=?H(epk∥exp∥r)∧jwt.iss=?iss
isValidPublicKey(iss,pkOIDC,block.cur)=?1\mathsf{isValidPublicKey(iss, \mathsf{pk_{OIDC}}, block.cur)} \stackrel{?}= 1isValidPublicKey(iss,pkOIDC​,block.cur)=?1

The following figure, extracted from the , provides an overview of the entire login process:

As mentioned in of the paper, unfortunately, according to the , the nonce field is not mandatory.

To answer this, let's recall the role of nonce. It was used to bind epk,exp\mathsf{epk, exp}epk,exp to the JWT. Thus, by modifying the circuit derived in Method 7, we can adapt zkLogin as follows:

C(x:{addr,epk,exp,iss,h,pkOIDC},w:{jwt,salt}):H(jwt.iss∥jwt.sub∥jwt.aud∥salt)=?addr∧JWT.Verify(pkOIDC,jwt)=?1∧h=?H(epk∥exp∥jwt)∧jwt.iss=?issC(x: \{\mathsf{addr, epk, exp, iss, \textcolor{red}{h}, pk_{OIDC}} \}, w: \{ \mathsf{jwt, salt} \}): \\ H(\mathsf{jwt.iss \| jwt.sub \| jwt.aud \| salt}) \stackrel{?}= \mathsf{addr} \land \mathsf{JWT.Verify}(\mathsf{pk_{OIDC}, jwt}) \stackrel{?}=1 \land \textcolor{red}{\mathsf{h}} \stackrel{?}= H(\mathsf{epk \| exp \| \textcolor{red}{jwt}}) \land \mathsf{jwt.iss \stackrel{?}= iss}C(x:{addr,epk,exp,iss,h,pkOIDC​},w:{jwt,salt}):H(jwt.iss∥jwt.sub∥jwt.aud∥salt)=?addr∧JWT.Verify(pkOIDC​,jwt)=?1∧h=?H(epk∥exp∥jwt)∧jwt.iss=?iss

Why Is r\mathsf{r}r No Longer Needed?

Previously, we introduced randomness r\mathsf{r}r to prevent the link between sub and epk\mathsf{epk}epk through nonce. However, in this case, since nonce does not exist, there is no longer a need for r\mathsf{r}r.

In previous methods, even if an attacker obtained a valid jwt\mathsf{jwt}jwt, they could not forge a transaction signature.

However, in this case, since we do not commit epk\mathsf{epk}epk via nonce an attacker who obtains a valid jwt\mathsf{jwt}jwt could use it to generate a different ephemeral key pair (esk,epk)(\mathsf{esk}, \mathsf{epk})(esk,epk) and sign arbitrary transactions.

This introduces a new security vulnerability, which must be addressed before deploying this approach. This could be mitigated to an extent through client-side proof generation, but still poses the same risk if the client themselves leak their valid own jwt\mathsf{jwt}jwt. Thus, since there is no current true solution to this vulnerability, transaction signatures with nonce-less JWT are not in use commercially today.

RSA.Sign(skOIDC,SHA2(jwt))→σOIDC\mathsf{RSA.Sign}(\mathsf{sk_{OIDC}, SHA2(jwt)}) \rightarrow \sigma_{\mathsf{OIDC}}RSA.Sign(skOIDC​,SHA2(jwt))→σOIDC​

Thus, JWT.Verify\mathsf{JWT.Verify}JWT.Verify must be implemented as follows:

RSA.Verify(pkOIDC,jwt.σOIDC,SHA2(jwt))→b∈{0,1}\mathsf{RSA.Verify(pk_{OIDC}, jwt.}{\sigma_{\mathsf{OIDC}}}\mathsf{,SHA2(jwt)}) \rightarrow b \in \{0, 1\}RSA.Verify(pkOIDC​,jwt.σOIDC​,SHA2(jwt))→b∈{0,1}

However, since jwt=Base64.Encode(claim)\mathsf{jwt} = \mathsf{Base64.Encode(claim)}jwt=Base64.Encode(claim), accessing a specific value like jwt.x\mathsf{jwt.x}jwt.x actually requires Base64.Decode(jwt).x\mathsf{Base64.Decode(jwt).x}Base64.Decode(jwt).x.

As a result, the ZK circuit must represent RSA signatures, SHA-2 hashing, and Base64 decoding—all of which are not ZK-friendly. This was implemented using , and it introduces the following computational overhead (Reference: ):

RSA signature verification – 15% (Utilizes a trick from )

Currently, the Sui zkLogin codebase is private. However, the implementation of Aptos Keyless Wallet is publicly available at 🔗 .

The function SelectivePayloadParsing(S,i,ℓ,j)→(k,v)\mathsf{SelectivePayloadParsing}(S, i, \ell, j) \rightarrow (k, v)SelectivePayloadParsing(S,i,ℓ,j)→(k,v) operates as follows. Assume SSS represents the above JSON structure. For simplicity, whitespace handling and boolean values are omitted from the explanation.

Extract the substring S′:=S[i:i+ℓ]S' := S[i: i +\ell]S′:=S[i:i+ℓ].

Example: If i=1i = 1i=1 and ℓ=12\ell = 12ℓ=12, then S′S'S′ becomes "sub":"123",.

Ensure S′S'S′ ends with either , or }:

S′[−1]=?Base64.Encode(,)∨S′[−1]=?Base64.Encode(})S'[- 1] \stackrel{?}=\mathsf{Base64.Encode(,)} \lor S'[- 1] \stackrel{?}=\mathsf{Base64.Encode(\})}S′[−1]=?Base64.Encode(,)∨S′[−1]=?Base64.Encode(})

Verify that S′[j]S'[j]S′[j] is ::

S′[j]=?Base64.Encode(:)S'[j] \stackrel{?}= \mathsf{Base64.Encode(:)}S′[j]=?Base64.Encode(:)

Using column index jjj, extract:

Key: k:=S′[0:j]k := S'[0:j]k:=S′[0:j]

Value: v:=S′[j+1:−1]v:=S'[j + 1:-1]v:=S′[j+1:−1]

Ensure both kkk and vvv start and end with ":

k[0]=?Base64.Encode(")∧k[−1]=?Base64.Encode(")∧v[0]=?Base64.Encode(")∧v[−1]=?Base64.Encode(")k[0] \stackrel{?}= \mathsf{Base64.Encode(")} \land k[-1] \stackrel{?}= \mathsf{Base64.Encode(")} \land v[0] \stackrel{?}= \mathsf{Base64.Encode(")} \land v[-1] \stackrel{?}= \mathsf{Base64.Encode(")}k[0]=?Base64.Encode(")∧k[−1]=?Base64.Encode(")∧v[0]=?Base64.Encode(")∧v[−1]=?Base64.Encode(")

For a string SSS of length nnn, where 0≤t<n0 \le t < n0≤t<n, the value S[t]S[t]S[t] can be computed as follows:

S[t]=St=S⋅OtS[t] = S_t = S\cdot O_tS[t]=St​=S⋅Ot​

For 0≤t<n0 \le t < n0≤t<n, the indexing operator OtO_tOt​ is defined as:

Ot(x)={1 if x=t0otherwiseO_t(x) = \begin{cases} 1 & \text{ if } x = t \\ 0 & \text{otherwise} \end{cases}Ot​(x)={10​ if x=totherwise​

For example, to check whether S′S'S′ ends with ,, we transform it into:

S′[−1]=S′[ℓ−1]=S[i+ℓ−1]=Si+ℓ−1=?Base64.Encode(,)S'[-1] = S'[\ell - 1] = S[i + \ell -1] = S_{i + \ell - 1} \stackrel{?}=\mathsf{Base64.Encode(,)}S′[−1]=S′[ℓ−1]=S[i+ℓ−1]=Si+ℓ−1​=?Base64.Encode(,)

Similarly, extracting S[i:i+â„“]S[i: i + \ell]S[i:i+â„“] follows:

S[i:i+ℓ]={St}t∈{i,…,i+ℓ−1}S[i: i + \ell] = \{S_t\}_{t \in \{i, \dots, i+\ell-1\}}S[i:i+ℓ]={St​}t∈{i,…,i+ℓ−1}​

Checking whether kkk starts with " can be rewritten as:

k[0]=S[i]=Si=?Base64.Encode(")k[0] = S[i] = S_i \stackrel{?}= \mathsf{Base64.Encode(")}k[0]=S[i]=Si​=?Base64.Encode(")

Since computing S[t]S[t]S[t] requires nnn multiplications, extracting S[i:i+m]S[i:i+m]S[i:i+m] requires n×mn \times mn×m constraints. However, we can significantly reduce constraints by leveraging field packing:

Even after adding boundary checks and unpacking logic, the constraint count drops from n×mn \times mn×m to 18m+n×m3218m + \frac{n \times m}{32}18m+32n×m​, resulting in a major efficiency gain. (The paper doesn't explain how.)

In addition to verifying transaction signatures using , as commonly used in Sui and Aptos, validators must now also verify ZK proofs.

Although a proof is generated only once, it is verified multiple times by validators. Given that ZK verification must not be significantly slower than traditional EdDSA signature verification, was chosen as the proving scheme. Groth16 requires only three pairing operations for verification, making it computationally efficient. (There are additional computations using public inputs, but these are omitted for simplicity.)

One advantage of Groth16 is that it allows . However, if at least one proof is invalid, all nnn proofs must be re-verified individually, leading to a worst-case scenario that could be exploited as a DDoS attack vector.

"This drastically reduces the time between when a user logs in with their keyless account to when that user is able to transact, from ~25 seconds to 3 seconds. In turn, this greatly improves the user experience of keyless accounts." —

According to AIP-75, generating a proof using on the client side takes approximately 25 seconds, which is unacceptable from a UX perspective. To address this, both Sui and Aptos have adopted a prover service to generate proofs on the server side.

Privacy Issue: The prover service gains access to w:{jwt,salt,r}w: \{\mathsf{jwt, salt, r}\}w:{jwt,salt,r}, meaning it can infer the relationship between sub and addr\mathsf{addr}addr (similar to the issue in Method 1). Fortunately, the prover does not know esk\mathsf{esk}esk, preventing transaction signature forgery.

The image below, taken from the , shows that Sui has a server-side proof generation time of around 3 seconds, which is similar to Aptos's proof generation time mentioned above:

Written by from

zkLogin from Sui
Aptos' Keyless Wallet
OAuth2
OIDC
https://www.jstoolset.com/jwt
AIP-61
the method presented by Alin Tomescu, Head of Cryptography at Aptos, at ZKSummit12
Appendix E
zkLogin paper
zkLogin paper
Section 4.5
OIDC specification 3.1.2.1
circom
https://youtu.be/IyTQ2FfglFE?t=1473
the xJsnark paper
Aptos Keyless ZK Proofs
EdDSA
Groth16
AIP-75
zkLogin paper
https://www.youtube.com/watch?v=IyTQ2FfglFE
https://www.youtube.com/watch?v=sKqeGR4BoI0
https://arxiv.org/abs/2401.11735
https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-61.md#the-keyless-zk-relation-mathcalr
Method 7
AIP-81
Section 4.3.1
zkLogin paper
here
A41
batch verification of nnn proofs simultaneously
ryan Kim