The Verifier Backend
This section will show you how to implement a verifier backend server that signs a claim if it is verified.
Before You Begin
This section will guide you through setting up and implementing the backend infrastructure required to verify user credentials in your dApp.
To follow along effectively, you'll need to understand the following: the authentication process you'll use, an overview of the backend implementation, and the workflow you'll follow, which are explained below.
ZKP and ID Proofs
The Zero-Knowledge Proof (ZKP) functionality allows dApps or services to request proof from users that they meet specific criteria, such as age or residency, without disclosing additional sensitive information beyond the specific claim being verified.
By integrating ZKP technology into the authentication flow, the verifier backend can securely handle these requests, ensuring user privacy and data integrity while enabling adequate verification of user credentials.
Overview
Here is an overview of key components in the authentication process within the Concordium blockchain ecosystem:
Identity Credentials: Every Concordium account includes an ID object and associated identity credentials essential for user verification.
Commitments to Attributes: Within user accounts, there are on-chain commitments to attributes. These commitments maintain user privacy while still allowing for attribute verification.
Verifier Role: The verifier is a crucial component that ensures the integrity of ZKP queries and prevents unauthorized reuse.
Workflow
The following workflow outlines the steps involved in the interaction between the decentralized application (dApp), the user's wallet, the verifier, and the smart contract.
It ensures secure and verified interactions between the various components involved in the authentication and authorization process.
Requesting a Challenge: The dApp communicates with the verifier to obtain a challenge, a one-time or time-bound random string crucial for creating proofs.
Creating Proofs: Using the challenge, the dApp requests proofs from the wallet for specific statements, such as age verification.
User Consent and Proof Generation: The wallet generates and provides the necessary proofs upon user consent.
Proof Verification: The dApp submits the proofs to the verifier for verification, ensuring correctness based on the challenge and statements.
Signing with Private Key: Utilizing the owner's private key, the dApp signs a message.
Contract Interaction: Based on the validated proofs, the smart contract's mint() function verifies the signature and authorizes minting.
Minting with ID 2.0
The tutorial involves setting up a backend system to enable identity verification during the NFT minting process. The backend verifier is crucial for authenticating users before they can mint non-fungible tokens (NFTs). Learn about the requirements and architectural flow below:
Requirements
Utilize the existing NFT minting tool, the React application.
Implement a verifier backend, leveraging shared components from the Concordium team.
Restrict minting to users aged 18 and above, with the flexibility to incorporate additional attributes or criteria.
Architectural Flow
The steps involved in integrating identity verification into the minting process are outlined below:
Request Challenge and Statements:
The dApp requests a challenge and relevant statements from the verifier backend.
Proof Request to Wallet:
Based on the challenge and statements, the dApp seeks proofs from the wallet.
User Consent and Proof Generation:
Upon user approval, the wallet generates the required proofs.
Verification by Verifier:
The dApp submits the proofs to the verifier for validation against the challenge and statements.
Signature and Contract Interaction:
Using the owner's private key, the dApp signs a message, facilitating interaction with the smart contract's mint() function.
You will use the backend code shared in Concordiumâs GitHub repository. There will be some modifications based on your needs.
To set up the Verifier Backend, use the following steps:
Step 1: Create a New Project
Run the following command to create a new project:
This command initializes a new Rust project named "backend":
Step 2: Install Concordium Rust-SDK
The Concordium Rust SDK, now available on crates.io, streamlines the integration process for Concordium-related functionalities. Here's how to install it:
Direct Installation from crates.io: Add the following line to your
Cargo.toml
file:
Manual Installation: While the SDK is not yet published on crates.io, manual installation is required. Follow these steps to clone and install the SDK:
On successful installation, you will see the following in your terminal:
Step 3: Install Dependencies
To develop serialization, encryption, and an HTTP server, add these dependencies to your "Cargo.toml" file:
tokio: Asynchronous runtime for Rust.
warp: Lightweight web server framework.
serde: Serialization/deserialization framework.
serde_json: JSON file format support.
clap: Command Line Argument Parser for Rust.
anyhow: Simplified error handling.
ed25519-dalek: Ed25519 signature support.
Add them as follows:
These dependencies include the necessary libraries for building the verifier backend.
Step 4: Build with Concordium Rust SDK
To build the Concordium Rust SDK, follow these steps:
Navigate to the Concordium Rust SDK Folder: Access the Concordium Rust SDK folder in your terminal or command prompt.
Execute Build Command: Run the following command to initiate the build process:
Handling Protobuf Error (For Mac Users): If you encounter a protobuf error during the build process on macOS:
To fix the error, you may need to install protobuf manually:
After installing protobuf, rerun the build command:
Step 5: Create "types.rs" File
Create a "types.rs" file to define the following data structures and responses:
Challenge Struct: Represents a 32-byte array ([u8; 32]) and is regenerated for each new client connection or request the backend receives.
WithAccountAddress: Used for storing the challenge created for a specific account.
ChallengeStatus: It stores the issued challenge, the address to which it's issued, and its creation time.
Server State: Represents the state of the server. Upon running the verifier backend, an empty state with an empty hashmap of challenges is created.
InjectStatementError Enum: Handles rejections with error codes. Includes variants such as
NotAllowed
,InvalidProofs
,NodeAccess
,LockingError
,UnknownSession
, andCredential
.ChallengeResponse Struct: Used for API responses, alongside
ChallengedProof
andProofWithContext
. When the backend receives the proof, it utilizes these structures to validate it using the client object.
Implement the data structures by adding the following code to your "types.rs" file:
You can refer to this GitHub repository for the code implementation
Step 6: Create "handlers.rs" File
Create another file called "handlers.rs" and add the following functions:
Add handle_get_challenge
Function
handle_get_challenge
FunctionThe handle_get_challenge()
function gets input as the state and an address. It does the following:
Runs asynchronously when someone asks for a challenge using an endpoint.
Invokes the
get_challenge_worker()
which does the following:Generates a random 32-byte message (Challenge).
Adds it to the stateâs challenges after encoding it.
Returns the challenge as a response and sends it back through the endpoint.
Implement the handle_get_challenge()
function with the following code:
Add handle_provide_proof
Function
handle_provide_proof
FunctionThe 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 to verify the proof by calling the check_proof_worker()
function.
Add check_proof_worker()
Function
check_proof_worker()
FunctionThe check_proof_worker()
function validates the cryptographic proof by locking the state and getting the status from the map using the challengeâs base_16 encoded key of the map.
You'll implement the following functionalities in the check_proof_worker()
function:
ChallengedProof Type Properties:
The request object is of the type ChallengedProof.
Accessing the ChallengedProof allows access to the challenge and the ProofWithContext struct.
The ProofWithContext struct contains both the credential and the proof for verification.
Similarly, the status object is of type ChallengeStatus, providing information about the address issued and the time created.
Handling POST Request:
The request object is available when the function is invoked with a POST request.
Extract the credential_id from the request object.
Every account has an account registration ID, the Credential ID of the first credential added to the account.
Fetching Account Information:
Create a variable to store the credential_id.
Use the Concordium Rust-SDK to fetch account information.
The function takes a mutable client (
concordium_rust_sdk::v2::Client
) as a parameter.Get the account information using the account address from the status.
Use
client.get_account_info()
with the account address andBlockIdentifier::LastFinal
as parameters.This function provides information about a given account address in the specified block.
Finding Credential:
Find the credential by accessing the initial element of the
account_credentials
map.The
account_credentials
map holds all currently active credentials on the account.Credentials include public keys to sign for the given credentials and any revealed attributes.
A credential contains commitments to these attributes.
The map holds the
AccountCredentialWithoutProofs
type, which includesInitialCredentialDeploymentValues
andCredentialDeploymentCommitments
.
To implement these functionalities for your check_proof_worker()
function, use the following code:
The line below from the code snippet ensures that the credential sent by the user is the same as the one the account has.
Commitments
Commitments are the attributes the user doesn't want to reveal on the account while creating their wallet. Therefore, a user can open specific commitments and reveal the attributes.
To understand commitments better, use this analogy: Assume that you have data that you want to protect from others seeing, and even from yourself changing. You put that data in an envelope, seal it, and send it to the public. No one can see it because it's sealed, and you cannot change it because it's out now.
The code below shows how the "commitments" is defined:
Step 7: Verify the proof
Follow the steps below to verify the proof:
Use the
verify
method of thestatement
object to verify the cryptographic proof.Pass the challenge, global context, credential ID, commitments, and proof as parameters to the
verify
method.
Verification Functionalities
You'll implement the following functionalities to handle the verification result:
If the verification is successful (
if
block):Remove the challenge from the map of challenges (
challenges
) since it's a one-time use.Sign the account address (as a string) with the private key (
key_pair
).Encode the signature in uppercase hexadecimal format.
Return the encoded signature as the result (
Ok
variant of theResult
).
If the verification fails (
else
block):Return an error indicating that the proofs are invalid (
Err
variant of theResult
).
Use the following code to implement these functionalities to verify the proof:
Step 8: Create "main.rs" File
Create the main.rs
file that serves as the entry point for the application. It will contain the main function and configuration for running the HTTP server.
You will do the following:
Define
IdVerifierConfig
Structure:This structure is used to receive command-line arguments.
It holds parameters such as node endpoint, port, log level, statement, sign key, and verify key.
It's derived using the
clap::Parser
trait to parse command-line arguments conveniently.
Import Necessary Modules:
Import
handlers
andtypes
modules which contain helper functions and data structures required for handling requests.
Import External Dependencies:
Import necessary external dependencies such as
clap
,concordium_rust_sdk
,log
, andwarp
.
Implement the
main()
Function:Annotate the
main()
function with#[tokio::main]
macro to transform it into an asynchronous main function.Parse command-line arguments using
IdVerifierConfig
structure.Initialize the logger with the specified log level.
Serialize the statement using the
concordium-rust-sdk
.Create a client to interact with the Concordium node.
Get the latest cryptographic parameters from the Concordium node.
Create a state variable with an empty map of challenges and the global context.
Add the following code to your "main.rs" file:
Step 9: Get Challenge
To allow your clients to retrieve challenges for verification purposes, you need to make your application respond to GET requests to the /api/challenge
endpoint. Your application will be able to generate a random challenge for the specified address and store it in the application state.
Follow the steps below to implement the "Get Challenge" functionality in the application:
Set CORS Configuration:
Define CORS settings to manage website access permissions. This ensures that the server allows requests from specified origins.
Implement the GET Endpoint:
Create an endpoint accessible at
localhost:8000/api/challenge
.This endpoint will handle GET requests to retrieve a randomly generated challenge for a given address.
Extract the address from the query payload.
Invoke
handle_get_challenge()
Function:Call the
handle_get_challenge()
function, passing the extracted address as input.This function asynchronously generates a challenge and stores it in the application state.
Store Challenge in State:
After generating the challenge, store it in a map in the application state.
Use the base16 encoded version of the challenge as the key and associate it with the address and the generation time.
Add the following code to your "main.rs" file to implement the Get Challenge functionalities:
This code does the following:
.map(|query: WithAccountAddress| query.address)
: This line maps the extracted query parameters to just theaddress
field of theWithAccountAddress
struct..and_then(handle_get_challenge)
: This then passes the extracted address to thehandle_get_challenge
function for further processing.
Step 10: Get Statement
Add a "Get Statement" endpoint to enable your dApp to provide the statement to clients when they make a GET request to the /api/statement
endpoint. This allows the dApp to understand the conditions that users need to meet for verification purposes.
To implement the "Get Statement" endpoint in your application, follow these steps:
Define the Endpoint:
Create a GET endpoint accessible at
localhost:8000/api/statement
.
Handle the GET Request:
When a GET request is made to this endpoint, retrieve the statement from your input variables.
Prepare the statement to be sent back as a response.
Send the Statement:
Respond to the GET request using the statement obtained in the previous step.
Use the following code:
The code does the following:
The
warp::get()
function is used to define a GET endpoint..and(warp::path!("api" / "statement"))
specifies the route for the endpoint, which is/api/statement
..map(move || warp::reply::json(&app.statement))
maps the endpoint to a closure that returns a JSON response containing the statement retrieved from theapp
configuration.
Step 11: Prove
Add a "Prove" functionality to enable your dApp handle POST to the /api/prove
endpoint by verifying the provided proof and responding with a signature if verification is successful.
To implement the "Prove" functionality, you'll need to follow these steps:
Implement the POST Endpoint:
Create an endpoint accessible at
localhost:8000/api/prove
.This endpoint will handle POST requests containing the challenge and the proof.
Handle the Proof:
When a POST request is made to this endpoint, extract the challenge and proof from the request body.
Re-create the key pair using the
verify_key
andsign_key
provided in the input variables.Pass the extracted challenge, proof, and key pair to the
handle_provide_proof
helper function for verification and signing.
Response:
If the proof is successfully verified, the endpoint should respond with a signature, which is the user's public key signed by the backendâs private key.
The user can use this signature to mint the token, as it will be verifiable by the smart contract using the public key of the backend address.
Main Function:
Initialize the application configuration using
IdVerifierConfig::parse()
.Set up logging based on the provided log level.
Deserialize the statement from the input variable.
Initialize the Concordium SDK client and retrieve the global cryptographic parameters.
Initialize the server state.
Define CORS settings for the server.
Define the endpoints for getting the challenge, getting the statement, and providing proof.
Start the HTTP server to listen for incoming requests on the specified port.
Additionally, spawn a task to handle cleaning up the server state periodically.
This code does the following:
This code block defines a POST endpoint accessible at
/api/prove
.It expects a JSON body containing the challenge and proof (
ChallengedProof
).The endpoint invokes the
handle_provide_proof
function to verify the proof and sign it.It recreates the key pair using the provided
verify_key
andsign_key
.If the proof is successfully verified, it responds with a signature.
Last updated