Welcome to zkPortal's documentation! Our tools allow you to verify your user's identity in a way that is Free, Fast and User-friendly.

Need to prove your identity? Go here!

Want to just get started and verify user's identities? Go here!

Free 🤑

Yes, that's right.

Fast 🚀

Users authenticate themselves faster than you can say identityprovingviapassportsissooutdatedandpronetofraud.

User-Friendly 🥇

User's only have to login to their online accounts, no hassle with physical documents anymore


Current methods to verify personal data are broken. By leveraging the latest cryptography, we make it possible to verify anyone’s personal data directly from their personal accounts at government and financial services websites. We eliminate society’s dependence on expensive manual document checks, ineffective algorithms and intermediaries who collect, leak and sell your data.

New legislation is forcing companies and crypto protocols around the world to franticly look for better identity infrastructure, while technology is breaking existing KYC solutions. We solve this by giving the world secure access to personal data.

Our solution lets users prove their identity through their existing online accounts. No need for them to grab a passport for the 100th time or risk losing it in the next hack. Various government, financial and telecom institutions already know the user's identity - we enable users to give you secure access. We eliminate your dependence on expensive manual document checks, ineffective algorithms and intermediaries who collect, leak and sell your data. To learn more about all the technical details, go here!

Getting started: proving your identity

Step 1: download the zkPortal mobile application

At the moment we are still in private beta. If you want to use zkPortal, just shoot an email to or sign up to our newsletter at


Step 2: authenticate using an identity provider

Simply open up the app, select one of the identity providers, login, and you will have automatically proved your identity!

If you just want to test the attestation functionality you can also use our Demo Exchange, for more info how to use it go here


Users can link their identity to their cryptocurrency account, in order to prove their uniqueness to particular on-chain or off-chain applications. Currently, the only supported applications require you to link your identity to Ethereum addresses. You can link address with WalletConnect or by scanning a QR code. Please visit in order to generate a QR code to prove you own your Ethereum account. For this, you will need to have Metamask installed.


Demo Exchange

For users to be able to test the attestations and applications we have build a Demo Exchange which allows easy generation of attestations. When you are in the mobile app you will see the logo for our Demo Exchange. When you click it you will be redirected to a simple login screen.


In this screen you can fill in a first name and last name (separated with an @ sign as indicated in the screenshot), those will be passed forward to the attestation service. The date of birth is hardcoded to 01/01/1970 and country: NL (for the Netherlands). If you want to try passport scanning you can select "Requires passport" option (passport data is not used with demo exchange)

After you have created a demo account you can move on to registering for an app with an ethereum key.

Airdrop Demo

To demonstrate how we can prove whether or not a user is unique we created a simple Airdrop demo. Please visit in order to apply for our test Airdrop - each unique user is guaranteed to only be eligible once!

Go ahead and try to hack us 🙀

How it works

Lets say you want to register an Airdrop with us: what we need from you is a unique name for your Airdrop as well as an expiry date. We will register in our backend everytime you are handing out an Airdrop. So lets say you want to send an Airdrop to ethereum address 0x1234abcd, first you check with our backend if we know this address (that means your user needs to first register his/her ethereum address of course in our App). If we give you the go ahead that we know the user and you have not send them yet an Airdrop we give you the 👍. If the user tries now to cheat the system and register a new address with us, we will happily do that but if you ask us again if you should provide that "apparent" new user with an Airdrop we will send you a 👎. That is because in the backend we are matching ethereum addresses to unique users and that match allows us to identify users even if they are using multiple addresses (which one should of course have) but not allow multiple Airdrops. Also, as we link to real identities bots are caught right away 💀.

Verify users for your own Airdrop

Do you want to verify user's uniqueness using zkPortal? Check out our documentation here!

Do you want to verify user identities?

Below are just a couple of examples for which you can use zkportal. Do you want to check your user's identities through our simple API? Whether for legal purposes, democratic voting and airdropping, or even account recovery, we got you covered. If you want to make sure that your users are not coming from a sanctioned jurisdition you can now use our new zero knowledge proof!

Reach out to us to join the private beta at

Verifying user uniqueness on Snapshot

We support snapshot for voting and provide a backend for it. Projects are then able to choose e.g. quadratic voting as a voting system as it is susceptible to sybil attacks as uniqueness is required for voting. See also:

For every vote and every update all votes and connected ethereum addresses need to be sent to the backend. This is provided by the general API-Post strategy from snapshot

Are you a DAO and do you want to enable 1 person 1 vote or quadratic voting? No problem, we have you covered:

Using the strategy in your Snapshot settings

To use the API strategy you have to select it in the settings of snapshot: setup

After that you edit the post command to include our endpoint:


Further you can edit the symbol and decimals if needed.


The snapshot strategy does not verify the TEE report yet! If enough community support exists we can merge a custom made strategy which would allow us to also provide a report in the response to the snapshot query to verify its source.

Letting users prove their uniqueness

All that is left, is for your DAO members to prove their uniqueness, you can point them to the instructions here!

Technical details

By default, addresses which are queried are given a score of 0. If a user proves their uniqueness with zkportal and links an address, the score will be set to 1. Even when users generate and link multiple addresses, only one address will get a score of 1.

Example Response from our backend:

  "score": [
      "score": 1,
      "address": "0xEA2E9cEcDFF8bbfF107a349aDB9Ad0bd7b08a7B7"
      "score": 0,
      "address": "0x3c4B8C52Ed4c29eE402D9c91FfAe1Db2BAdd228D"
      and so on

Verify user uniqueness for airdrops

If you want to have a fair airdrop or just make sure that your drop is really distributed to real people and not bots, with our easy to use interface you can make sure that you have a fair airdrop to your users. You can integrate with zkportal through the following steps:

Step 1. Request authentication token and appId

Request an authentication token and appid via mail.

Step 2. Integrate claim check into your app

When a user wants to claim an airdrop, you should in turn register the claim with zkPortal by sending a POST request to The response may be:

  • 201: the airdrop claim has been registered succesfully
  • 204: the airdrop claim was registered already in the past
  • 400: bad request
  • 401: unauthorized
  • 404: no user could be found which wants to link their identity with this airdrop
  • 500: internal server error

Two code examples follow below:

curl example
ZKPORTAL_APPID=<your app id from step 1>
ZKPORTAL_TOKEN=<your authentication token from step 1>
ZKPORTAL_AIRDROPID=<a unique id referencing your airdrop>
ZKPORTAL_USERACCOUNT=<user ethereum account>
curl -d '{"appid": ${ZKPORTAL_APPID}, "airdropid": ${ZKPORTAL_AIRDROPID}", account": ${ZKPORTAL_USERACCOUNT}' -H "Authorization: Bearer ${ZKPORTAL_TOKEN}>"

node example
const https = require("https");
const ZKPORTAL_APPID="your app id from step 1";
const ZKPORTAL_TOKEN="your authentication token from step 1"
const ZKPORTAL_AIRDROPID="a unique id referencing your airdrop"
const ZKPORTAL_USERACCOUNT="user ethereum account";
const postData = JSON.stringify({
  "appid": ZKPORTAL_APPID,
const options = {
  hostname: "",
  path: "/airdrop/claim",
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Content-Length": Buffer.byteLength(postData),
    "Authorization": "Bearer " + ZKPORTAL_TOKEN
const req = https.request(options, (res) => {
  console.log(`STATUS: ${res.statusCode}`);
  res.on("data", (chunk) => {
    console.log(`BODY: ${chunk}`);

req.on("error", (e) => {
  console.error(`problem with request: ${e.message}`);

Step 3. Ask your users to prove their identity using zkPortal

All that is left, is for your users to prove their uniqueness, you can point them to the instructions here!

zk Proof verification

zkPortal provides a way to verify ZK proofs in a browser using WebAssembly. When a ZK proof is generated, there's also a proving key generated along with it, and public inputs for the proof. To verify a proof you need to provide a verifying key, which is a part of a proving key and is provided to you by zkPortal, and public inputs.

Using verification library

The library comes with a WebAssembly module and supporting JavaScript code and type definitions. If your JS execution environment supports it, the library will use WebAssembly streaming API. In order to use it your JS execution environment also needs to support Response.arrayBuffer() method. The library can either use the bundled WebAssembly module or fetch it from a remote location. Note that to fetch the module from a remote location and to use streaming API, the web server needs to set Content-Type header to application/wasm.

Import the library into your project

Check out

A WebAssembly library for verifying zero-knowledge proofs from zkPortal.

This library is derived from ark-circom.

Proof verification function is exported and compiled into a WebAssembly module using wasm-pack.


The package is not published to NPM yet. Download dist directory if you want to add files to your project, or download the whole repository and import it as a local package.

import init, { verify_proof } from '@zkportal/zk-verifier';

async function verify() {
  await init(); // uses bundled WebAssembly module, you can also provide an argument, see API section of the README
  const verifyingKey = Uint8Array.from(...);
  const proof = Uint8Array.from(...);
  const publicInputs = Uint8Array.from(...);
  const success = verify_proof(verifyingKey, proof, publicInputs);


initInitialize the moduleOne of:
  • undefined to use bundled WebAssembly module
  • string with WebAssembly module URL
  • URL
  • Fetch API Request
  • Fetch API Response
  • BufferSource
  • WebAssembly.Module
  • Promise of any type above
Promise<any>Default export
verify_proofVerify a proof using a proof, verifying key, and public inputs
  • verifying_keyUint8Array
  • proofUint8Array
  • public_inputsUint8Array
boolean - verification successDestructured export


Examples provide the following files:

  • proof.json - contains serialized decimal bytes of a zero-knowledge proof
  • verifying_key.json - contains serialized decimal bytes of a verifying key you can use with the proof to verify it
  • public_inputs.json - contains serialized decimal bytes of public inputs you need to supply to verify the proof

If you use a bundler, then you can import those files, then create Uint8Array arrays from them and supply to verify_proof function.

  • Banlist This directory contains JSON files with serialized inputs you can use to verify that supplied country code "NL" is not in the list of banned countries "US IR RU".

    This example is a proof that a private, hidden from the verifier, country code string (supplied at the time of creating the proof) isn’t in the public, visible to verifier list of banned country codes (in this case it’s US, IR, RU).

  • Minimum age This directory contains JSON files with serialized inputs you can use to verify that supplied user's date of birth is earlier than the date of minimum age.

    This example is a proof that a private, hidden from the verifier, user's date of birth timestamp (supplied at the time of creating the proof) is earlier than the public, visible to verifier, minimum age timestamp (date of proof - required age).

Public API

This API is work in progress and subject to change


All the API requests must be sent to the base URL {{zkportal_backend}}.

We also have a test backend for you to use at base URL: {{zkportal_backend_test}}.

Please reach out to us first for an API key!

The test backend has 2 accounts added for all app integrations: 0x9db3fb0d6fa378ab7a51fef1dbf2aacd78108129 and 0xaa8f86e0e1cbeddeb30bf0ecfb1a795e03c21690.

The first one will have valid proofs of country and age. These valid proofs are taken directly from zk-verifier examples.

The second one will have invalid proofs, meaning that proof validation will fail. In this case, the proofs are exactly the same as for the first account but the public inputs for the proofs were changed to fail validation.

In the example, the country proof public input is a country blacklist USIRRU + user hash in the format described in this section. For verification to fail, the country blacklist part was changed to NLIRRU and encoded the same way. Since the private input to the proof was NL, the verification will fail.

In the example, the minimum age proof public input is a Unix timestamp for minimum required date of birth 1669637350 + user hash in the format described in this section. Since the example used 1669637349 as user's date of birth, in order for verification to fail, the user age timestamp must be bigger that the minimum age timestamp. For verification to fail, the minimum age timestamp part was changed to 1669637348, which is 1 second less that the user age. That means that the "user" is 1 second younger (1669637349 > 1669637348) than required, thus failing the proof verification.


All endpoints require an API key passed in the Authorization header.

Authorization: Bearer yourapikey

At the moment, we do not support API key generation. Please contact to get an API key.

An API key is linked to a certain Application. A user needs to link their Ethereum account to that application in zkPortal app before their data becomes visible for requests from that application.


All endpoints follow the same format for errors

  "code": 12345,
  "description": "Error description"

See Error Codes for error descriptions.

Common errors

All erroneous HTTP responses will come with a JSON body with error description and the code.

400 Bad Request

Possible reasons:

  • Missing a required query parameter
  • Invalid query parameter

403 Forbidden

Possible reasons:

  • API key is missing in the request
  • API key is not found in the database
  • API key is invalid
  • (For the future) API key has expired

404 Not Found

Possible reasons:

  • Not using the right endpoint
  • User doesn't exist

412 Precondition Failed

Possible reasons:

  • The backend is not running in TEE
  • The backend doesn't have access to one of the required services

429 Too Many Requests

The currently used limit is 100 RPS per IP.

500 Internal Server Error

Something went wrong on our side, please contact us with the details


This API produces application/json responses.

The API provides proofs of country and age if the user has submitted them.

Proof of country - a ZK proof that user's country is not in the ban list (specific to apps). A proof signature is created over country blacklist and hash of random number + user's country.

Proof of age - a ZK proof that the user is at least 18 years old. A proof signature is created over minimum age timestamp and hash of random number + user's date of birth timestamp (with hours, minutes, and seconds set to 0).

See Schema section for definitions of objects used in responses, e.g. GeneralProofObject.

About proving/verifying keys

We have generated static proving and verifying keys, a pair for each type of proof. zkPortal mobile app fetches them and uses to generate user's ZK proofs.

For convenience, we provide an API endpoint, which returns proving and verifying keys for all types of proofs we use. See the section below.

Get proving and verifying keys

The following endpoints serve static files:

  • {{zkportal_backend}}/static/age_proofs/proving_key
  • {{zkportal_backend}}/static/age_proofs/verifying_key
  • {{zkportal_backend}}/static/country_proofs/proving_key
  • {{zkportal_backend}}/static/country_proofs/verifying_key

API key: not required

Content type: application/octet-stream

Compression: not supported

Supported headers:

  • Range - supported unit is bytes


Request GET /health

API key: not required


  "apiVersion": 1,
  "isTee": true|false, // whether the backend is running in an enclave (SGX)
  "teeData": {
    "debug": true|false, // whether the enclave is running in debug mode
    "productId": 1, // uint16, unique identifier of the product running in the enclave
    "signerId": "", // hex representation of bytes uniquely identifying the enclave's signer
    "uniqueId": "", // hex representation of bytes uniquely identifying the enclave
    "securityVersion": 1 // uint, security version of the enclave
  } // optional, appears if "isTee" is true, enclave self report information

For more information about the information in an enclave self report, see TEE attestation report.

Get a list of linked accounts

Request: GET /accounts

API key: required


  "accounts": [
      "account": "ETH address",
      "proofOfCountry": GeneralProofObject|null,
      "proofOfAge": GeneralProofObject|null
  "total": 123 // total number of linked accounts


curl {{zkportal_backend}}/accounts -H 'Authorization: Bearer EXAMPLE_API_KEY'

Get a linked account by Ethereum address

Request: GET /accounts

Query parameters:

  • account - string, Ethereum address

API key: required


  "account": "ETH address",
  "proofOfCountry": GeneralProofObject|null,
  "proofOfAge": GeneralProofObject|null


curl {{zkportal_backend}}/accounts?account=0x646dF754896DCB25590306916de74C103bb7eFBF -H 'Authorization: Bearer EXAMPLE_API_KEY'

Get a list of accounts with proof of country

Request: GET /country_proofs

API key: required


  "accounts": [
      "account": "ETH address",
      "proofOfCountry": GeneralProofObject,
      "proofOfAge": null // is always null, whether the user has a proof of age or not
  "total": 123 // total number of accounts with proof of country


curl {{zkportal_backend}}/country_proofs -H 'Authorization: Bearer EXAMPLE_API_KEY'

Get a list of accounts with proof of age

Request: GET /age_proofs

API key: required


  "accounts": [
      "account": "ETH address",
      "proofOfCountry": null, // is always null, whether the user has a proof of country or not
      "proofOfAge": GeneralProofObject
  "total": 123 // total number of accounts with proof of age


curl {{zkportal_backend}}/age_proofs -H 'Authorization: Bearer EXAMPLE_API_KEY'



This object contains a ZK proof with validity and TEE attestation.

  "proof": ProofDataObject, // ZK proof object
  "validUntil": 1669597219, // Unix timestamp of the date when the proof expires
  "attestation": AttestationObject // TEE attestation object


This object contains the data related to the ZK proof, including everything needed to verify the proof

  "zkOutput": "", // Base64-encoded bytes of the ZK proof itself
  "proofDetails": ProofOfCountryDetailsObject|ProofOfAgeDetailsObject, // the values used to create a proof, specific to the circuit
  "signature": "", // Base64-encoded bytes of ZK proof's public inputs (publicInputs field) ASN.1 ECDSA signature.
  "publicInputs": "", // Base64-encoded bytes of the ZK proof's public inputs
  "verifyingKey": "" // Base64-encoded bytes of a ZK proof verifying key

For details about verifying a proof, see ZK proof section. The public key you get from verifying AttestationObject is the public part of the key, which was used to create signature in this object.


  "blacklist": "", // Concatenated list of ISO country codes banned from participating
  "userHash": "" // Base-64 encoded bytes of SHA256 hash over user's country ISO code + random bytes

Proof of country

Based on ProofOfCountryDetailsObject, publicInputs in ProofDataObject are the following components concatenated:

  1. Big-endian bits of country blacklist as ASCII characters. Example: USIRRU - 85 83 73 82 82 85, where 85 is 01010101 etc., so the full blacklist is 01010101 01010011 01001001 01010010 01010010 01010101.
  2. Big-endian bits of userHash (see Proof of Country Inputs for details about creating one). Example: 20a8b52013f6dee3aef039c1572aaa220463fe23aadc64ddd8aa3b8c84380bc0 - take 20, which is in hex form - 00100000 as bits - so the full userHash is
00100000 10101000 10110101 00100000 00010011 11110110 11011110 11100011 10101110 11110000 00111001 11000001 01010111 00101010 10101010 00100010 00000100 01100011 11111110 00100011 10101010 11011100 01100100 11011101 11011000 10101010 00111011 10001100 10000100 00111000 00001011 11000000
  1. Concatenate blacklist and user hash bits to get to publicInputs.

Here is a JavaScript snippet you can use for converting bytes (numbers 0-255) to bits

 * @param {Array<number>|Uint8Array} byteArray
 * @return {Array<number>} List of bits in big endian
function bytesToBits(byteArray) {
  const bitArr = Array(byteArray.length * 8);
  let idx = 0;
  for (let i = 0; i < byteArray.length; i++) {
    for (let j = 7; j >= 0; j--) {
      bitArr[idx] = (byteArray[i] >> j) & 0x01;
  return bitArr;


  "minimumAge": 1670577921, // Unix Timestamp of minimum age (e.g. if the minimum age is 18, then this will be "<Date Of Proof timestamp> - 18 years")
  "userHash": "" // Base-64 encoded bytes of SHA256 hash over user's age timestamp as bytes + random bytes

Proof of age

Based on ProofOfAgeDetailsObject, publicInputs in ProofDataObject are the following components concatenated:

  1. Big-endian bits of minimum age Unix timestamp. Example: 1669637350 - 99 132 164 230, where 99 is 01100011 etc., so the full minimum age is 01100011 10000100 10100100 11100110.
  2. Big-endian bits of userHash (see Proof of Age Inputs for details about creating one). Example: d4eb109a529a76c7d964d616fe1f2c780a686c37f034e003a7aa7f7f607fdc75 - take d4, which is in hex form - 11010100 as bits - so the full userHash is
11010100 11101011 00010000 10011010 01010010 10011010 01110110 11000111 11011001 01100100 11010110 00010110 11111110 00011111 00101100 01111000 00001010 01101000 01101100 00110111 11110000 00110100 11100000 00000011 10100111 10101010 01111111 01111111 01100000 01111111 11011100 01110101
  1. Concatenate minimum age and user hash bits to get to publicInputs.

Since timestamp is a number but the circuit takes bits for the minimum age, see the snippet below for converting numbers to bytes. After you get bytes, you can use the snippet in the Proof of country section to convert them to bits.

Here is a JavaScript snippet you can use for converting numbers to bytes

 * @param {number} int Number to convert
 * @param {number} size Byte size of the number to convert, e.g. 4 bytes for numbers 0-4294967295
 * @return {Uint8Array} Array of big endian bytes
function int2ba(int, size) {
  let hexstr = int.toString(16);
  if (hexstr.length % 2) {
    hexstr = '0' + hexstr;
  const ba = [];
  for (let i = 0; i < hexstr.length / 2; i++) {
    ba.push(parseInt(hexstr.slice(2*i, 2*i + 2), 16));
  if (size) {
    const oldlen = ba.length;
    for (let j = 0; j < (size - oldlen); j++) {
  return new Uint8Array(ba);


This object contains TEE attestation data, which allows to create a full circle of trust.

  "report": "", // Base64-encoded TEE report over a SHA256 hash of ephemeral ECDSA public key used for signing the proof inputs + proof expiry timestamp - hash(key|timestamp)
  "publicKey": "", // Base64-encoded bytes of PKIX, ASN.1 DER form of the ECDSA (curve P256) public key used to sign proof inputs in ProofDataObject
  "signature": "" // Base64-encoded signature for the ciphertext containing the data, which was used to create the proof

For details about verifying a TEE report, see SGX report verification example.

Error codes


Technical architecture

High level architecture and protocol

Our identity proofs are made possible by the fact that whenever a user retrieves data from a website, the response is encrypted using TLS. As was demonstrated by various researchers, it is possible to leverage this encryption as an attestation to the underlying data (see for a literature review this thesis).

setup layout

As illustrated in the image above, our backend infrastructure is involved in creating the proofs. When you make an identity proof on zkPortal using this trusted backend infrastructure, two properties should hold:

  • (1) Integrity: is the proof of your personal details correct?
  • (2) Privacy: are you only sharing data that you want to share?

Today, the backend consists of a popular type of Trusted Execution Environment called SGX. This gives strong guarantees about which code is exactly running in the background. In the future, we will strengthen the security model by leveraging secure Multi-Party Computation (MPC), so even multiple different Trusted Execution Environments can be used to guarantee integrity and privacy. Importantly, our backend infrastructure never stores Personal Identifiable Information (PII)! The data lives with the user on their mobile phone and the user is always in charge with whom, what data is shared. Our infrastructure provides a unique identifier - which we call UID below - and allows users to prove statements about themselves such as: am I a US citizen?

setup architecture

The image above provides a high level overview of the different types of services involved in making your private identity proof possible. The image excludes many details (for which you can check out our open source code soon™ ) but it should give you a clear intuition. It is important to note again in advance that at no point personal information can be leaked outside of the servers - more details will follow below. Let's see what happens at each step:

  • (1) First, the user logs into their data provider of choice in order to get an authentication cookie. Next, the zkPortal main service performs a TLS handshake with the data provider, after which the user receives a stream of AES masking material. With this masking material, the user is able to send an encrypted request to get their personal data from the provider. The data provider's response is forwarded to the zkPortal main service which verifies and decrypts the response.
  • (2) The final response data received from the data provider will essentially be notarized. At this point, the zkPortal main service has access to the user's full name, date of birth and Nationality. This Personal Identifiable Information (PII) is returned to the user. The problem we now face is, that we do not want to save the PII directly in the backend but rather a pseudonymous identifier. We will achieve this in the next step.
  • (3) The backend hashes the PII and sends it to the zkPortal UID service, which uses a keyed Pseudo-Random Function to transform the PII_HASH to a deterministic yet indistinguishable UID. The UID is returned and stored by the zkPortal main service, after which the PII and PII_HASH are thrown away.
  • (4) From this point onwards, our zkPortal main service knows a private and unique representation of the user's identity (namely, the UID) which can be linked to other identifiers (such as the users cryptocurrency public keys) and which can be used to prove whether or not the unique user participated in voting or airdrops already.

Architecture deep dive

We leverage a Trusted Execution environment called Intel SGX so you can cryptographically verify that our backend infrastructure is running a particular piece of software. SGX is used all around the world, including by Signal for contact discovery. Despite the periodic discovery of security vulnerabilities, the organisation (Intel) behind SGX handles them professionally and rapidly shares updates as soon as either theoretical or practical flaws are found. We will always make it possible for you to check that we are running the latest patched version to make sure we are not susceptible to previously found issues. Nonetheless, we want to make it even more robust in the future by switching to an MPC model, more on that later. Our backend code is written in Golang where we use the wonderful project EGo that transforms our Go code into SGX compatible binaries which we then can run.

In order to trust that the zkPortal mobile app is not leaking your personal data, there are three pieces to the puzzle you need to verify: A) what software are the SGX backends running? B) which services do the SGX backends communicate with? C) does the zkPortal mobile app only communicate with SGX?

We will answer all questions in the sections below.

A) what software are the SGX backends running?

To save you some work, we'd like to share that we will work with independent auditors to attest to which source code is running exactly in our backend. However, we believe it is essential to keep available the opportunity to let anyone verify whether they can trust our infrastructure!

SGX gives us the guarantee (based on the assumptions mentioned in the previous paragraph) that only a specific piece of code is running. You do not need an SGX-enabled machine to verify what software we are running, verification of what our backend is doing is available to everyone! If you really want to dig deep into the attestation process of SGX you can read more about it here.

You can verify the code running on our backend as follows. Soon ™ you can download our source code from Github: You can familiarise yourself already by following the instructions here. We will host this howto in the future ourselves with proper links to the source code. When you run the docker build command it will output a unique ID for the source code you just compiled. If you look at the Dockerfile you will see that the build script checks out our backend code, compiles it and it spits out our unique ID. For the next step you have to make a connection to our SGX servers, you can find them at the IPs: or To make it even easier easy for you we will provide a tool in the near future. But you can already attest today to the fact that you are talking to an SGX instance and it provides the unique ID which links it back to the source code you can see on Github.

Now you have a full audit trail from open source code to compiled code which is running on SGX and SGX attesting to the fact which code is running. You can verify that PII is only ever stored in memory, as it is quickly transformed to an unrecognisable UID by the zkPortal UID service. No private data is ever stored in our database.

Code-bound secrets

Note that it should not be possible for anyone to access the zkPortal UID service's secret key, which it uses to derive UIDs from a user's PII_HASH. For this purpose, we use SGX's “Unique Seal Key”, which is bound to both CPU and code. That means if you either change the code or the CPU you will not be able to recover the UID service's key, all data is irrevocably lost. The Unique Seal Key allows us not to break our promise in the future but also prohibits any changes to the code.

Loading certificates during boot

If you have checked our code and we really hope you did, you will see sth peculiar during the boot process into SGX. We are loading certificates into SGX. Those certificates are for access rights to our database, if we would put them into the source code we would have opened up a huge security issue as everybody would be able to access the database. Therefore we need to keep those secret, but if you look further in the code you will see that information that is loaded via a local unix socket is merely used as reading some bytes to authenticate with the database. This setup allows us to have a proper certificate based authentication with our database while still having reproducible builds to be able to verify what data is running on SGX.

B) which services do the SGX backends communicate with?

The zkPortal UID service is stateless, and is therefore in principle open to communicate with any service from the outside world. That leaves us to only explain why the zkPortal stateful backend only shares PII with the zkPortal UID service, and not elsewhere. Well, the zkPortal UID service can leverage reproducible builds and SGX to prove to other parties which connect to it that it is running a particular piece of code.

A problem with forcing our backend infrastructure to only communicate with a specific version of the zkPortal UID service, is that it makes updates hard. This might be familiar to people in the cryptocurrency space, where updating "immutable" smart contracts in a secure fashion often turns out to be a very hard problem. Fortunately, any security updates to SGX itself do not reflect the reproducible build mentioned above. Moreover, our zkPortal UID service is so simple, and leverages such stable algorithms, that it is unlikely to frequently require updates.

C) does the zkPortal mobile app only communicate with SGX?

Now that you have verified the hash of the code that is running on the SGX you still need to verify that the client (the zkPortal mobile application) is actually communicating only with the SGX instances defined above. This can easily be done with sniffing the traffic between the mobile phone and the internet using a tool like Wireshark. We will provide more detail and what to do in the near future.

Even more security: Multi-Party Computation

In the future, we will leverage Multi-Party Computation, in order to ensure no single TEE can cause the user’s data to leak. Each TEE holds only a portion of a TLS session’s private key, requiring all TEE nodes to collude in order to break integrity and privacy. This addresses two threat models: the unbribable hardware security of TEEs is therefore enhanced by the human security of MPC. If new serious flaws in TEEs are discovered, the human involvement in the MPC makes it possible for us to apply patches without significantly reducing security. This is the most secure approach that is possible given currently available technologies.

Audit trail

This section describes the security precautions at each step of user's interaction from creating an identity to sharing it with 3rd-party services.

The audit trail shows that user's personal data wasn't tampered with by the client and comes from a trusted source. As we do store the PII (personal identifiable information) on the users device we need to safeguard that only the data we have provided is used in proofs.

The trail

1. User creates an identity

User creates an identity by communicating to the identity source and TEE backend. During the process, backend attaches a TEE attestation report to the identity object before sending it to the user, then discards the identity data - user's device can verify that the identity wasn't tampered with, and can trust that the backend actually discarded the identity.

The trust is based on the guarantees described in the Architecture deep dive section.

2. User creates a zero-knowledge proof of their identity

  1. User's device generates public and private inputs for ZK proof, then creates a ZK proof
  2. User's device sends the proof back along with the identity data, which was attested by the backend in step 1

3. The backend verifies the received data

The backend does the following steps:

  1. reads private inputs from the identity data
  2. recreates public inputs
  3. verifies that the attestation report from step 1 wasn't tampered with
  4. verifies the proof using public inputs

4. The backend signs and attests the proof, then sends it back

  1. Public inputs are signed with an ephemeral ECDSA key, public part of the key is attached to the proof
  2. TEE attestation report is created over the public key and proof's validitiy timestamp - SHA256(PublicKey|ValidUntilTimestamp), then the report is attached to the proof
  3. The attested proof is sent back to the user's device
  4. Proof and identity are discarded

5. User receives the proof and can share it with 3rd parties

To have a complete audit trail on where the data came from, the 3rd party (consumer of the identity proof):

  1. Needs to verify the report, which discloses a hash of public key and validity timestamp. Use it to verify the ECDSA signature of proof data and proof expiry. That guarantees that the 3rd party can trust the inputs the user used on their device to create a ZK proof.
  2. Needs to verify the ZK proof. See ZK proof verification section for more information.
  3. If you want to go an extra mile, your consumer can also verify the code running on the backend in TEE by comparing UniqueID in the report with UniqueID you can get from reproducible builds.

SGX Report breakdown

SGX report is used to verify that the signing authority is rooted to a trusted authority such as the enclave platform manufacturer. Practically, it means that you can verify that the code is running in a Trusted Execution Environment. It also includes a number of attributes you, as a consumer of the report, need to verify yourself.


The report data that has been included in the report. This can be any data up to 64 bytes that the enclave signs and attests to.

How do I verify it?

Check the developer's documentation for details about the report data in each specific case.


Enclaves that represent different versions of a module can have different security version numbers. The SGX design disallows the migration of secrets from an enclave with a higher SecurityVersion to an enclave with a lower SecurityVersion. This restriction is intended to assist with the distribution of security patches, as follows.

If a security vulnerability is discovered in an enclave, the developer can release a fixed version with a higher SecurityVersion. As users upgrade, SGX will facilitate the migration of secrets from the vulnerable version of the enclave to the fixed version.

How do I verify it?

Check the developer's documentation and release pages about important security updates.


If true, the report is for a debug enclave. From a practical standpoint, this means that secrets will never be migrated between enclaves that support debugging and production enclaves.

How do I verify it?

Check the developer's documentation if they're running their enclaves in debug mode. Generally, you want to check that debug mode is disabled for production systems.


UniqueID uniquely identifies enclave. It changes if the program changes.

How do I verify it?

Check the developer's documentation and release pages about the version of the software they're running. You can also verify that there were no changes to the source code compared to the published source code thanks to Reproducible builds.


SignerID uniquely identifies enclave's signer. A developer generates a pair of RSA keys, which they use to sign enclaves.

How do I verify it?

Check the developer's documentation to find the signer ID for the software they're running.


ProductID uniquely identifies a Product - changed by the developer to indicate different software modules, which are a part of the same enclave. All the enclaves whose signatures have the same ProductID and are issued by the same RSA key (and therefore have the same UniqueID) are assumed to represent different versions of the same software module.

How do I verify it?

Check the developer's documentation to find the product ID for the software they're running.