Skip to main content

zkLogin

zkLogin is a Sui primitive that provides the ability for you to send transactions from a Sui address using an OAuth credential, without publicly linking the two.

zkLogin is designed with the following goals in mind:

  • Streamlined onboarding: zkLogin enables you to transact on Sui using the familiar OAuth login flow, eliminating the friction of handling cryptographic keys or remembering mnemonics.
  • Self-custody: A zkLogin transaction requires user approval via the standard OAuth login process--the OAuth provider cannot transact on the user's behalf.
  • Security: zkLogin is a two-factor authentication scheme: sending a transaction requires both a credential from a recent OAuth login and a salt not managed by the OAuth provider. An attacker who compromises an OAuth account cannot transact from the user's corresponding Sui address unless they separately compromise the salt.
  • Privacy: Zero-knowledge proofs prevent third parties from linking a Sui address with its corresponding OAuth identifier.
  • Optional verified identity: A user can opt in to verify the OAuth identifier that was used to derive a particular Sui address. This serves as the foundation for a verifiable on-chain identity layer.
  • Accessibility: zkLogin is one of several native Sui signature schemes thanks to Sui's cryptography agility. It integrates with other Sui primitives, like sponsored transactions and multisig.
  • Rigorousness: The code for zkLogin has been independently audited by two firms specializing in zero knowledge. The public zkLogin ceremony for creating the common reference string attracted contributions from more than 100 participants.

Are you a builder who wants to integrate zkLogin into your application or wallet? Dive into our Integration guide.

If you want to understand how zkLogin works, including how the zero-knowledge proof is generated, and how Sui verifies a zkLogin transaction, see this section.

If you are curious about the security model and the privacy considerations of zkLogin, visit this page.

More questions? See the FAQ section.

OpenID providers

The following table lists the OpenID providers that can support zkLogin or are currently being reviewed to determine whether they can support zkLogin.

ProviderCan support?DevnetTestnetMainnet
FacebookYesYesYesYes
GoogleYesYesYesYes
TwitchYesYesYesYes
SlackYesYesNoNo
KakaoYesYesNoNo
AppleYesYesNoNo
RedBullUnder reviewNoNoNo
MicrosoftUnder reviewNoNoNo
AWS (Tenant)Under reviewNoNoNo
AmazonUnder reviewNoNoNo
WeChatUnder reviewNoNoNo
Auth0Under reviewNoNoNo
OktaUnder reviewNoNoNo

Integration guide

Here is the high-level flow the wallet or frontend application must implement to support zkLogin-enabled transactions:

  1. The wallet creates an ephemeral KeyPair.
  2. The wallet prompts the user to complete an OAuth login flow with the nonce corresponding to the ephemeral public key.
  3. After receiving the JWT token, the wallet obtains a zero-knowledge proof.
  4. The wallet obtains a unique user salt based on a JWT token. The OAuth subject identifier and salt can be used to compute the zkLogin Sui address.
  5. The wallet signs transactions with the ephemeral private key.
  6. The wallet submits the transaction with the ephemeral signature and the zero-knowledge proof.

Let's dive into the specific implementation details.

Install the zkLogin TypeScript SDK

To use the zkLogin TypeScript SDK in your project, run the following command in your project root:

npm install @mysten/zklogin

# If you want to use the latest experimental version:
npm install @mysten/zklogin@experimental

Configure a developer account with OpenID provider

See latest table for all enabled providers. More OpenID-compatible providers will be enabled in the future.

For example, the following TypeScript code can be used to construct a login URL for testing.

const REDIRECT_URI = '<YOUR_SITE_URL>';

const params = new URLSearchParams({
// See below for how to configure client ID and redirect URL
client_id: $CLIENT_ID,
redirect_uri: $REDIRECT_URL,
response_type: 'id_token',
scope: 'openid',
// See below for details about generation of the nonce
nonce: nonce,
});

const loginURL = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;

You must configure the client ID ($CLIENT_ID) and redirect URL ($REDIRECT_URL) with each provider as follows:

Google

  1. Register for a Google Cloud account and access the dashboard.
  2. Select "APIs & Services" then "Credentials", where you can find the client ID. Set the redirect URL. This should be the wallet or application front end.

1

Sign up for Google developer account

2

Go to Credentials

3

Configure a Redirect URL

Facebook

  1. Register for a Facebook developer account and access the dashboard.

  2. Select "Build your app" then "Products" then "Facebook Login" where you can find the client ID. Set the redirect URL. This should be the wallet or application frontend.

1

Sign up for Facebook developer account

2

Go to Settings

Twitch

  1. Register for a Twitch developer account. Access the dashboard.

  2. Go to "Register Your Application" then "Application" where you can find the client ID. Set the redirect URL. This should be the wallet or application frontend.

1

Sign up for Twitch developer account

2

Go to Console

Kakao

  1. Register for a Kakao developer account. Access the dashboard and add an application.

1

Add applications to Kakao

  1. Go to "App Keys" where you can find the corresponding client ID for different platforms.
  • Native app key: Used to call APIs through the Android or iOS SDK.
  • JavaScript key: Used to call APIs through the JavaScript SDK.
  • REST API key: Used to call APIs through the REST API.

2

Find client ID

  1. Toggle on "Kakao Login Activation" and "OpenID Connect Activation". Set the redirect URL in "Kakao Login" under "Product Settings". This should be the wallet or application frontend.

1

Set redirect URL

Slack

  1. Register for a Slack developer account. Access the dashboard and go to "Create New App" then choose "From scratch".

    1

    Create app in Slack

  2. Find the Client ID and Client Secret under "App Credentials".

    1

    Find Client ID and Client Secret

  3. Set Redirect URL in "OAuth & Permissions" under "Features". This should be the wallet or application frontend.

    1

    Set Redirect URL

Apple

  1. Register for an Apple developer account. Go to the Certificates, Identifiers and Profiles section.

    1

    This is where you can create Certificates, Identifiers and Profiles

  2. Create an App ID

    • From the sidebar, select Identifiers and click the blue plus icon to create a new one.
    • Choose App IDs as the identifier type and click Continue.
    • In the next screen, enter a descriptive name for your App ID and a unique Bundle ID in reverse-dns format (for example, com.example.app).
    • Scroll down to the list of capabilities and enable Sign In with Apple by checking the box next to it.

    1

    This is how you can enable Sign In with Apple for your App ID

  3. Create a Services ID

    A Services ID identifies a specific instance of your app and is used as the OAuth client_id. You need a Services ID if you want to use Sign In with Apple for your web app.

    1

    This is where you Create a Services ID

  4. Create a new identifier and select Services IDs as the identifier type.

    • In the next step, enter a name for your app that will be displayed to the users during the sign-in process and a unique identifier that will be used as the OAuth client_id. Make sure to enable Sign In with Apple by checking the box next to it.
    • Click the Configure button next to Sign In with Apple to set up the domain and redirect URLs for your app. You need to specify the domain name where your app is hosted and the redirect URL that will handle the OAuth response from Apple.

    Select the App ID that you created in the previous step as the Primary App ID. This will associate your Services ID with your App ID.

    Enter the domain name of your app (for example, example-app.com) and the redirect URL that will receive the authorization code from Apple (for example, https://example-app.com/redirect). Note that Apple does not allow localhost or IP addresses as valid domains or redirect URLs.

    Click Save and then Continue and Register until you complete this step.

    You have now created an App ID and a Services ID for your app. The identifier of your Services ID is your OAuth client_id. In my example, that is com.example.client.

    1

    This is where you Set Redirect URL

Get JWT Token

  1. Generate an ephemeral KeyPair. Follow the same process as you would generating a KeyPair in a traditional wallet. See Sui SDK for details.

  2. Set the expiration time for the ephemeral KeyPair. The wallet decides whether the maximum epoch is the current epoch or later. The wallet also determines whether this is adjustable by the user.

  3. Assemble the OAuth URL with configured client ID, redirect URL, ephemeral public key and nonce: This is what the application sends the user to complete the login flow with a computed nonce.

import { generateNonce, generateRandomness } from '@mysten/zklogin';

const FULLNODE_URL = 'https://fullnode.devnet.sui.io'; // replace with the RPC URL you want to use
const suiClient = new SuiClient({ url: FULLNODE_URL });
const { epoch, epochDurationMs, epochStartTimestampMs } = await suiClient.getLatestSuiSystemState();

const maxEpoch = Number(epoch) + 2; // this means the ephemeral key will be active for 2 epochs from now.
const ephemeralKeyPair = new Ed25519Keypair();
const randomness = generateRandomness();
const nonce = generateNonce(ephemeralKeyPair.getPublicKey(), maxEpoch, randomness);

The auth flow URL can be constructed with $CLIENT_ID, $REDIRECT_URL and $NONCE.

For some providers ("Yes" for "Auth Flow Only"), the JWT token can be found immediately in the redirect URL after the auth flow.

For other providers ("No" for "Auth Flow Only"), the auth flow only returns a code ($AUTH_CODE) in redirect URL. To retrieve the JWT token, an additional POST call is required with "Token Exchange URL".

ProviderAuth Flow URLToken Exchange URLAuth Flow Only
Googlehttps://accounts.google.com/o/oauth2/v2/auth?client_id=$CLIENT_ID&response_type=id_token&redirect_uri=$REDIRECT_URL&scope=openid&nonce=$NONCEN/AYes
Facebookhttps://www.facebook.com/v17.0/dialog/oauth?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&scope=openid&nonce=$NONCE&response_type=id_tokenN/AYes
Twitchhttps://id.twitch.tv/oauth2/authorize?client_id=$CLIENT_ID&force_verify=true&lang=en&login_type=login&redirect_uri=$REDIRECT_URL& response_type=id_token&scope=openid&nonce=$NONCEN/AYes
Kakaohttps://kauth.kakao.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&nonce=$NONCEhttps://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&code=$AUTH_CODENo
Applehttps://appleid.apple.com/auth/authorize?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&scope=email&response_mode=form_post&response_type=code%20id_token&nonce=$NONCEN/AYes
Slackhttps://slack.com/openid/connect/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&nonce=$NONCE&scope=openidhttps://slack.com/api/openid.connect.token?code=$AUTH_CODE&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRETNo

Decoding JWT

Upon successful redirection the ID Provider will attach the JWT token as a URL parameter (Using the Google Flow as an example)

http://host/auth?id_token=tokenPartA.tokenPartB.tokenPartC&authuser=0&prompt=none

The id_token param is the JWT token in encoded format. You can validate the correctness of the encoded token and investigate its structure by pasting it in the jwt.io website.

To decode the JWT you can use a library like: jwt_decode: and map the response to the provided type JwtPayload:


const decodedJwt = jwt_decode(encodedJWT) as JwtPayload;

export interface JwtPayload {
iss?: string;
sub?: string; //Subject ID
aud?: string[] | string;
exp?: number;
nbf?: number;
iat?: number;
jti?: string;
}

User Salt Management

User salt is used when computing the zkLogin Sui address (see definition). The salt is required to be a 16-bytes value or a integer smaller than 2n**128n. There are several options for the application to maintain the user salt:

  1. Client Side:
    • Option 1: Request user input for the salt during wallet access, transferring the responsibility to the user, who must then remember it.
    • Option 2: Browser or Mobile Storage: Ensure proper workflows to prevent users from losing wallet access during device or browser changes. One approach is to email the salt during new wallet setup.
  2. Backend service that exposes an endpoint that returns a unique salt for each user consistently.
    • Option 3: Store a mapping from user identifier (e.g. sub) to user salt in a conventional database (e.g. user or password table). The salt is unique per user.
    • Option 4: Implement a service that keeps a master seed value, and derive a user salt with key derivation by validating and parsing the JWT token. For example, use HKDF(ikm = seed, salt = iss || aud, info = sub) defined here. Note that this option does not allow any rotation on master seed or change in client ID (i.e. aud), otherwise a different user address will be derived and will result in loss of funds.

Here's an example request and response for the Mysten Labs-maintained salt server (using option 4). If you wish to use the Mysten ran salt server, please contact us for whitelisting your registered client ID. Only valid JWT token authenticated with whitelisted client IDs are accepted.

curl -X POST https://salt.api.mystenlabs.com/get_salt -H 'Content-Type: application/json' -d '{"token": "$JWT_TOKEN"}'

Response: {"salt":"129390038577185583942388216820280642146"}

User salt is used to disconnect the OAuth identifier (sub) from the on-chain Sui address to avoid linking Web2 credentials with Web3 credentials. While losing or misusing the salt could enable this link, it wouldn't compromise fund control or zkLogin asset authority. See more discussion here.

Get the User's Sui Address

Once the OAuth flow completes, the JWT token can be found in the redirect URL. Along with the user salt, the zkLogin address can be derived as follows:

import { jwtToAddress } from '@mysten/zklogin';

const zkLoginUserAddress = jwtToAddress(jwt, userSalt);

Get the Zero-Knowledge Proof

The next step is to fetch the ZK proof. This is an attestation (proof) over the ephemeral key pair that proves the ephemeral key pair is valid.

First, generate the extended ephemeral public key to use as an input to the ZKP.

import { getExtendedEphemeralPublicKey } from '@mysten/zklogin';

const extendedEphemeralPublicKey = getExtendedEphemeralPublicKey(ephemeralKeyPair.getPublicKey());

You need to fetch a new ZK proof if the previous ephemeral key pair is expired or is otherwise inaccessible.

Because generating a ZK proof can be resource-intensive and potentially slow on the client side, it's advised that wallets utilize a backend service endpoint dedicated to ZK proof generation.

There are two options:

  1. Call the Mysten Labs-maintained proving service
  2. Run the proving service in your backend using the provided Docker images.

Call the Mysten Labs-maintained proving service

If you wish to use the Mysten ran ZK Proving Service for Mainnet, please contact us for whitelisting your registered client ID. Only valid JWT token authenticated with whitelisted client IDs are accepted.

To use prover-dev endpoint, you do not need to whitelist client IDs. Note that the proof generated with the prover-dev endpoint can only be submitted for Devnet zkLogin transactions, submitting it to Testnet or Mainnet fails.

NetworkProver URL
Mainnet, Testnethttps://prover.mystenlabs.com/v1
Devnethttps://prover-dev.mystenlabs.com/v1

You can use BigInt or Base64 encoding for extendedEphemeralPublicKey, jwtRandomness, and salt. The following examples show two sample requests with the first using BigInt encoding and the second using Base64.

curl -X POST $PROVER_URL -H 'Content-Type: application/json' \
-d '{"jwt":"$JWT_TOKEN", \
"extendedEphemeralPublicKey":"84029355920633174015103288781128426107680789454168570548782290541079926444544", \
"maxEpoch":"10", \
"jwtRandomness":"100681567828351849884072155819400689117", \
"salt":"248191903847969014646285995941615069143", \
"keyClaimName":"sub" \
}'

curl -X POST $PROVER_URL -H 'Content-Type: application/json' \
-d '{"jwt":"$JWT_TOKEN", \
"extendedEphemeralPublicKey":"ucbuFjDvPnERRKZI2wa7sihPcnTPvuU//O5QPMGkkgA=", \
"maxEpoch":"10", \
"jwtRandomness":"S76Qi8c/SZlmmotnFMr13Q==", \
"salt":"urgFnwIxJ++Ooswtf0Nn1w==", \
"keyClaimName":"sub" \
}'

Response:

{
"proofPoints":{
"a":["17267520948013237176538401967633949796808964318007586959472021003187557716854",
"14650660244262428784196747165683760208919070184766586754097510948934669736103",
"1"],
"b":[["21139310988334827550539224708307701217878230950292201561482099688321320348443",
"10547097602625638823059992458926868829066244356588080322181801706465994418281"],
["12744153306027049365027606189549081708414309055722206371798414155740784907883",
"17883388059920040098415197241200663975335711492591606641576557652282627716838"],
["1","0"]],

"c":["14769767061575837119226231519343805418804298487906870764117230269550212315249",
"19108054814174425469923382354535700312637807408963428646825944966509611405530","1"]
},
"issBase64Details":{"value":"wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw", "indexMod4": 2 },
"headerBase64":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ"
}

How to handle CORS error

To avoid possible CORS errors in Frontend apps, it is suggested to delegate this call to a backend service.

The response can be mapped to the inputs parameter type of getZkLoginSignature of zkLogin SDK.

const proofResponse = await post('/your-internal-api/zkp/get', zkpRequestPayload);

export type PartialZkLoginSignature = Omit<
Parameters<typeof getZkLoginSignature>['0']['inputs'],
'addressSeed'
>;
const partialZkLoginSignature = proofResponse as PartialZkLoginSignature;

Run the proving service in your backend

  1. Download the Groth16 proving key zkey file that will be later used as an argument to run the prover. There are zkeys available for Mainnet and Testnet, as well as a test zkey for Devnet. See the Ceremony section for more details on how the main proving key is generated. Please install git lfs which is needed before downloading the zkey.

    • Main zkey (for Mainnet and Testnet)

      wget -O - https://raw.githubusercontent.com/sui-foundation/zklogin-ceremony-contributions/main/download-main-zkey.sh | bash
    • Test zkey (for Devnet)

      wget -O - https://raw.githubusercontent.com/sui-foundation/zklogin-ceremony-contributions/main/download-test-zkey.sh | bash
    • To verify the download contains the correct zkey file, you can run the following command to check the Blake2b hash: b2sum ${file_name}.zkey.

      Networkzkey file nameHash
      Mainnet, TestnetzkLogin-main.zkey060beb961802568ac9ac7f14de0fbcd55e373e8f5ec7cc32189e26fb65700aa4e36f5604f868022c765e634d14ea1cd58bd4d79cef8f3cf9693510696bcbcbce
      DevnetzkLogin-test.zkey686e2f5fd969897b1c034d7654799ee2c3952489814e4eaaf3d7e1bb539841047ae8ee5fdcdaca5f4ddd76abb5a8e8eb77b44b693a2ba9d4be57e94292b26ce2
  2. For the next step, you need two Docker images from the mysten/zklogin repository (tagged as prover and prover-fe). To simplify, a docker compose file is available that automates this process. Run docker compose with the downloaded zkey from the same directory as the YAML file.

services:
backend:
image: mysten/zklogin:prover-stable
volumes:
# The ZKEY environment variable must be set to the path of the zkey file.
- ${ZKEY}:/app/binaries/zkLogin.zkey
environment:
- ZKEY=/app/binaries/zkLogin.zkey
- WITNESS_BINARIES=/app/binaries

frontend:
image: mysten/zklogin:prover-fe-stable
command: "8080"
ports:
# The PROVER_PORT environment variable must be set to the desired port.
- "${PROVER_PORT}:8080"
environment:
- PROVER_URI=http://backend:8080/input
- NODE_ENV=production
- DEBUG=zkLogin:info,jwks
# The default timeout is 15 seconds. Uncomment the following line to change it.
# - PROVER_TIMEOUT=30
ZKEY=<path_to_zkLogin.zkey> PROVER_PORT=<PROVER_PORT> docker compose up
  1. To call the service, the following two endpoints are supported:
    • /ping: To test if the service is up. Running curl http://localhost:PROVER_PORT/ping should return pong.
    • /v1: The request and response are the same as the Mysten Labs maintained service.

A few important things to note:

  • The backend service (mysten/zklogin:prover-stable) is compute-heavy. Use at least the minimum recommended 16 cores and 16GB RAM. Using weaker instances can lead to timeout errors with the message "Call to rapidsnark service took longer than 15s". You can adjust the environment variable PROVER_TIMEOUT to set a different timeout value, for example, PROVER_TIMEOUT=30 for a timeout of 30 seconds.

  • If you want to compile the prover from scratch (for performance reasons), please see our fork of rapidsnark. You'd need to compile and launch the prover in server mode.

  • Setting DEBUG=* turns on all logs in the prover-fe service some of which may contain PII. Consider using DEBUG=zkLogin:info,jwks in production environments.

Assemble the zkLogin signature and submit the transaction

First, sign the transaction bytes with the ephemeral private key using the key pair generated previously. This is the same as traditional KeyPair signing. Make sure that the transaction sender is also defined.

 const ephemeralKeyPair = new Ed25519Keypair();

const client = new SuiClient({ url: "<YOUR_RPC_URL>" });

const txb = new TransactionBlock();

txb.setSender(zkLoginUserAddress);

const { bytes, signature: userSignature } = await txb.sign({
client,
signer: ephemeralKeyPair, // This must be the same ephemeral key pair used in the ZKP request
});

Next, generate an address seed by combining userSalt, sub (subject ID), and aud (audience).

Set the address seed and the partial zkLogin signature to be the inputs parameter.

You can now serialize the zkLogin signature by combining the ZK proof (inputs), the maxEpoch, and the ephemeral signature (userSignature).

import { genAddressSeed, getZkLoginSignature } from "@mysten/zklogin";

const addressSeed : string = genAddressSeed(BigInt(userSalt!), "sub", decodedJwt.sub, decodedJwt.aud).toString();

const zkLoginSignature : SerializedSignature = getZkLoginSignature({
inputs: {
...partialZkLoginSignature,
addressSeed
},
maxEpoch,
userSignature,
});

Finally, execute the transaction.

client.executeTransactionBlock({
transactionBlock: bytes,
signature: zkLoginSignature,
});

Caching the ephemeral private key and ZK proof

As previously documented, each ZK proof is tied to an ephemeral key pair. So you can reuse the proof to sign any number of transactions until the ephemeral key pair expires (until the current epoch crosses maxEpoch).

You might want to cache the ephemeral key pair along with the ZKP for future uses.

However, the ephemeral key pair needs to be treated as a secret akin to a key pair in a traditional wallet. This is because if both the ephemeral private key and ZK proof are revealed to an attacker, then they can typically sign any transaction on behalf of the user (using the same process described previously).

Consequently, you should not store them persistently in an unsecure storage location, on any platform. For example, on browsers, use session storage instead of local storage to store the ephemeral key pair and the ZK proof. This is because session storage automatically clears its data when the browser session ends, while data in local storage persists indefinitely.

How zkLogin Works

In rough sketches, the zkLogin protocol relies on the following:

  1. A JWT token is a signed payload from OAuth providers, including a user-defined field named nonce. zkLogin leverages the OpenID Connect OAuth flow by defining the nonce as a public key and an expiry epoch.
  2. The wallet stores an ephemeral KeyPair, where the ephemeral public key is defined in the nonce. The ephemeral private key signs transactions for a brief session, eliminating the need for user memorization. The Groth16 zero-knowledge proof is generated based on the JWT token, concealing privacy-sensitive fields.
  3. A transaction is submitted on-chain with the ephemeral signature and the ZK proof. Sui authorities execute the transaction after verifying the ephemeral signature and the proof.
  4. Instead of deriving the Sui address based on a public key, the zkLogin address is derived from sub (that uniquely identifies the user per provider), iss (identifies the provider), aud (identifies the application) and user_salt (a value that unlinks the OAuth identifier with the on-chain address).

The complete zkLogin flow

1

(Step 0) We use Groth16 for our protocol's zkSNARK instantiation, requiring a singular generation of a structured common reference string (CRS) linked to the circuit. A ceremony is conducted to generate the CRS, which is used to produce the proving key in the ZK Proving Service, the verifying key in Sui Authority. See the Ceremony section for more details.

(Step 1-3) The user begins by logging into an OpenID Provider (OP) to obtain a JWT token containing a defined nonce. In particular, the user generates an ephemeral KeyPair (eph_sk, eph_pk) and embed eph_pk, along with expiry times (max_epoch) and randomness (jwt_randomness), into the nonce (see definition). After the user completes the OAuth login flow, an JWT token can be found in the redirect URL in the application.

(Step 4-5) The application frontend then sends the JWT token to a salt service. The salt service returns the unique user_salt based on iss, aud, sub upon validation of the JWT token.

(Step 6-7) The user sends the ZK proving service the JWT token, user salt, ephemeral public key, jwt randomness, key claim name (i.e. sub). The proving service generates a Zero-Knowledge Proof that takes these as private inputs and does the following: (a) Checks the nonce is derived correctly as defined (b) Checks that key claim value matches the corresponding field in the JWT, (c) Verifies the RSA signature from OP on the JWT, and (d) the address is consistent with the key claim value and user salt.

(Step 8): The application computes the user address based on iss, aud, sub, aud. This step can be done independently as long as the application has a valid JWT token.

(Step 9-10) A transaction is signed using the ephemeral private key to generate an ephemeral signature. Finally, the user submits the transaction along with the ephemeral signature, ZK proof and other inputs to Sui.

(After Step 10) After submitted on chain, Sui Authorities verify the ZK proof against the provider JWKs from storage (agreed upon in consensus) and also the ephemeral signature.

Entities

  1. Application frontend: This describes the wallet or frontend application that supports zkLogin. This frontend is responsible for storing the ephemeral private key, directing users to complete the OAuth login flow, creating and signing a zkLogin transaction.

  2. Salt Backup Service: This is a backend service responsible for returning a salt per unique user. See integration guide for other strategies to maintain salt.

  3. ZK Proving Service: This is a backend service responsible for generating ZK proofs based on JWT token, JWT randomness, user salt and max epoch. This proof is submitted on-chain along with the ephemeral signature for a zkLogin transaction.

Address definition

The address is computed on the following inputs:

  1. The address flag: zk_login_flag = 0x05 for zkLogin address. This serves as a domain separator as a signature scheme defined in crypto agility.

  2. kc_name_F = hashBytesToField(kc_name, maxKCNameLen): Name of the key claim, e.g., sub. The sequence of bytes is mapped to a field element in BN254 using hashBytesToField (defined below).

  3. kc_value_F = hashBytesToField(kc_value, maxKCValueLen): The value of the key claim mapped using hashBytesToField.

  4. aud_F = hashBytesToField(aud, maxAudValueLen): The Relying Party (RP) identifier. See definition.

  5. iss: The OpenID Provider (OP) identifier. See definition.

  6. user_salt: A value introduced to unlink the OAuth identifier with the on-chain address.

Finally, we derive zk_login_address = Blake2b_256(zk_login_flag, iss_L, iss, addr_seed) where addr_seed = Poseidon_BN254(kc_name_F, kc_value_F, aud_F, Poseidon_BN254(user_salt).

Terminology and notations

See below for all relevant OpenID terminology defined in spec and how they are used in zkLogin, along with definitions for protocol details.

OpenID provider (OP)

OAuth 2.0 authorization server that is capable of authenticating the end-user and providing claims to a relying party about the authentication event and the end-user. This is identified in the iss field in JWT token payload. Currently zkLogin supported OPs include Google, Facebook, and Twitch, and more compatible providers will be enabled in the future.

Relying party (RP) or client

OAuth 2.0 client application requiring end-user authentication and claims from an OpenID provider. This is assigned by OP when the developer creates the application. This is identified in the aud field in JWT token payload. This refers to any zkLogin enabled wallet or application.

Subject identifier (sub)

Locally unique and never reassigned identifier within the issuer for the end-user, which is intended to be consumed by the RP. Sui uses this as the key claim to derive user address.

JWK (JSON Web Key)

A JSON data structure that represents a set of public keys for an OP. A public endpoint (e.g. https://www.googleapis.com/oauth2/v3/certs) can be queried to retrieve the valid public keys corresponding to kid for the provider. Upon matching with the kid in the header of a JWT token, the JWT token can be verified against the payload and its corresponding JWK. In Sui, all authorities call the JWK endpoints independently, and update the latest view of JWKs for all supported providers during protocol upgrades. The correctness of JWKs is guaranteed by the quorum (2f+1) of validator stake.

JWT Token (JSON Web Token)

JWT token can be found in the redirect URL to RP after the user completes the OAuth login flow (i.e. https://redirect.com?id_token=$JWT_TOKEN). The JWT token contains a header, payload and a signature. The signature is an RSA signature that can be verified against jwt_message = header + . + payload and its JWK identified by kid. The payload contains a JSON of many claims that is a name, value pair. See below for the specific claims that are relevant to the zkLogin protocol.

Header

NameExample ValueUsage
algRS256zkLogin only supports RS256 (RSA + SHA-256).
kidc3afe7a9bda46bae6ef97e46c95cda48912e5979Identifies the JWK that should be used to verify the JWT.
typJWTzkLogin only supports JWT.

Payload

NameExample ValueUsage
isshttps://accounts.google.comA unique identifier assigned to the OAuth provider.
aud575519200000-msop9ep45u2uo98hapqmngv8d8000000.apps.googleusercontent.comA unique identifier assigned to the relying party by the OAuth provider.
noncehTPpgF7XAKbW37rEUS6pEVZqmoIA value set by the relying party. The zkLogin enabled wallet is required to set this to the hash of ephemeral public key, an expiry time and a randomness.
sub110463452167303000000A unique identifier assigned to the user.

For a zkLogin transaction, we do not use the iat and exp claims (i.e., timestamp). This is because we provide a different way for users to specify expiry times, namely via nonce.

Key Claim

We call the claim used to derive a users' address as the "key claim" e.g., sub or email. Naturally, we only want to use claims that are fixed once and never changed again. For zkLogin, we currently support sub as the key claim because OpenID spec mandates that providers do not change this identifier. In the future, this can be extended to use email, username, etc.

Notations

  1. (eph_sk, eph_pk): Ephemeral key pair refers to the private and public key pair used to produce ephemeral signatures. The signing mechanism is the same as traditional transaction signing, but it is ephemeral because it is only stored for a short session and can be refreshed upon new OAuth sessions. The ephemeral public key is used to compute nonce.
  2. nonce: An application-defined field embedded in the JWT token payload, computed as the hash of the ephemeral public key, JWT randomness, and the maximum epoch (Sui's defined expiry epoch). Specifically, a zkLogin compatible nonce is required to passed in as nonce = ToBase64URL(Poseidon_BN254([ext_eph_pk_bigint / 2^128, ext_eph_pk_bigint % 2^128, max_epoch, jwt_randomness]).to_bytes()[len - 20..]) where ext_eph_pk_bigint is the BigInt representation of ext_eph_pk.
  3. ext_eph_pk: The byte representation of an ephemeral public key (flag || eph_pk). Size varies depending on the choice of the signature scheme (denoted by the flag, defined in Signatures).
  4. user_salt: A value introduced to unlink the OAuth identifier with the on-chain address.
  5. max_epoch: The epoch at which the JWT token expires. This is u64 used in Sui.
  6. kc_name: The key claim name, e.g. sub.
  7. kc_value: The key claim value, e.g. 110463452167303000000.
  8. hashBytesToField(str, maxLen): Hashes the ASCII string to a field element using the Poseidon hash.

Ceremony

To preserve privacy of the OAuth artifacts, a zero-knowledge proof of possession of the artifacts is provided. zkLogin employs the Groth16 zkSNARK to instantiate the zero-knowledge proofs, as it is the most efficient general-purpose zkSNARK in terms of proof size and verification efficiency.

However, Groth16 needs a computation-specific Common Reference String (CRS) to be setup by a trusted party. With zkLogin expected to ensure the safe-keeping of high value transactions and the integrity of critical smart contracts, we cannot base the security of the system on the honesty of a single entity. Hence, to generate the CRS for the zkLogin circuit, it is vital to run a protocol which bases its security on the assumed honesty of a small fraction of a large number of parties.

What is the ceremony?

The Sui zkLogin ceremony is essentially a cryptographic multi-party computation (MPC) performed by a diverse group of participants to generate this CRS. We follow the MPC protocol MMORPG described by Bowe, Gabizon and Miers. The protocol roughly proceeds in 2 phases. The first phase results in a series of powers of a secret quantity tau in the exponent of an elliptic curve element. Since this phase is circuit-agnostic, we adopted the result of the existing community contributed perpetual powers of tau. Our ceremony was the second phase, which is specific to the zkLogin circuit.

The MMORPG protocol is a sequential protocol, which allows an indefinite number of parties to participate in sequence, without the need of any prior synchronization or ordering. Each party needs to download the output of the previous party, generate entropy of its own and then layer it on top of the received result, producing its own contribution, which is then relayed to the next party. The protocol guarantees security, if at least one of the participants follows the protocol faithfully, generates strong entropy and discards it reliably.

How was the ceremony performed?

We sent invitations to 100+ people with diverse backgrounds and affiliations: Sui validators, cryptographers, web3 experts, world-renowned academicians, and business leaders. We planned the ceremony to take place on the dates September 12-18, 2023, but allowed participants to join when they wanted with no fixed slots.

Since the MPC is sequential, each contributor needed to wait till the previous contributor finished in order to receive the previous contribution, follow the MPC steps and produce their own contribution. Due to this structure, we provisioned a queue where participants waited, while those who joined before them finished. To authenticate participants, we sent a unique activation code to each of them. The activation code was the secret key of a signing key pair, which had a dual purpose: it allowed the coordination server to associate the participant's email with the contribution, and to verify the contribution with the corresponding public key.

Participants had two options to contribute: through a browser or a docker. The browser option was more user-friendly for contributors to participate as everything happens in the browser. The Docker option required Docker setup but is more transparent—the Dockerfile and contributor source code are open-sourced and the whole process is verifiable. Moreover, the browser option utilizes snarkjs while the Docker option utilizes Kobi's implementation. This provided software variety and contributors could choose to contribute by whichever method they trust. In addition, participants could generate entropy via entering random text or making random cursor movements.

The zkLogin circuit and the ceremony client code were made open source and the links were made available to the participants to review before the ceremony. In addition, we also posted these developer docs and an audit report on the circuit from zkSecurity. We adopted challenge #0081 (resulting from 80 community contributions) from perpetual powers of tau in phase 1, which is circuit agnostic. We applied the output of the Drand random beacon at epoch #3298000 to remove bias. For phase 2, our ceremony had 111 contributions, 82 from browser and 29 from docker. Finally, we applied the output of the Drand random beacon at epoch #3320606 to remove bias from contributions. All intermediate files can be reproduced following instructions here for phase 1 and here for phase 2.

Finalization

The final CRS along with the transcript of contribution of every participant is available in a public repository. Contributors received both the hash of the previous contribution they were working on and the resulting hash after their contribution, displayed on-screen and sent via email. They can compare these hashes with the transcripts publicly available on the ceremony site. In addition, anyone is able to check that the hashes are computed correctly and each contribution is properly incorporated in the finalized parameters.

Eventually, the final CRS was used to generate the proving key and verifying key. The proving key is used to generate zero knowledge proof for zkLogin, stored with the ZK proving service. The verifying key was deployed as part of the validator software (protocol version 25 in release 1.10.1) that is used to verify the zkLogin transaction on Sui.

Security and Privacy

We'll walk through all zkLogin artifacts, their security assumptions, and the consequences of loss or exposure.

JWT Token

The JWT token's validity is scoped on the client ID (i.e. aud) to prevent phishing attacks. The same origin policy for the proof prevents the JWT token obtained for a malicious application from being used for zkLogin. The JWT token for the client ID is sent directly to the application frontend through the redirect URL. A leaked JWT token for the specific client ID can compromise user privacy, as these tokens frequently hold sensitive information like usernames and emails. Furthermore, if a backend salt server is responsible for user salt management, the JWT token could potentially be exploited to retrieve the user's salt, which introduces additional risks.

However, a JWT leak does not mean loss of funds as long as the corresponding ephemeral private key is safe.

User Salt

The user salt is required to get access to the zkLogin wallet. This value is essential for both ZK proof generation and zkLogin address derivation.

The leak of user salt does not mean loss of funds, but it enables the attacker to associate the user's subject identifier (i.e. sub) with the Sui address. This can be problematic depending on whether pairwise or public subject identifiers are in use. In particular, there is no problem if pairwise IDs are used (e.g., Facebook) as the subject identifier is unique per RP. However, with public reusable IDs (e.g., Google and Twitch), the globally unique sub value can be used to identify users.

Ephemeral Private Key

The ephemeral private key's lifespan is tied to the maximum epoch specified in nonce for creating a valid ZK proof. Should it be misplaced, a new ephemeral private key can be generated for transaction signing, accompanied by a freshly generated ZK proof using a new nonce. However, if the ephemeral private key is compromised, acquiring the user salt and the valid ZK proof would be necessary to move funds.

Proof

Obtaining the proof itself cannot create a valid zkLogin transaction because an ephemeral signature over the transaction is also needed.

Privacy

By default, there is no linking between the OAuth subject identifier (i.e. sub) and a Sui address. This is the purpose of the user salt. The JWT token is not published on-chain by default. The revealed values include iss, aud and kid so that the public input hash can be computed, any sensitive fields such as sub are used as private inputs when generating the proof.

The ZK proving service and the salt service (if maintained) can link the user identity since the user salt and JWT token are known, but the two services are stateless by design.

In the future, the user can opt in to verify their OAuth identity associated with an Sui address on-chain.

FAQ

What providers is zkLogin compatible with?

zkLogin can support providers that work with OpenID Connect built on top of the OAuth 2.0 framework. This is a subset of OAuth 2.0 compatible providers. See latest table for all enabled providers. Other compatible providers will be enabled via protocol upgrades in the future.

How is a zkLogin Wallet different from a traditional private key wallet?

Traditional private key wallets demand users to consistently recall mnemonics and passphrases, necessitating secure storage to prevent fund loss from private key compromise.

On the other hand, a zkLogin wallet only requires an ephemeral private key storage with session expiry and the OAuth login flow with expiry. Forgetting an ephemeral key does not result in loss of funds, because a user can always sign in again to generate a new ephemeral key and a new ZK proof.

How is zkLogin different from MPC or Multisig wallets?

Multi-Party Computation (MPC) and Multisig wallets rely on multiple keys or distributing multiple key shares and then defining a threshold value for accepting a signature.

zkLogin does not split any individual private keys, but ephemeral private keys are registered using a fresh nonce when the user authenticates with the OAuth provider. The primary advantage of zkLogin is that the user does not need to manage any persistent private key anywhere, not even with any private keys management techniques like MPC or Multisig.

You can think of zkLogin as a 2FA scheme for an address, where the first part is user's OAuth account and the second is the user's salt.

Furthermore, because Sui native supports Multisig wallets, one can always include one or more zkLogin signers inside a Multisig wallet for additional security, such as using the zkLogin part as 2FA in k-of-N settings.

If my OAuth account is compromised, what happens to my zkLogin address?

Because zkLogin is a 2FA system, an attacker that has compromised your OAuth account cannot access your zkLogin address unless they have separately compromised your salt.

If I lose access to my OAuth account, do I lose access to my zkLogin address?

Yes. You must be able to log into your OAuth account and produce a current JWT in order to use zkLogin.

Does losing my OAuth credential mean the loss of funds in the zkLogin wallet?

A forgotten OAuth credential can typically be recovered by resetting the password in that provider. In the unfortunate event where user's OAuth credentials get compromised, an adversary will still require to obtain user_salt, but also learn which wallet is used in order to take over that account. Note that modern user_salt providers may have additional 2FA security measures in place to prevent provision of user's salt even to entities that present a valid, non-expired JWT.

It's also important to highlight that due to the fact that zkLogin addresses do not expose any information about the user's identity or wallet used, targeted attacks by just monitoring the blockchain are more difficult. Finally, on the unfortunate event where one loses access to their OAuth account permanently, access to that wallet is lost. But if recovery from a lost OAuth account is desired, a good suggestion for wallet providers is to support the native Sui Multisig functionality and add a backup method. Note that it's even possible to have a Multisig wallet that all signers are using zkLogin, i.e. an 1-of-2 Multisig zkLogin wallet where the first part is Google and the second Facebook OAuth, respectively.

Can I convert or merge a traditional private key wallet into a zkLogin one, or vice versa?

No. The zkLogin wallet address is derived differently compared to a private key address.

Will my zkLogin address ever change?

zkLogin address is derived from sub, iss, aud and user_salt.

The address will not change if the user logs in to the same wallet with the same OAuth provider, since sub, iss, aud and user_salt (see definitions) will remain unchanged in the JWT token, even though the JWT token itself may look different every time the user logs in.

However, if the user logs in with different OAuth providers, your address will change because the iss and aud are defined distinctly per provider.

In addition, each wallet or application maintains its own user_salt, so logging with the same provider for different wallets may also result in different addresses.

See more on address definition.

Can I have multiple addresses with the same OAuth provider?

Yes, this is possible by using a different wallet provider or different user_salt for each account. This is useful for separating funds between different accounts.

Is a zkLogin Wallet custodial?

A zkLogin wallet is a non-custodial or unhosted wallet.

A custodial or hosted wallet is where a third party (the custodian) controls the private keys on behalf of a wallet user. No such third-party exists for zkLogin wallets.

Instead, a zkLogin wallet can be viewed as a 2-out-of-2 Multisig where the two credentials are the user's OAuth credentials (maintained by the user) and the salt. In other words, neither the OAuth provider, the wallet vendor, the ZK proving service or the salt service provider is a custodian.

Generating a ZK proof is expensive, is a new proof required to be generated for every transaction?

No. Proof generation is only required when ephemeral KeyPair expires. Since the nonce is defined by the ephemeral public key (eph_pk) and expiry (max_epoch), the ZK proof is valid until what the expiry is committed to nonce in the JWT token. The ZK proof can be cached and the same ephemeral key can be used to sign transactions till it expires.

Does zkLogin work on mobile?

zkLogin is a Sui native primitive and not a feature of a particular application or wallet. It can be used by any Sui developer, including on mobile.

Is account recovery possible if the user loses the OAuth credentials?

Yes, the user can follow the OAuth providers' recovery flow. The ephemeral private key can be refreshed and after completing a new OAuth login flow, the user can obtain new ZK proof and sign transactions with the refreshed key.

What are some assumptions for the zkLogin circuit?

Due to the way Groth16 works, we impose length restrictions on several fields in the JWT. Some of the fields that are length-restricted include aud, iss, the JWT's header and payload. For example, zkLogin can currently only work with aud values of up to length 120 (this value is not yet final). In general, we tried to make sure that the restrictions are as generous as possible. We have decided on these values after looking at as many JWTs that we could get our hands on.

How is zkLogin different from other solutions that support social login?

While providing social login with Web2 credentials for Web3 wallet is not a new concept, the existing solutions have one or more of the trust assumptions:

  1. Trust a different network or parties to verify Web2 credentials other than the Blockchain itself, usually involving a JWK oracle posted on-chain by a trusted party.
  2. Trust some parties to manage a persistent private key, whether it uses MPC, threshold cryptography or secure enclaves.
  3. Relying on smart contracts (account abstraction) to verify the JWT token on-chain with revealing privacy fields, or to verify ZK proofs on-chain which can be expensive.

Some of the existing deployed solutions rely on some of these assumptions. Web3Auth and DAuth social login requires deployment of custom OAuth verifiers to Web3auth Auth Network nodes to verify the JWT token. Magic Wallet and Privy also require custom OAuth identity issuer and verifiers to adopt the DID standard. All of the solutions still require persistent private keys management, either with trusted parties like AWS via delegation, Shamir Secret Sharing or MPC.

The key differentiators that zkLogin brings to Sui are:

  1. Native Support in Sui: Unlike other solutions that are blockchain agnostic, zkLogin is deployed just for Sui. This means a zkLogin transaction can be combined with Multisig and sponsored transactions seamlessly.

  2. Self-Custodial without additional trust: We leverage the nonce field in JWT token to commit to ephemeral public key, so no persistent private key management is required with any trusted parties. In addition, the JWK itself is an oracle agreed upon by the quorum of stakes by the validators with trusting any source of authority.

  3. Full privacy: Nothing is required to submit on-chain except the ZK proof and the ephemeral signature.

  4. Compatible with Existing Identity Providers: zkLogin is compatible with providers that adopt OpenID Connect. No need to trust any intermediate identity issuers or verifiers other than the OAuth providers themselves.

How to verify a zkLogin signature offchain?

The following options support a zkLogin signature over either transaction data or personal message.

  1. Use keytool: See usage in keytool.
$SUI_BINARY keytool zk-login-sig-verify -h
  1. Use a self hosted server endpoint: See usage in zklogin-verifier.