Skip to content

Rotating Canvas Games

Menu
  • Games
    • Balloon Shooter
    • Choozak
    • Polar
    • Tap Shoot Zombie!
    • Speedboat Santa
    • Rapid Car Rush
    • Turtle Leap
    • Crossword
  • HTML5 Games
    • Speedboat Santa HTML5
    • Choozak HTML5
  • Blog
  • Tutorials Index
  • About Us
  • Contact Us
  • Privacy Policy & Terms and Conditions
    • Privacy Policy
    • Terms and Conditions
    • Copyright Notice
    • Disclaimer
Menu

Sui ZK Login – Deep Dive

Posted on December 4, 2023May 4, 2024 by Rahul Srivastava

What are ZK Proofs

In simpler words, Zero Knowledge Proof help a prover verify a statement is true without revealing the details. Lets see some examples to understand

  • You can prove that you know the number which is a pre-image of a value without revealing the number
  • You can prove that you know the private key corresponding to a public key
  • You can prove that you are one of 50 students in a class without revealing your details

We have some public and private inputs which fed into a ZK circuit will generate output(s). Verifier can be certain that prover knows the private inputs if the output matches the value which they are looking for

In circom circuits, inputs are private and outputs are public by default.

ZK login

Sui’s ZK login process takes users JWT and salt and generates input signals which the circuit requires. If circuit runs successfully then we know that user knows the JWT and the private salt. They have a very detailed documentation of the whole process here. Please check it out

To understand the circuit better, lets take the following JWT as input. This is the same JWT used in their tests

Circuit is broadly divided in 10 steps. Lets go through it one by one. Total list of inputs can be seen here. Before we go through the steps lets look at one of the helper functions which we will use to calculate hash values

HashBytesToField: (Poseidon Hash of string)

  • Lets calculate poseidon hash of name field : “sub” with length set to 32 characters
  • First convert sub to ascii array=[115,117,98];
  • Pad it by 0 to total length of 32 bytes. [115, 117, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
  • Divide this array in chunks of 31 bytes [117, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],[115] in little endian format (big integer in multiple of 8 less than 254 = 248 i.e. 31 bytes)
  • Convert this array to big integer [“207397477721056441812414413661424614846155824613720115982633852925168844800”, “115”]
  • Feed this to Poseidon hash function to get hash 9102752833182448263444250585012134730074321235810986230287216596098480554553

In addition to above we need to find starting index of few of the fields in base64 encoded JWT. Below is an illustration of steps to get there

Step 1: Parse out the JWT Header

This requires 2 input signals.

signal input padded_unsigned_jwt[inCount];
signal input payload_start_index; //header length + '.'.length

Steps to get padded_unsigned_jwt (SHA2 padded)

  • Remove signature from JWT. We need only header + ‘.’ + payload (in base64)
  • Convert the modified JWT to binary format
  • Add “1” at end of this string
  • We need to create a padded string with length in multiple of 512 (SHA2 block is 64 bytes in size). Fill the rest of the string except last 8 bits.
  • In last 8 bits, store the length of modified JWT in binary format
  • Pad the string to max length provided in circuit. For our case its 1600
  • Convert the string to byte format

Below is js code for below. Github link

const getUnsignedPaddedJWT=({jwt,length,paddingValue}:{jwt: string,length: number,paddingValue:number}):
{paddedUnsignedJwt:number[],payloadLen:number,numSha2Blocks: number,payloadStartIndex:number}=>{
    const jwtArray=jwt.split('.');
    const s=jwtArray[0]+'.'+jwtArray[1];
    
    //get binary
    let jwtBinary = getBigNumber( getPaddedBase64Ascii( {base64:s,length:s.length,paddingValue:0})).toString(2).padStart(s.length*8,"0");
    const len=jwtBinary.length;
    //Add 1
    jwtBinary=jwtBinary+"1";
    //Fill rest of array with 0's except last byte
    Array(512-64-jwtBinary.length%512).fill(0).map(_m=>jwtBinary+="0");
    //Fill last byte with len
    jwtBinary+=BigInt(len).toString(2).padStart(64,"0");
    const numSha2Blocks=jwtBinary.length/512;
    const unsignedPaddedJWT:number[]=[];
    chunkString(jwtBinary,8)!.map(m=>unsignedPaddedJWT.push(parseInt(BigInt('0b'+m).toString(10))));
    Array(length-unsignedPaddedJWT.length).fill(0).map(m=>unsignedPaddedJWT.push(paddingValue));
    return {paddedUnsignedJwt:unsignedPaddedJWT,numSha2Blocks,payloadLen:jwtArray[1].length,
        payloadStartIndex:jwtArray[0].length+1};
}

Apart from this, there is a check that ‘.’ appears at correct position in JWT. Header Field is also created using hash bytes to field function which we mentioned above.

Step 2: Check validity of SHA2 padding & Compute SHA2

Inputs for this step

signal input num_sha2_blocks; //number of sha2 blocks used to created SHA2 padded jwt. 10 for google's jwt 5120/512
signal input payload_len; //length of payload in jwt

Step 3: Check signature

In this step, signature and modulus are input signals. This part verifies if the RSA signature is valid or not.

signal input signature[32];
signal input modulus[32];

We need to do the following to convert signature and modulus in above format. We need to pass limbs (word of size of 8 bytes) as input. We have to follow below steps

  • Siginature and modulus are 2^2048 size numbers. Convert the string to BigInt
  • Convert the bigint to boolean and pad to 2048 chars.
  • Chunk boolean in 32 char size and convert the chunk to bigint number of base 10 (decimal)
  • Reverse the array to make it little endian

To get modulus for signature in google auth, we need to download the modulus data from here. Modulus updates once everyday or so. It usually has more than one set of value. In JWT header, there key id (‘kid’), which we need to match in json data to get correct modulus.

Step 4: Check extend key claim & extract key name & value

Inputs for this step:

signal input ext_kc[maxExtKCLen];
signal input ext_kc_length;
signal input kc_index_b64;
signal input kc_length_b64;
signal input kc_name_length;
signal input kc_colon_index;
signal input kc_value_index;
signal input kc_value_length;

This step of the circuit takes the above inputs and parses the original google jwt to get the extended claims name and value fields. It compares it with the above inputs. Apart from this two more temp signals are created for future use kc_name_F & kc_value_F. They are poseidon hashes for the fields. We calculate it using the helper function HashBytesToField mentioned above. Also below is illustration of how to calculate the above fields

Step 5: Check the nonce in JWT

Inputs for this step. Similar to above step but few less fields are required as name will always be “nonce” and length of nonce will always be 27.

signal input ext_nonce[maxExtNonceLength];
signal input ext_nonce_length;
signal input nonce_index_b64;
signal input nonce_length_b64;
signal input nonce_colon_index;
signal input kc_value_index;

signal input eph_public_key[2];
signal input max_epoch;
signal input jwt_randomness;

Similar to above step, circuit extracts nonce value and validates it in jwt. Second part of the step is to check if input signals to calculate nonce match or not

We calculate eph_public_key array by first creating Ed25519PublicKey. Then convert into BigInt(Big Endian). eph_public_key[0]= publicKey / 2^128, eph_public_key[1]= publicKey % 2^128
Nonce is poseidon hash of (eph_public_key[0],eph_public_key[1], max_epoch , jwt_randomness). Frontend creates these values before login is done so that nonce can be passed as argument in JWT. This confirms that user knows owns the JWT. Also nonce is always of 27 bytes because we take the last 40 chars of 64 char poseidon hash hex value. 40chars =20 bytes which in turn creates a base64 string of 28 bytes with last char as ‘=’ which we remove.

Step 6: Check email verified claim

In our case this will be nonce string as key claim is not email for our google JWT. Verification happens similar to that of step 4. Inputs for this step

signal input ext_ev[maxExtEVLength]
signal input ex_ev_length;
signal input ex_index_b64;
signal input ev_length_b64;
signal input name_length;
signal input ev_colon_index;
signal input ev_value_index;
signal input ev_value_length;

Step 7: Check and extract aud

Here are the input signals for this step. Similar to step 4, we extract aud and validate it and tt the end we store aud_value_F by hashing byte value to field.

signal input ext_aud[maxExtAudLength]
signal input ex_aud_length;
signal input aud_index_b64;
signal input aud_length_b64;
signal input aud_colon_index;
signal input aud_value_index;
signal input aud_value_length;

Step 8: Reveal the iss portion of base64 jwt

Here are the input signals for this step

signal input iss_index_b64;
signal input iss_length_b64;

Circuit slices the iss from jwt based on the above index and length. Then circuit calculates poseidon hash and position modulo 4 of iss in JWT.

Step 9: Compute the address seed

Here is the input signal for this step

signal input salt;

Circuit uses salt that there is no connect between JWT (web2) and Sender Address (web3) and no one without the salt can generate the address.

Address send = Poseidon Hash of ( kc_name_F (calculated in step 4), kc_value_F (calculated in step 4), aud_value_F (calculated in step 7), Poseidon_Hash(salt))
Note: Salt is hashed first and then hashed together with other fields

Step 10: Validate the public signal matches hash of the private signals

Here is the input signal for this step

signal input all_inputs_hash;

The above hash is hash of values listed below

  • eph_public_key[0] //used to calculate nonce in frontend
  • eph_public_key[1] //used to calculate nonce in frontend
  • address_seed //calculated in step 9
  • max_epoch //used to calculate nonce in frontend
  • iss_b64_F //Poseidon hash of 32 byte fields of iss in base64 format.
  • iss_index_in_payload_mod_4 //Position modulo 4 of starting index of iss in JWT. It can be 0,1,2,3
  • header_F //Hash of 32 byte fields of header
  • modulus_F //Hash of 32 bytes fields of modulus

If all constraints match zk circuits passes and login can proceed.

  • Tweet
©2025 Rotating Canvas Games | Built using WordPress and Responsive Blogily theme by Superb
 

Loading Comments...