Preparing a Self-sovereign Identity recipe by using the ZKP and Verifiable Credentials ingredients

Zakwan Jaroucheh
LastingAsset
Published in
7 min readFeb 16, 2023

In the last few days, I was experimenting with the Eden3 protocol which is a blockchain-based protocol that aims to create a decentralized platform for community-based decision-making. I was trying to follow their tutorial to initiate an identity, create some claims and verify these claims by using a smart contract. In this blog, I’ll go through the main steps to give you a rough idea of how it works. Before I go deep, let’s start with some background (and maybe for some people basic) information :)

In today’s digital world, our personal information is constantly being collected, stored, and shared by various organizations. From social media platforms to online shopping websites, we are required to disclose our personal information in order to access their services. However, with the increasing prevalence of data breaches and identity theft, it has become imperative to protect our personal information from unauthorized access and misuse. This is where the concept of self-sovereign identity comes into play.

Self-sovereign identity (SSI) is a decentralized identity management model that allows individuals to own and control their personal information. Instead of relying on centralized authorities such as governments or corporations to verify our identity, SSI enables individuals to create, store, and share their own digital identities. This means that we can decide what information to share, with whom, and for how long. It also provides individuals with greater privacy and control over their personal information, as well as protection against identity theft and fraud.

To achieve the vision of self-sovereign identity, verifiable credentials and zero-knowledge proofs (zkp) can be used. Verifiable credentials are digital credentials that contain information about an individual’s identity, qualifications, and other attributes. These credentials are issued by trusted entities, such as universities, employers, and government agencies. Unlike traditional identity documents, verifiable credentials can be easily shared and verified, making them an ideal tool for self-sovereign identity.

Zero-knowledge proofs, on the other hand, are cryptographic techniques that allow individuals to prove the validity of their identity attributes without revealing the actual information. With zkp, individuals can prove that they meet certain criteria without revealing any additional information that is not necessary for verification. This provides greater privacy and security, as the individual’s personal information remains encrypted and inaccessible to unauthorized parties.

Now let’s jump into the code. Let’s first create an identity by creating BabyJubJub keypair

// BabyJubJub key

// generate babyJubjub private key randomly
babyJubjubPrivKey := babyjub.NewRandPrivKey()

// generate public key from private key
babyJubjubPubKey := babyJubjubPrivKey.Public()

Each identity state is represented by three identity trees: claim tree, revocation tree, and roots tree. All of these are Sparse Merkle Trees.


ctx := context.Background()

// Tree storage
store := memory.NewMemoryStorage()

// Generate identity trees

// Create empty Claims tree
clt, _ := merkletree.NewMerkleTree(ctx, memory.NewMemoryStorage(), 32)

// Create empty Revocation tree
ret, _ := merkletree.NewMerkleTree(ctx, memory.NewMemoryStorage(), 32)

// Create empty Roots tree
rot, _ := merkletree.NewMerkleTree(ctx, memory.NewMemoryStorage(), 32)

Every time the issuer wants to issue a claim against some other identity/object or against themselves, they need to add that claim to the Claim tree. The first claim that needs to be added is the auth claim which represents the ownership of the identity private key. Let’s create the auth claim and add it to the Claim tree:

// Create Auth Claim
// The hash of the predefined auth schema is "ca938857241db9451ea329256b9c06e5"
authSchemaHash, _ := core.NewSchemaHashFromHex("ca938857241db9451ea329256b9c06e5")

// Add revocation nonce. Used to invalidate the claim. This may be a random number in the real implementation.
revNonce := uint64(1)

authClaim, _ := core.NewClaim(authSchemaHash,
core.WithIndexDataInts(babyJubjubPubKey.X, babyJubjubPubKey.Y),
core.WithRevocationNonce(revNonce))

// Get the Index of the claim and the Value of the authClaim
hIndex, hValue, _ := authClaim.HiHv()

// add auth claim to claims tree with value hValue at index hIndex
clt.Add(ctx, hIndex, hValue)

Now we need to generate the Merkle Tree Proof that the auth claim exists in the Claim tree and another proof that this claim has not been revoked.

// Add revocation nonce. Used to invalidate the claim. This may be a random number in the real implementation.
revNonce := uint64(1)

// 1. Generate Merkle Tree Proof for authClaim at Genesis State
authMTPProof, _, _ := clt.GenerateProof(ctx, hIndex, clt.Root())

// 2. Generate the Non-Revocation Merkle tree proof for the authClaim at Genesis State
authNonRevMTPProof, _, _ := ret.GenerateProof(ctx, new(big.Int).SetUint64(revNonce), ret.Root())

As mentioned before any identity state can be represented by the three above trees, and the ID of the identity can be retrieved based on this state:

// Retrieve identity state

state, _ := merkletree.HashElems(
clt.Root().BigInt(),
ret.Root().BigInt(),
rot.Root().BigInt())


// Snapshot of the Genesis State
genesisTreeState := circuits.TreeState{
State: state,
ClaimsRoot: clt.Root(),
RevocationRoot: ret.Root(),
RootOfRoots: rot.Root(),
}

// Retrieve Identifier (ID)

id, _ := core.IdGenesisFromIdenState(core.TypeDefault, state.BigInt())

Let’s create another claim that some identity has a specific age and add that claim to the Claim tree to produce a new state.

// Create a new random claim
schemaHex := hex.EncodeToString([]byte("myAge_test_claim"))
schema, _ := core.NewSchemaHashFromHex(schemaHex)

code := big.NewInt(25)

newClaim, _ := core.NewClaim(schema, core.WithIndexDataInts(code, nil))

// Get hash Index and hash Value of the new claim
hi, hv, _ := newClaim.HiHv()

// Add claim to the Claims tree
clt.Add(ctx, hi, hv)

// Fetch the new Identity State
newState, _ := merkletree.HashElems(
clt.Root().BigInt(),
ret.Root().BigInt(),
rot.Root().BigInt())

So far, we know the identity ID, the old state (the genesis state where we have only one claim, the auth claim), the proof of the existence of the auth claim on the Claim tree, and the new state (after adding the age claim). The last step is for the identity to sign the old and new state using their private key:

// Sign a message (hash of the genesis state + the new state) using your private key
hashOldAndNewStates, _ := poseidon.Hash([]*big.Int{state.BigInt(), newState.BigInt()})

signature := babyJubjubPrivKey.SignPoseidon(hashOldAndNewStates)

Generate ZKP

In order to generate the ZKP of the state transition, we need to use the stateTransition circuit. To do that, we need to feed the above parameters to the circuit as input. The following code will convert these parameters to a json format that we are going to use to generate the proof.

// Generate state transition inputs
stateTransitionInputs := circuits.StateTransitionInputs{
ID: id,
OldTreeState: genesisTreeState,
NewState: newState,
IsOldStateGenesis: true,
AuthClaim: circuits.Claim{
Claim: authClaim,
Proof: authMTPProof,
NonRevProof: &circuits.ClaimNonRevStatus{
Proof: authNonRevMTPProof,
},
},
Signature: signature,
}

// Perform marshalling of the state transition inputs
inputBytes, _ := stateTransitionInputs.InputsMarshal()

fmt.Println(string(inputBytes))
{"authClaim":["304427537360709784173770334266246861770","0","1544754317725512589371965764415883485748874777550452225772657054222263193025","19277888743220710324480198580766059212282324897601757365434627368727930379177","1","0","0","0"],"authClaimMtp":["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","0","0","0"],"authClaimNonRevMtp":["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","0","0","0"],"authClaimNonRevMtpAuxHi":"0","authClaimNonRevMtpAuxHv":"0","authClaimNonRevMtpNoAux":"1","userID":"356980028367566120805062787500019413451119557661316317796450681725020471296","newUserState":"3460014463593306640657793702189525710647713395100793883376075741928494721794","oldUserState":"16811987078627135270221845561045768054843847431814182832503006631051171375821","isOldStateGenesis":"1","claimsTreeRoot":"15563426065860454726011316910357988468145239132067367117445903895120881667655","revTreeRoot":"0","rootsTreeRoot":"0","signatureR8x":"15455196333944538107254310953588975727922131028996178991129692816814602476251","signatureR8y":"18014895788833863269681432062079837783565918585266123675772249710234033456662","signatureS":"746848586487536906042526486315692348776316087798601178491792331262474277478"}

Now we can use the script generate.sh to generate the proof.

~/iden3-demo/compiled-circuits$ ./generate.sh stateTransition
{
authClaim: [
'304427537360709784173770334266246861770',
'0',
'1544754317725512589371965764415883485748874777550452225772657054222263193025',
'19277888743220710324480198580766059212282324897601757365434627368727930379177',
'1',
'0',
'0',
'0'
],
authClaimMtp: [
'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', '0',
'0', '0'
],
authClaimNonRevMtp: [
'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', '0',
'0', '0'
],
authClaimNonRevMtpAuxHi: '0',
authClaimNonRevMtpAuxHv: '0',
authClaimNonRevMtpNoAux: '1',
userID: '356980028367566120805062787500019413451119557661316317796450681725020471296',
newUserState: '3460014463593306640657793702189525710647713395100793883376075741928494721794',
oldUserState: '16811987078627135270221845561045768054843847431814182832503006631051171375821',
isOldStateGenesis: '1',
claimsTreeRoot: '15563426065860454726011316910357988468145239132067367117445903895120881667655',
revTreeRoot: '0',
rootsTreeRoot: '0',
signatureR8x: '15455196333944538107254310953588975727922131028996178991129692816814602476251',
signatureR8y: '18014895788833863269681432062079837783565918585266123675772249710234033456662',
signatureS: '746848586487536906042526486315692348776316087798601178491792331262474277478'
}
...

[INFO] snarkJS: OK!

The script creates two json files: public.json (which contains the public information such as the id, old state and new sate), and proof.json which contains the proof:

{
"pi_a": [
"14635220018075388529268419525430102831724649349033783579853565728676106845297",
"13967846691105632163580813931096788507818763634395789084239551684727623769090",
"1"
],
"pi_b": [
[
"3382072814410318492884536840685724287630134697508079484139335811372751533850",
"19774845211696210880591089401117983349422153996149532966128913344643722296765"
],
[
"12651541068352415212831798606734352832479469468640442073359718291242220874614",
"21803691545254858942768283456141291385142604429850782904390167597548653010131"
],
[
"1",
"0"
]
],
"pi_c": [
"1118188338767482557471679356392957505193115660947274727648780795048579840731",
"12574911344717913066994425056897101175403632898110271845383558095523136187023",
"1"
],
"protocol": "groth16",
"curve": "bn128"
}

On-chain Verification

What we need to do is to find a way to link the identity with its recent state in a trustless way. And here comes the power of the smart contract. A smart contract can be implemented to map each identity to its state and update the state after verifying that the proof provided is valid.

I’ve deployed the relevant smart contract, called State, to the Polygon Mumbai test net at “0xdB4c661456A023a403CF83784f9d15a1D3540702”.

The two json files from the above step can also be exported as Solidity calldata in order to execute the verification on-chain.

snarkjs generatecall

Here is what the output would look like:

["0x205b3db113bc1bbb0988d81084cd04684e9f626ca900f3d4d48c5c7ba5adbc71", "0x1ee1856495c7c66e93b188bf52880e477bc7c3d13fa42f9dc2bff4e839e1dc02"],[["0x2bb82a6ee1933c55f0b5517e81f85fd38b2259bafbea3916bd2d3717a452bdbd", "0x077a2f74da53602b21219628f4a92bf1b2764ed5ad80fa7565b643642930371a"],["0x303473bbdf9e1f38cc25cd76df9aa7018f26a7d4f93b4c0582564821a6085cd3", "0x1bf884bb76cd7b1387c40e71a24274bafcf3e070639cb927626dc87b74b56b76"]],["0x0278df46a349056460387c8b8d2b200e11b36aeb239b5e60b748c340307c3adb", "0x1bcd25c90ab140b07cc79b5b5f98f65f42176e52a6782c44892323a0c300aa8f"],["0x00ca0b252b3f5ac5f13c9f675c297c5f2f043335dfc4d18c1265a5de49ab0000","0x252b3f5ac5f13c9f675c297c5f2f043335dfc4d18c1265a5de49abbb3d0aaacd","0x07a64c7d4c5be707b9188cca29261d3fc89dc3086af0b229ff73f7dbcc47eb02","0x0000000000000000000000000000000000000000000000000000000000000001"] 

The Solidity calldata output represents:

- `a[2]`, `b[2][2]`, `c[2]`, namely the proof
- `public[4]`, namely the public inputs of the circuit

Finally, we call the smart contract via the script update_state.js:

~/iden3-demo/hardhat$ npx hardhat run scripts/update-state.js --network mumbai
Identity State at t=0 BigNumber { value: "0" }
Identity State at t=1 BigNumber { value: "3460014463593306640657793702189525710647713395100793883376075741928494721794" }

pheww .. congrats, the identity state transition has been completed successfully. It is important to note that there is no connection between the ECDSA key pair associated with the Ethereum address that carries out the State Transition and the Baby Jubjub key pair that governs an identity.

Final thoughts

By using an identifier, individuals can track an identity’s status in a manner that is timestamped and resistant to tampering. The identifier remains unchanged throughout the life of the identity, whereas the identity’s state changes each time it is modified, such as when a claim is issued or revoked. Each ZK proof generated by an identity is compared to the identity state that is published on the chain. It’s important to note that the only information stored on-chain is the mapping that connects an identifier to its present identity state. Also, it is impossible to access any information stored in the identity trees, like the contents of a claim, by starting from the identifier and the identity state.

The source code can be found here.

--

--

Zakwan Jaroucheh
LastingAsset

Associate Professor. PhD in Software Engineering. Entrepreneur, Co-Founder of LastingAsset. Crypto, AI, & Blockchain enthusiast.