Skip to main content

Become an issuer

Intermediate
Tutorial

Overview

An issuer is a service or app that can issue a verifiable credential (VC) to a user. On ICP, an issuer is an exposed API that receives calls from an identity provider and does not trigger any workflows itself. To become an issuer, your canister must implement the issuer API as described in the verifiable credential spec.

Issuers receive credentials requests from relying partys through an identity provider such as Internet Identity (II). The end-user must allow or revoke the communication before the credential is issued.

Internet Identity is used as the identity provider in this tutorial.

Development of the issuer API

The issuer API consists of four endpoints:

  1. vc_consent_message: Responds with a message of consent to be displayed to the user to receive their approval of issuing the credentials.

  2. derivation_origin: Returns the URL used to derive the user's principal for the issuer.

  3. prepare_credential: Checks the validity of the request and prepares the actual credential requested.

  4. get_credential: Issues the actual credential requested by the user.

First, the identity provider sends a request to the issuer API endpoint vc_consent_message to get the consent message to show the user. A consent message is text shown to the user to confirm that they allow the issuing of a VC.

An example implementation in Rust can be found below:

pub fn get_vc_consent_message_en(
credential_spec: &CredentialSpec,
) -> Result<Icrc21ConsentInfo, Icrc21Error> {
match verify_credential_spec(credential_spec) {
Ok(SupportedCredentialType::EarlyAdopter(since_year)) => Ok(Icrc21ConsentInfo {
consent_message: format!("You became an early adopter in {}.", since_year),
language: "en".to_string(),
}),
Ok(SupportedCredentialType::EventAttendance(event_name)) => Ok(Icrc21ConsentInfo {
consent_message: format!("You have attended the event {}.", event_name),
language: "en".to_string(),
}),
Err(err) => Err(Icrc21Error::ConsentMessageUnavailable(Icrc21ErrorInfo {
description: format!("Credential spec not supported: {:?}", err),
})),
}
}

Step 2: Implement a derivation origin (derivation_origin)

Next, the issuer API endpoint derivation_origin is called by the identity provider to obtain a URL to be used as the derivation origin for user's principal.

If the issuer doesn't rely on alternative derivation origins, then it must return the canister's URL: https://<issuer-canister-id>.icp0.io.

If the issuer relies on the alternative derivation origins feature, the endpoint must return the same value for the derivationOrigin when logging in.

The returned value is subject to verification via .well-known/ii-alternative-origins, as describe in the alternative origins documentation.

An example implementation in Rust can be found below:

use ic_cdk::update;
use vc_util::issuer_api::{DerivationOriginData, DerivationOriginError, DerivationOriginRequest};

#[update(name = "derivation_origin")]
async fn vc_derivation_origin(
_req: DerivationOriginRequest,
) -> Result<DerivationOriginData, DerivationOriginError> {
let dfx_network = option_env!("DFX_NETWORK").unwrap();
let origin = match dfx_network {
"local" => format!("http://{}.localhost:4943", ic_cdk::api::id()),
"ic" => format!("https://{}.icp0.io", ic_cdk::api::id()),
_ => {
return Err(DerivationOriginError::Internal(
"Invalid DFX_NETWORK".to_string(),
))
}
};
Ok(DerivationOriginData { origin })
}

The returned derivation origin is subject to verification via .well-known/ii-alternative-origins, as described in the feature description.

Step 3: Prepare the credentials (prepare_credential)

Before a credential can be issued, the request for the credential must be checked for validity using the prepare_credential API endpoint. The objective of this endpoint is to check the validity of the request and update the certified_data with a new root hash that includes the signature on the credential. Upon success, the actual credential is prepared.

For more details on how to validate the request or create the actual credential, check out the VC specification.

An example implementation can be found in the verifiable credentials playground example.

The value of prepared_context is used to transfer information between the prepare_credential and get_credential steps. It is up to the issuer API to decide on the content of this field since the issuer API creates prepared_context and is the only entity that consumes it.

For example, when using canister signatures prepared_context contains a time stamped and unsigned VC, for which the canister signature will be available through the get_credential call.

Step 4: Issue the credential (get_credential)

Lastly, the issuer API endpoint get_credential is used to issue the credential requested by the user. The same checks used to prepare the credential are ran again, with an additional verify step to confirm that prepared_context is consistent with prepare_credential. If successful, the issuer API returns the signed credential in JWT-format.

An example implementation can be found in the verifiable credentials playground example.

An issuer may follow the convention below for an easier verification of the returned credentials.

Given a credential specification such as:

"credentialSpec": {
"credentialType": "SomeVerifiedProperty",
"arguments": {
"argument_1": "value_1",
"another_argument": 42,
}

The returned JWT should contain a property named by the value of credentialType in credentialSubject from the specification, with key-value entries listing the arguments from the specification, namely:

"SomeVerifiedProperty": {
"argument_1": "value_1",
"another_argument": 42,
}

For example, for a VerifiedAdult credential, you would use the following credential specification:

"credentialSpec": {
"credentialType": "VerifiedAdult",
"arguments": {
"minAge": 18,
}

A compliant issuer would issue a VC that contains credentialSubject with the property:

"VerifiedAdult": {
"minAge": 18,
}

Resources