# ZK Login Authentication Flow

## Overview

Aсki Naсki employs an Zk-auth authentication scheme for conducting Multi-factor Wallet smart-contract transactions. Zk-auth is based on the OpenID Connect protocol and zk-SNARK system Groth16. It combines convenience for users and security. Transactions are confirmed via existing OpenID credentials. Multiple OpenID based providers like Google, Facebook, Apple etc are already supported by Zk-auth, and we are going to extend the list. The anonymity is preserved. Blockchain accounts and OpenID accounts are not publicly linked. This is achieved using zk-SNARK.

Zk-auth allows for quick authentication. The user is not burdened with remembering cumbersome seed phrase in the majority of cases. But we still use seed phrase for recovery.

Our scheme is inspired by [zkLogin](https://arxiv.org/abs/2401.11735).  But we modify it by removing extra service for salt back up since the corrupted salt service may deanonymize the link between blockchain account and OpenID account. To prevent possible privacy violations we replace the server-generated salt with a user-owned Password that is self-maintained by the user.\
Also, we add the ability to recover access to Wallet smart-contract in accidental cases like the loss of device or OpenID credentials.

Our multi-factor authentication scheme allows users not to input any extra information to confirm transactions while the JWT token is valid and not expired. So the user story is rather simple. At the same time, an attacker who compromised OpenID credentials cannot transact unless he separately compromised the user-owned Password.

If the user has lost the device and Password, he still will be able to restore access and at the same time to prevent the adversary from using the wallet.

Let’s summarize the properties that Zk-auth provides:

* In the majority of cases, one may transact on Acki Nacki using the familiar OpenID authentication flow. However, we do not eliminate the necessity in mnemonics to provide the ability to recover access to Wallet.
* Transaction requires approval from the user via the standard OpenID credentials, but the OpenID provider (or attacker who compromised an OpenID account) cannot transact himself, pretending to be the user. This is provided by extra user-owned Salt Password and by the fact that the OpenID provider does not know the matching between blockchain accounts and OpenID accounts. The last one is achieved using zero-knowledge proofs.

{% hint style="info" %}
At the present moment we support the following list of OpenID providers: Google, Facebook, AWS, Twitch, Apple, Slack, Kakao, Microsoft, KarrierOne, Credenza3.
{% endhint %}

## Multi-factor Wallet initialization

To initialize a Multi-factor Wallet, the user must have valid OpenId credentials (for example related to Google email). Also, there are some extra secrets that the user creates during signUp to handle the access to Multi-factor Wallet: Salt Password, Recovery Password and Seed Phrase.

At first the user creates the **Salt Password**. It must be simple and convenient to remember and use it. At second the user creates a strong **Recovery Password**, meeting certain security conditions: be long enough and contain both digits and special characters. Finally **Seed Phrase** is generated by a Client Application exploited by the user. All this data is produced just before Multi-factor Wallet smart-contract deploying, and it is used for deployment.&#x20;

{% hint style="danger" %} <mark style="color:red;">The user should store</mark> <mark style="color:red;"></mark><mark style="color:red;">**Salt**</mark> <mark style="color:red;">**Password, Recovery Password,**</mark> <mark style="color:red;"></mark><mark style="color:red;">and your</mark> <mark style="color:red;"></mark><mark style="color:red;">**Seed Phrase**</mark> <mark style="color:red;"></mark><mark style="color:red;">in a secret place.</mark>&#x20;
{% endhint %}

Directly before Multi-factor Wallet smart-contract deployment, Seed Phrase and Recovery Password are used by Client Application to derive corresponding ed25519 keypairs (*SK\_SeedPhrase, PK\_SeedPhrase*) and (*SK\_Recovery, PK\_Recovery*). Also, Poseidon hash *zkID* is computed based on OpenID account data (stable id) and user-owned Password.  *zkID* hash is used to link Multi-factor Wallet smart-contract and OpenID account, but anonymously.&#x20;

So for deployment of Multi-factor Wallet smart-contract the following triple is prepared: hash *zkID*, ed25519 public key *PK\_SeedPhrase* and  ed25519 public key *PK\_Recovery*.

{% hint style="warning" %}
This Client Application does not backup user-owned Password, Recover Password, Seed Phrase and related secret keys. It will store only fresh JWT token related to OpenId Connect, zero-knowledge proof and some extra data, which we will discuss in more detail below.
{% endhint %}

{% hint style="info" %}
Issuer value on the diagram below is a string identifying OpenId provider like Google, Facebook etc.
{% endhint %}

<figure><img src="https://2264326627-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FahUdZU2HOHQjENWKWyJo%2Fuploads%2FlrUrCECQWVMfggVaPfIx%2FzkStart.drawio.png?alt=media&#x26;token=d7a69b71-27f7-4cf5-8ea9-fc745a055e69" alt=""><figcaption></figcaption></figure>

## Transactions Authentication sketch

Key aspects of transacting with OpenID credentials:

* A **JWT** is a signed access token obtained from an OpenID provider, containing a payload that includes a field named 'nonce'. We add into nonce: user’s temporary ephemeral public key, timestamp of its expiration and some extra randomness.
* **Client Application** generates and stores the temporary ephemeral key pair, where the ephemeral public key is added into nonce of JWT. The ephemeral private key is used to sign transactions during some predetermined period of time (\~ 2 weeks), eliminating the need for the user to back it up.
* The Groth16 zero-knowledge proof is generated based on JWT. The aim is to prove that the user really owns an OpenID account, i.e. has a related signed valid JWT. However, JWT contains fields deanonymizing users. Thus we generate zero-knowledge proof per JWT to hide some JWT fields.
* A transaction is submitted on-chain being signed by an ephemeral secret key and supplied with valid zero-knowledge proof. TVM executes the transaction after verifying the ephemeral signature and the zero-knowledge proof.

## Main Entities

* **Application frontend (Client Application)**: This is the Client frontend application that supports our flow to create and authenticate transactions. Client Application is responsible for deploying Wallet smart-contract with valid user's data, storing the ephemeral private key, maintaining OpenID, creating and signing transactions.
* **Proof Service**: This is a backend service responsible for generating zero-knowledge proofs based on JWT, extra randomness, Salt Password and expiration timestamp (for ephemeral keypair). The proof is submitted on-chain along with the ephemeral signature for the transaction.

{% hint style="info" %}
The delegation of zero-knowledge proof computation to the extra Proof Service backend is a necessary step for now. This is motivated by the fact that the protocol deals with non-ZK-friendly cryptographic primitives: SHA-2, modular exponentiation for RSA signature verification. It causes an essential number of R1CS constraints in the corresponding circuit that was written for Zk-auth protocol in Circom language. Our circuit was essentially inspired by [zkLogin](https://docs.sui.io/concepts/cryptography/zklogin) existing  implementation. We did only very small optimizations for the part related to JWT token parsing. But the circuit is still cumbersome and has about 2^20 constraints. This makes the proof computation impractical in Client Application. That’s why following the experience of [zkLogin](https://docs.sui.io/concepts/cryptography/zklogin) we chose to delegate proof computation to a powerful service. The corruption of Proof Service (corruption = control by the adversary) will lead to deanonymization immediately. The adversary in this case gets access to JWT token and user-owned Salt Password, he can calculate *zkId* and discover the link between blockchain and OpenID  accounts. However user Wallet assets are still safe and can not be maintained by the adversary. Since the related JWT token ephemeral private key  is still hidden, and the adversary can not create a valid signature for the transaction.  Only corrupting both Proof Service and OpenID provider is required to steal the Wallet. Since in this case the adversary may create a valid JWT token for his new independent ephemeral key pair and then he will be able to provide valid proof per JWT and sign the transaction by his ephemeral private key.

Since the problem of deanonymization exists in the case of Proof Service corruption, we continue researching the possibility to compute proofs on the client's side. Some extra circuit optimizations are required to reduce the number of constraints. This problem is still under review.
{% endhint %}

## Keys priorities and loss case handling

(***SK\_SeedPhrase, PK\_SeedPhrase***) – master key pair that is used to maintain Multi-factor Wallet smart-contract. It is used for recovery. Knowledge of *SK\_SeedPhrase* (Seed Phrase) allows one to change *zkID* or *PK\_RecoveryPassword* in contract.

{% hint style="warning" %}
To change *PK\_SeedPhrase* in contract one should have: access to OpenID account, Salt Password, Recovery Password.
{% endhint %}

We have protection against several of the most likely accident scenarios of loss.

* **If mobile device and/or access to OpenID account and/or Salt Password are lost**, then use Seed Phrase to change *zkID* in Multi-factor Wallet smart-contract.
* **If Recovery Password is lost**, then use Seed Phrase to replace  *PK\_RecoveryPassword* by fresh  *PK\_NewRecoveryPassword* in contract.
* **If Seed Phrase is lost**, then the user must have a mobile device with not yet expired JWT, related zero-knowledge proof and Recovery Password. It allows one to change PK\_SeedPhrase in contract.
* **If Seed Phrase is lost and mobile phone is lost (or JWT is expired)**, then to change *PK\_SeedPhrase* the user needs OpenID account access, Salt Password and Recovery Password.

{% hint style="info" %}
For now, it is the responsibility of a user to make a strong Recovery Password and backup it. The ideal Recovery Password is the second Seed Phrase, but this is too cumbersome. So our requirements for the Recovery Password are more lightweight.
{% endhint %}

{% hint style="info" %}
(*SK\_RecoveryPassword, PK\_RecoveryPassword*) is a key pair that is used only in the case the Seed Phrase was lost by the user.
{% endhint %}

## Multi-factor Wallet transactions maintenance details

### Transaction WITH signIn to OpenID provider

At the first time the user starts with signIn to the relevant OpenID account. To make a signIn request, the user generates an ephemeral random temporary ed25519 key pair (*SK\_e, PK\_e*). Public key *PK\_e*, its expiration timestamp *T\_max* and extra generated randomness *R* are concatenated and the concatenation is hashed using Poseidon hash function. The hash is put into a 'nonce' field that is added into a semi-finished JWT token prepared by Client Application. JWT payload is sent to the OpenID provider together with standard  authenticating data. OpenID provider authenticates the user,  signs JWT payload with public fresh JWK RSA private key and sends signed JWT back. Signed JWT is used as a certificate for *PK\_e* issued by an OpenID provider.

Since we want to provide anonymity, we can not send JWT into Wallet smart-contract to prove that the user is a valid owner of an OpenID account embedded into both JWT and *zkID* previously stored by contract. Instead, we produce zero-knowledge Groth16 proof to prove that the user really got such JWT. And the contract verifies the zk-proof.

We suppose that the Client Application will run on a device having small computational power. Groth16 proof calculation is computationally hard, that's why we can not handle it on mobile devices. We deploy our own Proof service for computing proofs. Client Application sends a request to Proof service providing as input JWT and Salt Password. Private input to calculate zk-proof contains the following data: signed JWT, Salt Password, extra randomness *R* used for nonce computation. Public input consists of ephemeral public key *PK\_e*, its expiration timestamp *T\_max*, OpenID provider public RSA JWK key, *zkID*. The Proof service generates zero-knowledge proof for a related Zk-auth arithmetic circuit (AC) that takes aforementioned private and public inputs. Zk-auth AC does the following computations:

* partially parse JWT token (payload);
* checks that `nonce` claim  in JWT is correctly formed, \
  i.e. `nonce = Poseidon(PK_e || T_max || R)`;
* checks that `iss` claim in JWT contains the valid OpenID provider name;
* verifies the RSA signature (third part of JWT token) that was done by OpenID provider using his private JWK key for this JWT (recall that JWT header and payload of JWT are signed by provider using RSA private key).&#x20;
* checks that *zkID* is correct, \
  i.e. *`zkID`*` ``= Poseidon(stable id || issuer || Salt Password)`

{% hint style="info" %}
`iss` claim in JWT token is constant identifying OpenId provider. \
For example, for Google `iss` claim equals to "<https://accounts.google.com>".
{% endhint %}

{% hint style="info" %}
The extra randomness *R* during nonce computation is added to strengthen unlink ability between blockchain and OpenID accounts. The data about ephemeral public keys is publicly available in blockchain. More particularly, Wallet contract stores *zkID* and fresh ephemeral public key. Hence, an honest but curious OpenID provider knowing stable ids of all his users can carry out dictionary attack to deanonymize blockchain accounts.

Groth16 zk-proof computed by Proof Service is sent to Wallet smart-contract to authenticate the user. To prevent the case of malicious Proof service pretending to be the user and some other possible attacks, an extra step into the authentication process is added. The user must sign a message sent into the Wallet smart-contract by *SK\_e* that only the user knows.
{% endhint %}

{% hint style="info" %}

There is a single public key pair (*proof\_key, verify\_key*) generated during the trusted setup phase called Powers-of-tau ceremony, which we describe in a separate document. This key pair is generated only once and depends on a related Zk-auth AC that we briefly discussed already. This key pair is not a secret and used for all clients later.  Proof service uses *proof\_key* (that is the same for everyone) to generate proofs. In the meantime, *verify\_key* (that is also public) is embedded into TVM that has a  respective instruction VERGRTH16, using this key to verify zero-knowledge proofs. The last instruction is used by the Multi-factor Wallet smart-contract.

The described process is fulfilled by the user at the first time. And then each time when the keypair (*SK\_e, PK\_e*) becomes expired, the user must relogin to get a fresh JWT for the new *PK\_e*. But until *PK\_e* is not expired, Client Application must store and use JWT and related zero-knowledge proof generated by Proof service. The frequency of OpenID relogin is regulated by us, since we choose *T\_max* ourselves and completely ignore standard `exp` claim in JWT that is set by the OpenID provider.&#x20;

Let’s summarize how a transaction is conducted at the first time (or if an ephemeral key pair/JWT  is expired).

* The user makes a signIn in an OpenID account, gets signed JWT  and requests zk-proof for it from Proof service using Salt Password.
* The user sends to the Multi-factor Wallet contract fresh zk-proof and related *PK\_e, T\_max*, OpenID JWK RSA public key data.
* Multi-factor Wallet contract checks that *T\_max* is not expired valid Unix timestamp. Then it verifies zk-proof using VERGRTH16 instruction. The public data used for zk-proof verification: *PK\_e, T\_max*, OpenID JWK RSA public key and *zkID*. If *T\_max* is not expired  and zk-proof is valid, then the Wallet contract saves a pair (*PK\_e, T\_max*) into mapping \_factors.
* The user sends a message/transaction to Wallet to transfer some amount. The message is signed by *SK\_e*. Multi-factor Wallet contract checks that the related public key *PK\_e* was previously added into mapping *\_factors* and its timestamp *T\_max* is not smaller than the latest block time. If this is true, then transfer will be done.
  {% endhint %}

{% hint style="warning" %}
The secret key *SK\_e* is stored in local storage in the browser or secure storage/element in a smartphone, locked by standard passkey.
{% endhint %}

{% hint style="info" %}
The *T\_max* timestamp aimed to limit operation time of ephemeral keypair (*PK\_e, SK\_e*). Using the parameter *T\_max* we maintain the required reasonable frequency of OpenID relogin to handle Wallet transactions. We must keep a balance between security and user comfort. It’s reasonable to choose *T\_max* quite big to avoid cumbersome using experience to relogin too often. But the user always has an option to add/change ephemeral keypair at any moment of time. At the same time multiple ephemeral key pairs could be handled by the Wallet contract for the same OpenId account (for example each key pair per new device).
{% endhint %}

<figure><img src="https://2264326627-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FahUdZU2HOHQjENWKWyJo%2Fuploads%2Fkv5Tn7yLoecxnpYyPrth%2FNormal%20operating.png?alt=media&#x26;token=b1a0379d-550e-4bea-bdaa-dab389238a6e" alt=""><figcaption></figcaption></figure>

### Transaction WITHOUT signIn to OpenID provider

This is the case when the user has not expired ephemeral key pair (*SK\_e, PK\_e*), for which public key *PK\_e* was previously added into the Multi-factor Wallet contract (like we described above). Then the user sends only a message signed by *SK\_e*. Multi-factor Wallet contract checks that the related public key *PK\_e* was previously added into mapping \_factors and *T\_max* is fresh. If this is true, then the message will be accepted by contract.

{% hint style="info" %}
We minimize the number of VERGRTH16 instruction calls to achieve the best transaction performance. VERGRTH16 is quite cumbersome. So the user calls it only once for a fresh JWT zk-proof. The Wallet contract validates zk-proof. If it’s ok, then it saves the related ephemeral public key and the time of its expiration. Then the user only sends to contract messages signed by ephemeral secret key. The contract checks that the key is present, not expired and the signature is valid.
{% endhint %}

<figure><img src="https://2264326627-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FahUdZU2HOHQjENWKWyJo%2Fuploads%2FuqDRuGtObjU5U8kRlCVF%2FNormal%20operating%20no%20relogin.png?alt=media&#x26;token=b934dbb2-781e-4876-9143-77f2adf66337" alt=""><figcaption></figcaption></figure>

## More details on handling OpenID via zero-knowledge proofs

In Acki Nacki blockchain we allow users to login into their Wallets with OpenID accounts credentials. For this we use JWT tokens obtained after successful authentication from Google, Facebook and other major services supporting OpenID. We do not reveal JWT tokens themselves and therefore do not leak access to the original service and preserve anonymity. This is achieved through a zero-knowledge proof protocol that provides blind verification of the properties of JWT tokens. We use Groth16 over the elliptic curve BN254, a non-interactive zero-knowledge proof verification system.

### OpenID and JSON Web Tokens (JWTs)

We use the OpenID protocol. In this protocol a user can log into a trusted third party (Google, Facebook, etc.) and get a signed access token attesting that they logged in the form of a signed JSON Web Token (JWT). A signed JWT looks like three base64-encoded payloads separated by a dot:

<figure><img src="https://2264326627-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FahUdZU2HOHQjENWKWyJo%2Fuploads%2FbZuAqxnuNS5jOLt3NADb%2Fp4.jpg?alt=media&#x26;token=33e843c0-b5c8-4b20-aee2-0d4badd7b849" alt=""><figcaption></figcaption></figure>

When decoded, the first part of the payload is a header, the second is the JWT's content itself (called the payload), and the third one is the signature that is done by the OpenID provider secret JWK key. One can use the debugger on jwt.io to inspect such JWTs:

<figure><img src="https://2264326627-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FahUdZU2HOHQjENWKWyJo%2Fuploads%2FAFCqYvA2ZGxryLedzzrz%2Fp5.jpg?alt=media&#x26;token=71929c80-77a1-463f-b607-b04f51ce8d91" alt=""><figcaption></figcaption></figure>

There are the following important fields in the JWT payload :

* the issuer `iss` field, indicates who issued and signed the JWT.
* the audience, `aud` field, indicates who the JWT was meant for.
* the subject `sub` field, represents a unique user ID (from the point of view of the issuer) who the JWT is authenticating.
* the `nonce` field contains a user nonce for the application to prevent replay attacks.

### Verifying JWTs

To verify a JWT, one needs to verify the signature over the JWT. To verify a signature one must know the public key of the issuer of the JWT. All issuers have a published JSON Web Key Set (JWKS). For example, Facebook's JWKS can be downloaded from <https://www.facebook.com/.well-known/oauth/openid/jwks> and looks like the picture below.

<figure><img src="https://2264326627-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FahUdZU2HOHQjENWKWyJo%2Fuploads%2FBfapdCzH1Mii9vu2Tzxu%2Fp6.jpg?alt=media&#x26;token=6b7f3927-57eb-40ef-86a8-321dcf211364" alt="" width="543"><figcaption></figcaption></figure>

JWKS contains several JSON Web Keys (JWKs) identified by their key ID `kid`. Several keys are often displayed to provide support for key rotation. Since this information is external to the JWT, the network must know who the issuer is, and specifically `kid` that was used to issue the JWT.

Since the issuer of a JWT is contained in the payload, not in the header, the Zk-auth circuit (described below) must extract this value and witness it in its public input.

### Zk-auth arithmetic circuit

Here we discuss what the Zk-auth circuit does at a high level. Given the following public input:

* the issuer `iss` field (that we expect to find in JWT);
* the RSA public key of the issuer.

It extracts the following as public output:

* the ephemeral public key contained in the `nonce` field of the JWT, as well as expiration information;
* *zkID* value introduced before, which is a hash linking user's OpenID account (stable ID) with blockchain address;
* the header of the JWT (which the network needs to validate, and also contains the key ID used by the issuer)
* the audience `aud` field of the JWT.

Zk-auth circuit in addition to extracting above public outputs performs the following:

* It inserts the actual JWT in the Zk-auth circuit as a private witness.
* It checks that the issuer passed as public input is indeed the one contained in the JWT.
* It hashes the JWT with SHA-256 and then verifies the signature (passed as private input) over the obtained digest using the issuer's public key (passed as public input).
* It derives *zkID* value deterministically using the Poseidon hash function and the user identifier (e.g., an email) as well as some user randomness.

The signature is verified in zk-auth circuit to avoid issuers from being able to track users on-chain via the signatures and digests.

The idea at this point is for the network to make sure that, besides the validity of the zk-proof, the address is strongly correlated to the user.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.ackinacki.com/for-users/wallets/zk-login-authentication-flow.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
