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.


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.


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.

  1. 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.

  2. Creating Proofs: Using the challenge, the dApp requests proofs from the wallet for specific statements, such as age verification.

  3. User Consent and Proof Generation: The wallet generates and provides the necessary proofs upon user consent.

  4. Proof Verification: The dApp submits the proofs to the verifier for verification, ensuring correctness based on the challenge and statements.

  5. Signing with Private Key: Utilizing the owner's private key, the dApp signs a message.

  6. 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:


  • 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:

  1. Request Challenge and Statements:

    • The dApp requests a challenge and relevant statements from the verifier backend.

  2. Proof Request to Wallet:

    • Based on the challenge and statements, the dApp seeks proofs from the wallet.

  3. User Consent and Proof Generation:

    • Upon user approval, the wallet generates the required proofs.

  4. Verification by Verifier:

    • The dApp submits the proofs to the verifier for validation against the challenge and statements.

  5. 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:

cargo new backend

This command initializes a new Rust project named "backend":

Step 2: Install Concordium Rust-SDK

The Concordium Rust SDK, now available on, streamlines the integration process for Concordium-related functionalities. Here's how to install it:

  1. Direct Installation from Add the following line to your Cargo.toml file:

concordium-rust-sdk = 1
  1. Manual Installation: While the SDK is not yet published on, manual installation is required. Follow these steps to clone and install the SDK:

# Create a folder named 'deps'
mkdir deps

# Clone the repository inside the 'deps' folder
git clone deps/concordium-rust-sdk

# Update the submodules
git submodule update --init --recursive

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:

tokio = { version = "1", features = ["full"] }
warp = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
log = "0.4.11"
env_logger = "0.9"
clap = { version = "4", features = ["derive"] }
anyhow = "1.0"
chrono = "0.4.19"
thiserror = "1"
rand = "0.8"
ed25519-dalek = "1.0.1"
hex = "0.4.3"

path = "../deps/concordium-rust-sdk/"

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:

  1. Navigate to the Concordium Rust SDK Folder: Access the Concordium Rust SDK folder in your terminal or command prompt.

  2. Execute Build Command: Run the following command to initiate the build process:

cargo build
  1. Handling Protobuf Error (For Mac Users): If you encounter a protobuf error during the build process on macOS:

  1. To fix the error, you may need to install protobuf manually:

brew install protobuf
  1. After installing protobuf, rerun the build command:

cargo build

Step 5: Create "" File

  1. Create a "" 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, and Credential.

    • ChallengeResponse Struct: Used for API responses, alongside ChallengedProof and ProofWithContext. When the backend receives the proof, it utilizes these structures to validate it using the client object.

  2. Implement the data structures by adding the following code to your "" file:

use concordium_rust_sdk::{
        self as crypto_common,
        derive::{SerdeBase16Serialize, Serialize},
        Buffer, Deserial, ParseResult, ReadBytesExt, SerdeDeserialize, SerdeSerialize, Serial,
    endpoints::{QueryError, RPCError},
        constants::{ArCurve, AttributeKind},
        types::{AccountAddress, GlobalContext},
use std::{
    sync::{Arc, Mutex},

    Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, SerdeBase16Serialize, Serialize,
pub struct Challenge(pub [u8; 32]);

#[derive(serde::Deserialize, Debug, Clone)]
pub struct WithAccountAddress {
    pub address: AccountAddress,

pub struct ChallengeStatus {
    pub address: AccountAddress,
    pub created_at: SystemTime,

pub struct Server {
    pub challenges: Arc<Mutex<HashMap<String, ChallengeStatus>>>,
    pub global_context: Arc<GlobalContext<ArCurve>>,

/// An internal error type used by this server to manage error handling.
pub enum InjectStatementError {
    #[error("Not allowed")]
    #[error("Invalid proof")]
    #[error("Node access error: {0}")]
    NodeAccess(#[from] QueryError),
    #[error("Error acquiring internal lock.")]
    #[error("Proof provided for an unknown session.")]
    #[error("Issue with credential.")]

impl warp::reject::Reject for InjectStatementError {}

/// Response in case of an error. This is going to be encoded as a JSON body
/// with fields 'code' and 'message'.
pub struct ErrorResponse {
    pub code: u16,
    pub message: String,

#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct ChallengeResponse {
    pub challenge: Challenge,

#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct ChallengedProof {
    pub challenge: Challenge,
    pub proof: ProofWithContext,

#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
pub struct ProofWithContext {
    pub credential: CredentialRegistrationID,
    pub proof: Versioned<Proof<ArCurve, AttributeKind>>,

You can refer to this GitHub repository for the code implementation

Step 6: Create "" File

Create another file called "" and add the following functions:

Add handle_get_challenge Function

The 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:

use crate::crypto_common::base16_encode_string;
use crate::types::*;
use concordium_rust_sdk::{
    common::{self as crypto_common, types::KeyPair},
        constants::{ArCurve, AttributeKind},
        types::{AccountAddress, AccountCredentialWithoutProofs},
use log::warn;
use rand::Rng;
use std::convert::Infallible;
use std::time::SystemTime;
use warp::{http::StatusCode, Rejection};

static CLEAN_INTERVAL_SECONDS: u64 = 600;

pub async fn handle_get_challenge(
    state: Server,
    address: AccountAddress,
) -> Result<impl warp::Reply, Rejection> {
    let state = state.clone();
    log::debug!("Parsed statement. Generating challenge");
    match get_challenge_worker(state, address).await {
        Ok(r) => Ok(warp::reply::json(&r)),
        Err(e) => {
            warn!("Request is invalid {:#?}.", e);

/// A common function that produces a challenge and adds it to the state.
async fn get_challenge_worker(
    state: Server,
    address: AccountAddress,
) -> Result<ChallengeResponse, InjectStatementError> {
    let mut challenge = [0u8; 32];
    rand::thread_rng().fill(&mut challenge[..]);
    let mut sm = state
        .map_err(|_| InjectStatementError::LockingError)?;
    log::debug!("Generated challenge: {:?}", challenge);
    let challenge = Challenge(challenge);

        ChallengeStatus {
            created_at: SystemTime::now(),
    Ok(ChallengeResponse { challenge })

Add handle_provide_proof Function

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 to verify the proof by calling the check_proof_worker() function.

Add check_proof_worker() Function

The 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:

  1. 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.

  2. 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.

  3. 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 and BlockIdentifier::LastFinal as parameters.

    • This function provides information about a given account address in the specified block.

  4. 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 includes InitialCredentialDeploymentValues and CredentialDeploymentCommitments.

To implement these functionalities for your check_proof_worker() function, use the following code:

pub async fn handle_provide_proof(
    client: concordium_rust_sdk::v2::Client,
    state: Server,
    statement: Statement<ArCurve, AttributeKind>,
    request: ChallengedProof,
    key_pair: KeyPair,
) -> Result<impl warp::Reply, Rejection> {
    let client = client.clone();
    let state = state.clone();
    let statement = statement.clone();
    match check_proof_worker(client, state, request, statement, key_pair).await {
        Ok(r) => Ok(warp::reply::json(&r)),
        Err(e) => {
            warn!("Request is invalid {:#?}.", e);

/// A common function that validates the cryptographic proofs in the request.
async fn check_proof_worker(
    mut client: concordium_rust_sdk::v2::Client,
    state: Server,
    request: ChallengedProof,
    statement: Statement<ArCurve, AttributeKind>,
    key_pair: KeyPair,
) -> Result<String, InjectStatementError> {
    let status = {
        let challenges = state
            .map_err(|_| InjectStatementError::LockingError)?;


    let cred_id = request.proof.credential;
    let acc_info = client
        .get_account_info(&status.address.into(), BlockIdentifier::LastFinal)

    // TODO Check remaining credentials
    let credential = acc_info

    if crypto_common::to_bytes(credential.value.cred_id()) != crypto_common::to_bytes(&cred_id) {
        return Err(InjectStatementError::Credential);

    let commitments = match &credential.value {
        AccountCredentialWithoutProofs::Initial { icdv: _, .. } => {
            return Err(InjectStatementError::NotAllowed);
        AccountCredentialWithoutProofs::Normal { commitments, .. } => commitments,

    let mut challenges = state
        .map_err(|_| InjectStatementError::LockingError)?;

    if statement.verify(
        &request.proof.proof.value, // TODO: Check version.
    ) {
        let sig = key_pair.sign(&acc_info.response.account_address.0);
    } else {

The line below from the code snippet ensures that the credential sent by the user is the same as the one the account has.

if crypto_common::to_bytes(credential.value.cred_id()) != crypto_common::to_bytes(&cred_id) {
        return Err(InjectStatementError::Credential);


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:

let commitments = match &credential.value {
        AccountCredentialWithoutProofs::Initial { icdv: _, .. } => {
            return Err(InjectStatementError::NotAllowed);
        AccountCredentialWithoutProofs::Normal { commitments, .. } => commitments,

Step 7: Verify the proof

Follow the steps below to verify the proof:

  • Use the verify method of the statement 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:

  1. 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 the Result).

  2. If the verification fails (else block):

    • Return an error indicating that the proofs are invalid (Err variant of the Result).

Use the following code to implement these functionalities to verify the proof:

if statement.verify(
        &request.proof.proof.value, // TODO: Check version.
    ) {
        let sig = key_pair.sign(&acc_info.response.account_address.0);
    } else {

Step 8: Create "" File

Create the 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:

  1. 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.

  2. Import Necessary Modules:

    • Import handlers and types modules which contain helper functions and data structures required for handling requests.

  3. Import External Dependencies:

    • Import necessary external dependencies such as clap, concordium_rust_sdk, log, and warp.

  4. 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 "" file:

mod handlers;
mod types;
use crate::handlers::*;
use crate::types::*;

use clap::Parser;
use concordium_rust_sdk::common::types::KeyPair;
use concordium_rust_sdk::{
    common::{self as crypto_common},
        constants::{ArCurve, AttributeKind},
use log::info;
use std::{
    sync::{Arc, Mutex},
use warp::Filter;

/// Structure used to receive the correct command line arguments.
#[derive(clap::Parser, Debug)]
#[clap(version, author)]
struct IdVerifierConfig {
        long = "node",
        help = "GRPC V2 interface of the node.",
        default_value = "http://localhost:20000"
    endpoint: concordium_rust_sdk::v2::Endpoint,
        long = "port",
        default_value = "8100",
        help = "Port on which the server will listen on."
    port: u16,
        long = "log-level",
        default_value = "debug",
        help = "Maximum log level."
    log_level: log::LevelFilter,
        long = "statement",
        help = "The statement that the server accepts proofs for."
    statement: String,
        long = "sign-key",
        help = "Sign key of the first credential of the signer"
    sign_key: String,
        long = "verify-key",
        help = "Verify key of the first credential of the signer"
    verify_key: String,

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:

  1. Set CORS Configuration:

    • Define CORS settings to manage website access permissions. This ensures that the server allows requests from specified origins.

  2. 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.

  3. 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.

  4. 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 "" file to implement the Get Challenge functionalities:

// 1a. get challenge
let get_challenge = warp::get()
    .and(warp::path!("api" / "challenge"))
    .and_then(move |query: WithAccountAddress| {
        handle_get_challenge(challenge_state.clone(), query.address)

This code does the following:

  • .map(|query: WithAccountAddress| query.address): This line maps the extracted query parameters to just the address field of the WithAccountAddress struct.

  • .and_then(handle_get_challenge): This then passes the extracted address to the handle_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:

  1. Define the Endpoint:

    • Create a GET endpoint accessible at localhost:8000/api/statement.

  2. 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.

  3. Send the Statement:

    • Respond to the GET request using the statement obtained in the previous step.

Use the following code:

// Define the GET endpoint for getting the statement
let get_statement = warp::get()
    .and(warp::path!("api" / "statement")) // Define the route for the endpoint
    .map(move || warp::reply::json(&app.statement)); // Return the statement as JSON

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 the app 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:

  1. 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.

  2. 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 and sign_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.

  3. 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.

  4. 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.

// 2. Provide proof
let provide_proof = warp::post()
    .and(warp::filters::body::content_length_limit(50 * 1024))
    .and(warp::path!("api" / "prove"))
    .and_then(move |request: ChallengedProof| {
        let kp = KeyPair::from(ed25519_dalek::Keypair {
            public: ed25519_dalek::PublicKey::from_bytes(
            secret: ed25519_dalek::SecretKey::from_bytes(

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 and sign_key.

  • If the proof is successfully verified, it responds with a signature.

Last updated