The Verifier Backend
In the scope of this part, we will implement a verifier backend server that signs a claim if it is verified.
ZKP and ID Proofs
The Concordium Wallet for Web allows dApps or services to request proof that the user meets some requirement, such as proof the user is over a certain age or resides in a specific set of countries or areas. You can find more info in this link. But basically, when you have a Concordium account you have an ID object and the identity credential in your wallet. On-chain (in your account) there exists a (list of) commitment(s) to your attributes. No one can know who you are other than being able to see your public address. But with the brilliant Zero-Knowledge Proof technology- proving a claim without revealing any information but the claim itself-, any dApp that wants to make sure that its users meet some criteria, the answer to the query which uses a ZK proof to show correctness. We call these criteria statements. These statements can be located in the dApp itself or stored in the verifier which is a backend HTTP server. For this particular tutorialâs scenario it is being older than 18 is the statement that dApps asking for.
When the dApps wants to prove someone meets the criteria, it should first communicate with the verifier. The verifier is one of the key elements of this architecture, basically, dApp uses to verify the claims but it has a key function above all else. The verifier makes sure that a ZKP query cant be reused by someone else for example if it's stolen somehow. When the dApp communicates with the verifier, it asks for a challenge simply a one-time or time-bound random string, that will be used while creating the proofs. The verifier, when doing verification checks whether the proof is created with the particular challenge issued for the query. If the proof is not created with the particular challenge, will not be verified.
When you have your Concordium account, meaning your ID object is created there are a set of attributes that is inside of your encrypted data structure. A full list of the attributes can be found in this here but some of them are listed below.
First name, Last name
Sex
Date of birth
Country of nationality
âĻ
Using the Concordium Web Wallet, our dApp can request proofs for any of these attributes from its users. There is no possible way for us to know anything beyond that the statement doesn't include in the first place. When the user agrees to reveal these pieces of information, he/she will start experiencing true Self-Sovereign Identity. Thatâs the beauty and its the future of Web 3. We will give the control back to the users!
Minting with ID 2.0
Letâs get started with technical implementations. Itâs always good to define the requirements and the steps that will lead you to implement the solution.
We want to use the existing NFT minting tool React application for the sake of time and implementation.
We will implement a verifier backend and re-use as much as possible that is shared by the Concordium team.
Our minting dApp will allow people only older than 18, but we can increase the set of attributes or add new combinations.
Nice, we have a very short requirements list. Now take a look at the flow from the architectural point of view in general.
When the user wants to mint something, dApp goes to the verifier backend and asks for a challenge alongside the statement(s).
The dApp sends a request for proof of the given challenge and statement to the wallet.
The user accepts the requests, wallet sends back the proof.
The dApp sends it to the verifier, it verifies the proof is correct according to the challenge and statement.
The dApp uses the private key of the owner, to sign a message.
The smart contractâs mint() function checks the signature created by the owner and allows for mint.
Verifier Backend
We will use the backend code shared in Concordiumâs GitHub repo in this link. There will be some modifications based on our needs.
First, letâs create an empty project called the backend.
Concordium Rust-SDK
Concordium Rust SDK it's published on crates.io now and you can directly add concordium-rust-sdk = 1 it
your Cargo.toml file. You don't have to clone and install it.
We will use a lot of features that Concordium Rust-SDK provides and we need to install it. Since itâs not published on crates.io yet, you have to clone your code and build it.
Create a folder called deps and then clone the repo inside of it. Run the command below to clone the code and then you will update the submodules with the next one.
For the development, serialization, encryption, and an HTTP running server, we will need some dependencies to add the dependencies below to your cargo.toml file. Letâs go over some of these dependencies.
tokio: A runtime for writing reliable, asynchronous, and slim applications with the Rust programming language.
warp: A super-easy, composable, web server framework for warp speeds.
serde: A very helpful framework for serializing/deserializing data structures generically.
serde_json: A JSON serialization/deserialization file format.
clap: Command Line Argument Parser for Rust (CLAP)
anyhow: Easy error handling trait.
ed25519-dalek: To produce and consume Ed25519 signatures and for other key operations.
And now we can build it with the âcargo buildâ command in the concordium-rust-sdk folder. For Mac users, if you face a protobuf error in this step, you might need to install it manually. Then build it again.
Implementation
Letâs start with the implementation and create a types.rs file. We will use almost the same code in this link. In this file, we will create our data structures, and responses, and manipulate error codes.
First, we have the Challenge struct which is a u8 32 bytes array. This will be re-generated every time a new client connects or the backend gets a request.
WithAccountAddress is used for storing the challenge created for a particular account.
ChallengeStatus is used for storing the issued challenge, to whom its issued(address), and its creation time on the state.
The Server is our state. When we run our verifier backend, we will create an empty state with an empty hashmap of challenges.
The InjectStatementError enum will be used for handling rejections with error codes.
Finally, we have the ChallengeResponse struct which will be used the API responses, ChallengedProof and ProofWithContext. When the backend receives the proof, it will use these to validate it using the client object.
Handlers.rs
Nice, letâs create another file called handlers.rs. In this module, we will explain all functions one by one and add them to the file.
The handle_get_challenge() function gets input as the state and an address. It runs asynchronously when someone asks for a challenge using an endpoint. It invokes the get_challenge_worker() which generates a random 32 bytes message aka Challenge adds it to the stateâs challenges after encoding it the challenge as a key and the address + issuing time as value(ChallengeStatus). As a result, it returns back the challenge as a response to send it back through the endpoint.
The handle_provide_proof() function, gets the client, state, statement, request, and key_pair as input. It serves through an API endpoint and is primarily used for verifying the proof by calling the check_proof_worker() function.
The check_proof_worker() function, validates the cryptographic proof. First, it locks the state and gets the status from the map using the challengeâs base_16 encoded key of the map. Since the request is a ChallengedProof type, we can access the challenge and more than that, it holds the ProofWithContext struct meaning both the credential and the proof is available to use in verification. Similarly, the status is a ChallengeStatus meaning we know the address issued and the time created. We will need these in the next step. And finally, the Statement is a struct that holds a list of atomic statements.
When this function is invoked with a POST request, we will have the request object and we will use it to extract the credential_id. Please note that every account has an account registration ID, which is the Credential ID of the first credential added to the account. Create a variable for the cred_id and letâs get the account information using the Concordium Rust-SDK. The function takes mutable client: concordium_rust_sdk::v2::Client as a parameter. We will get the account information using that. Use the account address from the status, as the first parameter of the client.get_account_info() and the BlockIdentifier::LastFinal as the second param. Basically, this function provides you with the required information for a given account address in the given block. So we give it the LastFinal block means the last finalized block at the time of the query.
Then we will find the credential by simply getting the initial element of the account_credentials map which is the map of all currently active credentials on the account. This includes public keys that can sign for the given credentials, as well as any revealed attributes. A credential contains commitments to these attributes. The map holds the AccountCredentialWithoutProofs which has the InitialCredentialDeploymentValues and CredentialDeploymentCommitments.
The line below from the code snippet makes sure that the credential sent by the user is the same, as the one that the account has.
Then we will get the commitments, which are simply the protectors of the attribute credentials in a way. They are the attributes that the user doesn't want to reveal on the account while creating their wallet. So a user can decide to open certain commitments and reveal the attributes.
There is a great non-cryptographic analogy that explains commitments really well. Assume that you have data that you want to protect from others to see and even from you to change. You put that in an envelope, seal and send it to the public. No one can see it because it's sealed and you can not change it because it's out now.
And finally, we verify the proof with this part and respond back with the result which is the signature. We need the request, global_context, cred_id, commitments, and the proof itself to do that. If it's successful, we can remove the challenge from our map-since it's a one-time thing- and sign the account address (as string) with our private key. We used this approach to create and share the signature but it's also fine to sign any message. In the smart contract, while minting, we would like to verify that the claim is verified and signed with our private key. It may sound a bit complicated but you will understand it better while implementing the dApp.
Main.rs
Dont worry! We are almost there, now we need to wrap it up and create our main program to run our HTTP server that listens to all endpoints required to create and send a challenge, share the statement, and verify the claim. Create a file called main.rs. We will use warp to run an async HTTP server in a few easy steps as already mentioned and definitely need the handlers.rs and types.rs to invoke the helper functions and the data structures.
We need to create a struct called IdVerifierConfig that accepts command line parameters while running the application. First, it should have a node endpoint to build & configure HTTP/2 channels. (which gRPCv2 uses to stream). Second, we will need a port for our server to listen and a logger which we will use the log crate. And finally, we will give the statement, verify_key and sign_key (the keys we get from our exported wallet file) as parameters in string form. Please note that for all parameters we specified some default values with the clap.
As the final step, we will add our main() function. Letâs add #[tokio::main] macro just before main. It transforms the async main() function into a synchronous main() function that initializes a runtime instance and executes the async main() function.
First we need to parse the params given as input while running the executable. After initializing the log file, we serialize the statement (see the concordium-rust-sdk for more details), create a client and get the latest cryptographic parameters which are public and our global_context until the finalized last block from Concordium. (or the request made) Create a state variable(initiate it) with empty challenges, and the global context.
Get Challenge
cors is a standard in HTTP related to the permissions to access and manage a website we set our server's settings, and then implement the first endpoint which is the getting challenge. So when someone wants to get a randomly generated challenge for his address, he/she must call this endpoint. We will get the address from the query payload and invoke handle_get_challange(). Since we don't need input this is a GET function that is available at âlocalhost:8000/api/challengeâ, and we will use the same channel when the challenge is generated, and stored on the state in a map with its key like (base16 encoded version, <address, time>)
Get Statement
The second endpoint is the get statement, when our dApp wants to verify that a user meets some conditions, it needs to know what conditions they are, we will just answer back the statement from our input variables using the same channel. We don't need input so this is also a GET endpoint that is available at âlocalhost:8000/api/statementâ
Prove
The last endpoint is the prove. Basically, the request that dApp posts, (so this is a POST endpoint) include the challenge and the proof. We will send it to the handle_provide_proof() helper function to proof and sign it. In order to sign it, we need to re-create our key pair which are created using our verify_key and sign_key. Basically, when the proof is verified, this endpoint sends back a signature (the public key of the user signed by the backendâs private key) that can be verifiable in the smart contract. Then the user will be able to mint the token because the signature will be verifiable by the smart contract using the public key of the backend address.
Last updated