Skip to main content

Quick Start Guide

Introduction into Self Sovereign Identity (SSI)

Self-sovereign identity (SSI) is complex. This Quick Start Guide explains the fundamental concepts to get up and running with Hyperledger Identus. This guide will familiarize you with the general concepts and how to create decentralized identifiers (DIDs), issue credentials, make connections, and verify credentials with verifiable presentations. Refer to the Concepts and Components sections for a more in-depth explanation.

The trust triangle is the most basic process for conveying trust in the digital world. There are three roles in an SSI ecosystem: Holders, Issuers, and Verifiers.

Component Diagram

Holders can be any entity, such as individuals, organizations, and digital or physical things. They will hold verifiable credentials (VCs) and use a verifiable presentation to share their VCs.

Issuers can also be any entity that makes claims about an entity. These claims are attestations, or evidence of something, about the Holder. As an example, an insurance company would provide proof of valid insurance.

Verifiers are the relying party in the triangle. They will request information from the Holder, such as proof of insurance, and the Holder will use a verifiable presentation to share the appropriate VCs with the Verifier. The Holder's digital signature, the issuer DID get verified, and the contents therein to ensure nothing has been tampered with.

Hyperledger Identus flow

The diagram details how the concepts fit alongside the Identus components in a typical SSI interaction.

Component Diagram

An overview of Hyperledger Identus components

Identus consists of core libraries that facilitate typical SSI interactions between Issuers, Holders, and Verifiers.

A Cloud Agent

A Cloud Agent can issue, hold, and verify verifiable credentials (VCs) for any entity and manage decentralized identifiers (DIDs) and DID-based connections. The Cloud Agent has an easy-to-use REST API to enable easy integration into any solution and uses DIDComm V2 as a messaging protocol for Cloud Agent-to-Cloud Agent communication.

It is maintained as an open source through the Hyperledger Identus.

More in depth documentation about Cloud Agent can be found here.

Wallet SDKs

Wallet SDKs for web and mobile (iOS, Android, TypeScript) enable identity holders to store credentials and respond to proof requests. They are typically used in applications that allow identity holders to interact with issuers and verifiers.

More in-depth documentation about the different Wallet SDKs can be found here (TypeScript, Swift, KMP)

Mediator

Mediators are for storing and relaying messages between Cloud Agents and Wallet SDKs. They act as a proxy that remains connected to the network and receives any message, credential, or proof request on behalf of the Wallet SDKs (which can be offline occasionally).

More in-depth documentation about Mediator can be found here.

Node for a Verifiable Data Registry (VDR)

To issue and verify VCs to and from DIDs, we need a Verifiable Data Registry (VDR) that is globally resolvable and always on. In Identus's case, it is prism-node, anchoring key information required for issuance and verification on the Distributed Ledger.

Pre-Requisites

Agent Deployment

This guide will demonstrate a single-tenant deployment with API Key authentication disabled and an in-memory ledger for published DID storage, which is the simplest configuration to get started as a developer. More advanced configuration options can be found in Multi-Tenancy Management and associated Environment Variables configuration options.

We develop on modern machines equipped with either Intel based x64 processors or Apple ARM processors with a minimum of four cores, 16 GB of memory and 128GB+ of SSD-type storage.

  1. To spin up an Cloud Agent you must:
git clone https://github.com/hyperledger/identus-cloud-agent
  1. Once cloned, create a new file named ./identus-cloud-agent/infrastructure/local/.env-issuer to define the Issuer Agent environment variable configuration with the following content:
API_KEY_ENABLED=false
AGENT_VERSION=1.36.1
PRISM_NODE_VERSION=2.4.1
PORT=8000
NETWORK=identus
VAULT_DEV_ROOT_TOKEN_ID=root
PG_PORT=5432
  1. Create a new file named ./identus-cloud-agent/infrastructure/local/.env-verifier to define the Verifier Agent environment variable configuration with the following content:
API_KEY_ENABLED=false
AGENT_VERSION=1.36.1
PRISM_NODE_VERSION=2.4.1
PORT=9000
NETWORK=identus
VAULT_DEV_ROOT_TOKEN_ID=root
PG_PORT=5433
  1. Setting the API_KEY_ENABLED to false disables the requirement of using API Keys.
caution

API_KEY_ENABLED disables API Key authentication. This should not be used beyond Development purposes.

  1. Start the issuer and verifier Cloud Agents by running the below commands in the terminal.
  • Issuer Cloud Agent:

Mac OSX terminal shell

 ./infrastructure/local/run.sh -n issuer -b -e ./infrastructure/local/.env-issuer -p 8000 -d "$(ipconfig getifaddr $(route get default | grep interface | awk '{print $2}'))"

Linux terminal shell

 ./infrastructure/local/run.sh -n issuer -b -e ./infrastructure/local/.env-issuer -p 8000 -d "$(ip addr show $(ip route show default | awk '/default/ {print $5}') | grep 'inet ' | awk '{print $2}' | cut -d/ -f1)"
  • The Issuer API endpoint will be accessible on port 8000 http://localhost:8000/cloud-agent/ with a Swagger Interface available at http://localhost:8000/cloud-agent/redoc.

  • Verifier Cloud Agent:

For Mac OSX terminal shell

 ./infrastructure/local/run.sh -n verifier -b -e ./infrastructure/local/.env-verifier -p 9000 -d "$(ipconfig getifaddr $(route get default | grep interface | awk '{print $2}'))"

For Linux terminal shell

 ./infrastructure/local/run.sh -n verifier -b -e ./infrastructure/local/.env-verifier -p 9000 -d "$(ip addr show $(ip route show default | awk '/default/ {print $5}') | grep 'inet ' | awk '{print $2}' | cut -d/ -f1)"
  • The Verifier API endpoint will be accessible on port 9000 http://localhost:9000/cloud-agent/ with a Swagger Interface available at http://localhost:9000/cloud-agent/redoc.

Agent configuration

Creating LongForm PrismDID

  1. Run the following API request against your Issuer API to create a PRISM DID:
curl --location \
--request POST 'http://localhost:8000/cloud-agent/did-registrar/dids' \
--header 'Accept: application/json' \
--data-raw '{
"documentTemplate": {
"publicKeys": [
{
"id": "auth-1",
"purpose": "authentication"
},
{
"id": "issue-1",
"purpose": "assertionMethod"
}
],
"services": []
}
}'
  1. Publish the DID by replacing {didRef} with the longFormDid output value from the previous step.
curl --location \
--request POST 'http://localhost:8000/cloud-agent/did-registrar/dids/{didRef}/publications' \
--header 'Accept: application/json'
  1. The short version of the DID is the publishedPrismDID.
info

📖Learn more about PRISM DIDs and why it is necessary to publish specific DIDs here.

Create a credential schema (JWT W3C Credential)

  1. To create a credential schema on the Issuer API instance, run the following request:
info

Replace the [[publishedPrismDID]] in the example request with the did value from the previous step.

  1. We need to capture the schema's guid as its used in further steps.
curl -X 'POST' \
'http://localhost:8000/cloud-agent/schema-registry/schemas' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"name": "driving-license",
"version": "1.0.0",
"description": "Driving License Schema",
"type": "https://w3c-ccg.github.io/vc-json-schemas/schema/2.0/schema.json",
"author": [[publishedPrismDID]],
"tags": [
"driving",
"license"
],
"schema": {
"$id": "https://example.com/driving-license-1.0.0",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Driving License",
"type": "object",
"properties": {
"emailAddress": {
"type": "string",
"format": "email"
},
"givenName": {
"type": "string"
},
"familyName": {
"type": "string"
},
"dateOfIssuance": {
"type": "string",
"format": "date-time"
},
"drivingLicenseID": {
"type": "string"
},
"drivingClass": {
"type": "integer"
}
},
"required": [
"emailAddress",
"familyName",
"dateOfIssuance",
"drivingLicenseID",
"drivingClass"
],
"additionalProperties": true
}
}'

Starting Sample App

All wallet SDK's come bundled with a sample application, that cover all the Identus flows, including establishing connections, issuance, and verification flows.

  1. Clone the TypeScript SDK repository.
git clone https://github.com/hyperledger/identus-edge-agent-sdk-ts
  1. Ensure you have all applications installed for building the SDK and their dependencies

rust and wasm-pack are leveraged to build and use the AnonCreds and DIDComm Rust libraries within TypeScript. To build the SDK locally or run demonstration applications, you must have these applications installed.

The following should work Linux and MacOS. If you experience any issues, refer to the latest installation instructions for your platform.

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
  1. Run the following:
  • Build the source SDK:
cd identus-edge-agent-sdk-ts
git submodule update --init --recursive
npm i
npm run build
  • Start the React demo:
cd demos/next
npm i
npm run build
npm run start

Deploy & Establish Mediation

Mediation is the process that ensures messages get routed and stored correctly between Issuers, Verifiers and Holders, even if they are offline. The mediator offers a service that is always running and can securely store messages and deliver them to the associated DIDs using DIDComm. This enables use-cases where connectivity to a (mobile) wallet cannot be guaranteed.

Preparation

  1. To get the mediator deployed locally for the demo, clone the Mediator repository.
git clone https://github.com/hyperledger/identus-mediator
  1. With a Docker service running, open a new terminal and run:
info

The latest mediator version can be found at Mediator releases. Change the version in the example if you want to use the latest version.

Mac OSX terminal shell

MEDIATOR_VERSION=0.15.0 SERVICE_ENDPOINTS="http://$(ipconfig getifaddr $(route get default | grep interface | awk '{print $2}')):8080;ws://$(ipconfig getifaddr $(route get default | grep interface | awk '{print $2}')):8080/ws" docker-compose up

Linux terminal shell

MEDIATOR_VERSION=0.15.0 SERVICE_ENDPOINTS="http://$(ip addr show $(ip route show default | awk '/default/ {print $5}') | grep 'inet ' | awk '{print $2}' | cut -d/ -f1):8080;ws://$(ip addr show $(ip route show default | awk '/default/ {print $5}') | grep 'inet ' | awk '{print $2}' | cut -d/ -f1):8080/ws" docker-compose up

MEDIATOR_ENDPOINTS is then set to your local IP address:8080.

  1. More advanced documentation and configuration options can be found here.

  2. Now you need to capture the Mediator's Peer DID in order to start DIDCOMM V2 Mediation protocol, you can do so by opening you browser at the mediators endpoint.

Demo application

info

In order to complete this step you'll need to connect to the mediators Peer DID which you can fetch by making the following API request.

curl --location \
--request GET 'localhost:8080/invitation' \
--header 'Content-Type: application/json'

Follow the steps in your desired platform as stated below:

  1. Open http://localhost:3000/debug in your browser,
  2. paste the mediator peer DID (obtained from the from attribute after fetching from the mediator's invitation endpoint),
  3. click Edge Agent tab in the bottom left,
  4. click Connect button,
  5. click Start button.
  1. If you are running the SampleApp, click the Start Agent button.

The below code examples show how to establish mediation when building your own application.

Code examples 3. The following code examples represent establishing mediation and instantiating the Cloud Agent.

  const mediatorDID = SDK.Domain.DID.fromString(
[[MEDIATOR DID PEER]]
);
const api = new SDK.ApiImpl();
const apollo = new SDK.Apollo();
const castor = new SDK.Castor(apollo);
const didcomm = new SDK.DIDCommWrapper(apollo, castor, pluto);
const mercury = new SDK.Mercury(castor, didcomm, api);
const store = new SDK.PublicMediatorStore(pluto);
const handler = new SDK.BasicMediatorHandler(mediatorDID, mercury, store);
const manager = new SDK.ConnectionsManager(castor, mercury, pluto, handler);
const seed = apollo.createRandomSeed()
const agent = new SDK.Agent(
apollo,
castor,
pluto,
mercury,
handler,
manager,
seed.seed
);
/**
* This internally will attempt to load an existing mediator from the
* database. If it does not exist it will try to achieve mediation
* automatically, by creating a PeerDID and sending a MediationRequest.
* After this step the mediator starts capturing messages for the PeerDID we specied.
*/
await agent.start()

Establish Holder connections

To connect the Holder to both Cloud Agent instances, you must run this in both Issuer and Verifier endpoints.

Establish a connection - Agent side

A connection must be established between the Holder and Cloud Agents to correctly deliver the Issuance + Verification Messages to the Holder.

Establish connection on the Issuer Cloud Agent

curl --location \
--request POST 'http://localhost:8000/cloud-agent/connections' \
--header 'Content-Type: application/json' \
--data-raw '{
"label": "Cloud Agent demo connection with holder"
}'
  1. This request will return a JSON response with an invitation and its URL. The Issuer Cloud Agent would share this URL as a QR code, and the holder would scan it with the wallet app.
  • Copy the invitationUrl and the connectionId.

Establish connection on the Verifier Cloud Agent

curl --location \
--request POST 'http://localhost:9000/cloud-agent/connections' \
--header 'Content-Type: application/json' \
--data-raw '{
"label": "Cloud Agent demo connection with holder"
}'
  1. This request will return a JSON response with an invitation and its URL. The Verifier Cloud Agent would share this URL as a QR code, and the holder would scan it with the wallet app.
  • Copy the invitationUrl and the connectionId.

Establish a connection - Holder side

  1. Now that you have the invitation, it's time for the Holder to accept it.

Demo application

  1. Open a browser at localhost:3000.
  2. Start the Edge Agent by clicking the button.
  3. Paste the invitation URL generated in the previous step into the CloudAgent connection section and click on Create Connection.
  • The application will react when the connection gets established correctly and show a new connection.
Code examples
const parsed = await props.agent.parseOOBInvitation(new URL([[OOB URL]]));
await props.agent.acceptDIDCommInvitation(parsed);

Issue a Credential from the Issuer to the holder

The credential issuance flow consists of multiple steps, detailed in this section. It starts with the Issuer sending a Credential Offer to the Holder, which would accept or reject this invitation and create a credentialRequest from it. The credential request gets sent through DIDComm to the Issuer, issuing and sending the credential back to the Holder.

The Issuer can create a credential offer in two ways:

  1. As a direct credential offer DIDComm message for a holder with an existing connection
  2. As an credential offer as attachment in an OOB invitation message for connectionless issuance

Create a Credential Offer with an existing connection Issuer Agent

  1. To trigger the creation of a credential-offer, we call the credential-offers-endpoint, as follows:
info

Please replace the following variables in the example request before sending:

  • connectionId: The ID of the connection previously established between agent and holder. This is part of the response of the POST message from the agent when calling the cloud-agent/connections endpoint. It is returned in the connectionId attribute. There is a unique connection ID for the relationship between issuer and holder and verifier and holder. In this example, please use the connectionId returned when creating the connection between issuer and holder
  • publishedPrismDID: The short form of the PRISM DID created when setting up the Issuer agent

The connectionId is just the ID of the connection we previously established with the issuer.

The Issuing DID is the published PRISM DID in its short version which was also used to create and publish the credential schema.

  • ``
curl --location --request POST 'http://localhost:8000/cloud-agent/issue-credentials/credential-offers' \
--header 'Content-Type: application/json' \
--data-raw '{
"claims": {"emailAddress":"sampleEmail", "familyName":"", "dateOfIssuance":"2023-01-01T02:02:02Z", "drivingLicenseID":"", "drivingClass":1},
"connectionId": [[connectionId]],
"issuingDID": [[publishedPrismDID]],
"automaticIssuance": true
}'

Create CredentialRequest from CredentialOffer Holder

  1. Because this credential Offer was created with the automaticIssuance true, as soon as the CloudAgent receives this credentialRequest it will respond with the IssuedCredential message and send this back to the holder.
info

automaticIssuance is optional. It can also be manually triggered and confirmed by the Holder.```

Demo application

  1. The holder will at some point receive a CredentialOffer, which the holder must accept, and then, a CredentialRequest is created and sent back to the Issuer through DIDComm V2 protocols.
  1. The CredentialOffer message will be automatically accepted as soon as it reaches the browser. In exchange, a CredentialRequest message will get sent back to the CloudAgent.
Code examples
  1. The exchange between CredentialOffer and CredentialRequest is demonstrated through more advanced code samples below, showcasing how different platforms handle it.
props.agent.addListener(ListenerKey.MESSAGE,async (newMessages:SDK.Domain.Message[]) => {
//newMessages can contain any didcomm message that is received, including
//Credential Offers, Issued credentials and Request Presentation Messages
const credentialOffers = newMessages.filter((message) => message.piuri === "https://didcomm.org/issue-credential/2.0/offer-credential");

if (credentialOffers.length) {
for(const credentialOfferMessage of credentialOffers) {
const credentialOffer = OfferCredential.fromMessage(credentialOfferMessage);
const requestCredential = await props.agent.prepareRequestCredentialWithIssuer(credentialOffer);
try {
await props.agent.sendMessage(requestCredential.makeMessage())
} catch (err) {
console.log("continue after err", err)
}
}
}
})

Store the Issued Credential [Holder]

caution

The sample application are using an insecure storage solution which should only be used for testing purposes and not production environments!

Code examples 6. Once the Holder receives a credential from the Cloud Agent, it needs to store the credential somewhere:

props.agent.addListener(ListenerKey.MESSAGE,async (newMessages:SDK.Domain.Message[]) => {
//newMessages can contain any didcomm message that is received, including
//Credential Offers, Issued credentials and Request Presentation Messages
const issuedCredentials = newMessages.filter((message) => message.piuri === "https://didcomm.org/issue-credential/2.0/issue-credential");
if (issuedCredentials.length) {
for(const issuedCredential of issuedCredentials) {
const issueCredential = IssueCredential.fromMessage(issuedCredential);
await props.agent.processIssuedCredentialMessage(issueCredential);
}
}
})

Request a verification from the Verifier Cloud Agent to the Holder (JWT W3C Credential)

Now that the Holder has received a credential, it can be used in a verification workflow between a Holder and a Verifier. This requires the following steps:

  1. Verifier creates a proof request
  2. Holder receives the proof request
  3. Holder creates a proof presentation and shares this with the verifier
  4. Verifier verifies the proof presentation
info

In the example, we demonstrate two verification flows:

  1. Verification with an established connection between the Holder and the Verifier.
  2. Connectionless verification in which the Holder and Verifier do not have a pre-established connection.

Verifier Agent

  1. To run this section, we will use the connection we created between the Holder and the Verifier.
curl --location \
--request POST 'http://localhost:9000/cloud-agent/present-proof/presentations' \
--header 'Content-Type: application/json' \
--data-raw '{
"connectionId": [[connectionId]],
"proofs": [
{
"schemaId": [[schemaId]],
"trustIssuers": [
[[PUBLISHED PRISM DID FROM THE ISSUER]]
]
}
],
"options": {
"challenge": "A challenge for the holder to sign",
"domain": "domain.com"
}
}'
  • This API request will return a presentationRequestId, which the verifier can use later to check the current status of the request.

Holder: Receives the Presentation proof request

  1. The Holder needs an Edge Agent running with the message listener active. It will receive the presentation proof request from the Verifier Cloud Agent for the correct type of messages as detailed below:
Code examples
props.agent.addListener(ListenerKey.MESSAGE,async (newMessages:SDK.Domain.Message[]) => {
//newMessages can contain any didcomm message that is received, including
//Credential Offers, Issued credentials and Request Presentation Messages
const requestPresentations = newMessages.filter((message) => message.piuri === "https://didcomm.atalaprism.io/present-proof/3.0/request-presentation");

if (requestPresentations.length) {
for(const requestPresentation of requestPresentations) {
const lastCredentials = await props.pluto.getAllCredentials();
const lastCredential = lastCredentials.at(-1);
const requestPresentationMessage = RequestPresentation.fromMessage(requestPresentation);
try {
if (lastCredential === undefined) throw new Error("last credential not found");

const presentation = await props.agent.createPresentationForRequestProof(requestPresentationMessage, lastCredential)
await props.agent.sendMessage(presentation.makeMessage())
} catch (err) {
console.log("continue after err", err)
}
}
}
})

Verifier: Will then check on the API if the Presentation Request has been completed or not.

curl --location \
--request GET 'http://localhost:9000/cloud-agent/present-proof/presentations/[[presentationRequestId]]' \
--header 'Accept: application/json'
  1. The response body establishes the completion of the request and can be verified for correctness.