Verification (Validation) 🎯
In this section, we will collectively undertake the roles of the Verifier
individual.
These processes include:
- 📥 We will receive the
Presentation
object provided by theClaimer
. - 🧐 We will verify the accuracy of the information within this data.
- 🔗 You will confirm whether the
attestation
process is correct by checking the hash value on the chain. This attestation might also have been revoked. This situation needs to be considered as well. - 🤔 You will ensure that the Claimer sent the identity document.
A Presentation
is a document produced using a credential
. Unlike a credential
, a presentation
can hide some of its documents. These are pieces of information that the verifiers
don't need to know. This presentation
also proves that the credential
belongs to the Claimer
.
🔍 When a Claimer receives an attested Credential, they can store it in their wallets. But how can we determine its validity if there's no copy of this credential?
🔒 KILT provides a Verifier with the opportunity to verify the accuracy and validity of the information presented in a credential by a Claimer.
💡 The Verifier trusts this third party directly based on their reputation or through a delegation structure involving the Attester.
✅ For the verification process:
🔑 The Claimer needs a private key associated with the credential and their identifiers.
🔒 The Verifier needs the identifier of the Attester they trust.
🔎 During the verification process, the Claimer aims to prove three things to the Verifier:
1️⃣ The credential is valid and has not been revoked by the Attester. 2️⃣ The attributes specified in the credential genuinely belong to the Claimer. 3️⃣ The credential contains the information relevant to the Verifier for a specific use case.
🎨 Creating the Presentation
We need to find a way to send the Claimer and wait for the Verifier to verify it. The Claimer also needs to prove that they own this credential
. To prove this ownership, they need to create and sign a presentation
.
📦 Package Inclusions:
import * as Kilt from '@kiltprotocol/sdk-js'
This line incorporates the KILT SDK into the project. This SDK provides the necessary functions and classes to interact with the KILT protocol.
🛠 createPresentation
Function:
export async function createPresentation(
credential: Kilt.ICredential,
signCallback: Kilt.SignCallback,
challenge?: string
): Promise<Kilt.ICredentialPresentation> {
This function is an asynchronous function to create a credential presentation. It takes three parameters:
credential
: The credential that will be presented to the Verifier.signCallback
: The callback function to sign the credential presentation.challenge
: An optional parameter. This is a random string requested by the verifier and is used to confirm that the verifier asked the credential presentation.
The function returns an ICredentialPresentation
object, which is the credential presentation itself.
🎨 Creating the Presentation:
return Kilt.Credential.createPresentation({
credential,
signCallback,
challenge
})
This section creates a credential presentation using the Kilt.Credential.createPresentation
function. This function takes the three parameters mentioned above and returns a credential presentation.
If we want to understand the function by looking at the entire code:
import * as Kilt from '@kiltprotocol/sdk-js'
export async function createPresentation(
credential: Kilt.ICredential,
signCallback: Kilt.SignCallback,
challenge?: string
): Promise<Kilt.ICredentialPresentation> {
// Create the presentation from credential, DID, and challenge.
return Kilt.Credential.createPresentation({
credential,
signCallback,
challenge
})
}
In summary, this code enables the creation of a credential presentation. This presentation is used when presenting a credential to a verifier. Using this presentation, the verifier can verify the credential's validity and the identity of the credential's owner.
Verify (Validation)
Now, let's write our verification
code. First, we'll create a function named getChallenge
to prove that the Claimer
has the credential
by signing it.
Then, we'll perform the validation or verification
process with the verifyPresentation
function. We will use the verify.ts
file for these operations.
Package and Module Imports
import { config as envConfig } from 'dotenv'
import * as Kilt from '@kiltprotocol/sdk-js'
import { createPresentation } from './claimer/createPresentation'
import { generateKeypairs } from './claimer/generateKeypairs'
import { generateLightDid } from './claimer/generateLightDid'
In this section, the necessary modules and packages are imported. In particular, environment variables are loaded using dotenv
, and the KILT SDK and other helper functions are included.
Generating a Unique Challenge
function getChallenge(): string {
return Kilt.Utils.UUID.generate()
}
This function generates a unique challenge for the verification process.
Presentation Verification Function
async function verifyPresentation(
presentation: Kilt.ICredentialPresentation,
challenge: string,
trustedAttesterUris: Kilt.DidUri[]
): Promise<boolean> {
We can start the process by creating a function for verification. This function takes in 3 parameters and returns a Promise
with a value of True
or False
. Looking at these parameters one by one:
presentation
: The credential presentation to be verified.challenge
: The unique value sent by the Verifier (Verifier). This is used to confirm that the credential presentation was requested by the verifier.trustedAttesterUris
: The list of DID URIs of Attesters (Attester) trusted by the verifier.
This function verifies a credential presentation. It checks if the presentation is valid, its ownership, and if the attestation is correct.
Access to the KILT API:
Kilt.ConfigService.get('api')
This line provides access to the KILT API.
Try Catch Structure
try {
.
.
.
} catch {
return false
}
Upon entering the structure, we first encounter a try
catch
structure. This structure checks if the attestation
is correct. If it's not correct, the structure is caught by the catch
block, and it's understood that the attestation
is not on the chain with a value of false
.
Verifying the Presentation:
const { revoked, attester } = await Kilt.Credential.verifyPresentation(
presentation,
{ challenge }
)
Inside the Try structure, we are greeted by the Kilt.Credential.verifyPresentation
function, which performs the actual verification. This function takes in 2 variables. These are the presented presentation
and the challenge
values to prove that the Claimer is the correct individual.
This function returns 2 values. These values indicate whether the attestation
has been revoked
, i.e., lost its validity, and who the attester of the presentation is.
if (revoked) {
return false
}
If the revoke
value is true
, i.e., if the credential
has lost its validity, the verification
value is given as output as false
.
return trustedAttesterUris.includes(attester)
This line checks if the presenter (attester
) is on the list of trusted attestors (trustedAttesterUris
). If the attester is on this list, the function returns true
; otherwise, it returns false
.
One of the most important attributes a Verifier needs to check is whether or not they trust an attester
. If they don't trust, they may not accept the document, whether the attestation
is on the chain or not. This situation creates a trust race among the attesters
.
Verification Flow
export async function verificationFlow(
credential: Kilt.ICredential,
signCallback: Kilt.SignCallback,
trustedAttesterUris: Kilt.DidUri[] = []
) {
This function manages the verification process. Firstly, it generates a challenge, then creates a presentation, and finally verifies the presentation. This function takes in 3 parameters. Looking at these parameters one by one:
credential: Kilt.ICredential
: The credential to be presented to the Verifier.signCallback: Kilt.SignCallback
: The callback function to sign the credential presentation.trustedAttesterUris: Kilt.DidUri[]
: The list of DID URIs of Attesters (Attester) trusted by the verifier. It's an optional parameter and starts with an empty list by default.
By creating this function, we manage the entire verification
process in one place.
The verificationFlow
function represents a broader verification process that includes the verifyPresentation
function. The verifyPresentation
function alone only checks the validity of a credential presentation. However, in the real world, a verification process requires more than just checking the validity of a presentation. That's why we need the verificationFlow
function.
The objectives of the verificationFlow
function are:
Generating a Unique Challenge: The Verifier sends a unique challenge to the Claimer. This is used to ensure that the presentation is created in real-time and for a specific request.
Creating the Presentation: Using this challenge, the Claimer creates and signs the credential presentation.
Verifying the Presentation: The Verifier verifies the presentation received from the Claimer with the
verifyPresentation
function.Reporting the Results: If the presentation is correct, the verifier prints a successful verification message. Otherwise, it prints a failed verification message.
Each of these steps is a critical component of a decentralized identity verification process. The verifyPresentation
function is just a part of this process. The verificationFlow
function brings these steps together to provide a complete verification process. That's why we need not only the verifyPresentation
function but also the verificationFlow
function that combines these steps.
Sure, let's continue with the English translation:
Generating a Unique Challenge
const challenge = getChallenge()
This line generates a unique challenge to be sent to the Claimer. This ensures that the presentation is created in real-time and for a specific request. We had previously defined this function.
Creating the Credential Presentation
const presentation = await createPresentation(
credential,
signCallback,
challenge
)
This line creates a credential presentation using the provided credential, signature callback function, and challenge. We had previously defined this function in the claimer
folder.
Verifying the Presentation:
const isValid = await verifyPresentation(
presentation,
challenge,
trustedAttesterUris
)
This line verifies the created credential presentation. If the presentation is valid, the isValid
value will be true
.
Reporting the Results:
if (isValid) {
console.log('Verification successful! You are allowed to enter the club 🎉')
} else {
console.log('Verification failed! 🚫')
}
This section prints a message based on the validity of the presentation. If the presentation is valid, a successful verification message is printed. Otherwise, a failed verification message is printed.
When we look inside the verificationFlow()
function, we essentially execute the functions we previously wrote. Sequentially:
- We generate a
Challenge
to authenticate the Claimer. - The Claimer creates a
presentation
. - As a
Verifier
, we verify the presented authentication.
Main Function
This section contains the main function that will execute when the file is run directly. It loads environment variables, connects to KILT, and initiates the verification flow.
if (require.main === module) {
;(async () => {
This if
structure checks if the module is being run directly.
Loading Environment Variables:
envConfig()
This line loads the environment variables from the .env
file. This is used to store various configuration values used in the project.
Connecting to KILT:
await Kilt.connect(process.env.WSS_ADDRESS as string)
This line establishes a connection to the KILT network. The connection address is retrieved from the .env
file.
Generating Claimer's DID Information:
const claimerDidMnemonic = process.env.CLAIMER_DID_MNEMONIC as string
const { authentication } = generateKeypairs(claimerDidMnemonic)
const claimerDid = generateLightDid(claimerDidMnemonic)
These steps are used to generate the Claimer's DID (Decentralized Identifier) information. This is required to sign the credential presentation.
As you might have noticed, when querying the Claimer's DID, we executed a new Light DID
generation function. The reason for this is that light DID
s, which are not stored on-chain, are open to being regenerated each time.
The verificationFlow()
function also runs the presentation
creation function. Therefore, we need the Claimer's DID.
Loading Attester's DID Information:
const attesterDid = process.env.ATTESTER_DID_URI as Kilt.DidUri
This line retrieves the Attester's DID URI from the .env
file.
Loading the Credential:
const credential = JSON.parse(process.env.CLAIMER_CREDENTIAL as string)
This line loads the Credential
that we previously obtained by running our code in the attestation
section and saved in the .env
file.
Initiating the Verification Flow:
await verificationFlow(
credential,
async ({ data }) => ({
signature: authentication.sign(data),
keyType: authentication.type,
keyUri: `${claimerDid.uri}${claimerDid.authentication[0].id}`
}),
[attesterDid]
)
This line calls the previously defined verificationFlow
function to start the verification process.
Error Handling:
} catch (e) {
console.log('Error in the verification flow')
throw e
}
This section catches any errors that might occur during the verification process and prints an error message.
When we look at the entirety of the code:
import { config as envConfig } from 'dotenv'
import * as Kilt from '@kiltprotocol/sdk-js'
import { createPresentation } from './claimer/createPresentation'
import { generateKeypairs } from './claimer/generateKeypairs'
import { generateLightDid } from './claimer/generateLightDid'
function getChallenge(): string {
return Kilt.Utils.UUID.generate()
}
// Verifies validity, ownership & attestation.
async function verifyPresentation(
presentation: Kilt.ICredentialPresentation,
challenge: string,
trustedAttesterUris: Kilt.DidUri[]
): Promise<boolean> {
Kilt.ConfigService.get('api')
try {
const { revoked, attester } = await Kilt.Credential.verifyPresentation(
presentation,
{ challenge }
)
if (revoked) {
return false
}
return trustedAttesterUris.includes(attester)
} catch {
return false
}
}
export async function verificationFlow(
credential: Kilt.ICredential,
signCallback: Kilt.SignCallback,
trustedAttesterUris: Kilt.DidUri[] = []
) {
const challenge = getChallenge()
const presentation = await createPresentation(
credential,
signCallback,
challenge
)
const isValid = await verifyPresentation(
presentation,
challenge,
trustedAttesterUris
)
if (isValid) {
console.log('Verification successful! You are allowed to enter the club 🎉')
} else {
console.log('Verification failed! 🚫')
}
}
if (require.main === module) {
;(async () => {
envConfig()
try {
await Kilt.connect(process.env.WSS_ADDRESS as string)
const claimerDidMnemonic = process.env.CLAIMER_DID_MNEMONIC as string
const { authentication } = generateKeypairs(claimerDidMnemonic)
const claimerDid = generateLightDid(claimerDidMnemonic)
const attesterDid = process.env.ATTESTER_DID_URI as Kilt.DidUri
const credential = JSON.parse(process.env.CLAIMER_CREDENTIAL as string)
await verificationFlow(
credential,
async ({ data }) => ({
signature: authentication.sign(data),
keyType: authentication.type,
keyUri: `${claimerDid.uri}${claimerDid.authentication[0].id}`
}),
[attesterDid]
)
} catch (e) {
console.log('Error in the verification flow')
throw e
}
})()
}
The functions, in order:
- Packages are imported.
- A
challenge
is generated for the Claimer to prove they possess thecredential
. - A
Presentation
verification function is written, which outputs eitherTrue
orFalse
. - The
verificationFlow()
function is written to execute all processes from start to finish. - A function is written to specify what the function will do when run on its own.
Let's Run the Code!
To run the code, make sure you are in the kilt-rocks
directory in the terminal. Then, you can execute the following command:
yarn ts-node verify.ts
You've successfully executed all the entities with all the codes! I'm proud of you! Let's create projects together with KILT-SDK!