ZK Login Authentication Flow
Last updated
Last updated
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 . 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.
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.
The user should store Salt Password, Recovery Password, and your Seed Phrase in a secret place.
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.
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.
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.
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.
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.
(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.
To change PK_SeedPhrase in contract one should have: access to OpenID account, Salt Password, Recovery Password.
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.
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).
checks that zkID is correct,
i.e. zkID
= Poseidon(stable id || issuer || Salt Password)
The secret key SK_e is stored in local storage in the browser or secure storage/element in a smartphone, locked by standard passkey.
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.
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.
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:
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:
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.
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.
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.
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.
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 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 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.
iss
claim in JWT token is constant identifying OpenId provider.
For example, for Google iss
claim equals to "".