Skip to main content

Verifying a Credential

This section demonstrates how to build a basic verifier according to the Credential API Specification. Before continuing, please make sure you have already set up the communication session and Well-Known DID Configuration.

This guide explains specifically how a web server can request a credential presentation from one of its visitors (the claimer). After the browser extension verified the Well-Known DID Configuration and the encrypted communication channel between the extension and the server was established, the web server can request the credential presentation. This is a two step process.

First the server sends a message to the extension that request the presentation of a credential. Since we don't want to see just any credential, but expect specific content, we also require that the credential conforms to a specific CType. When the extension receives the request, it will prompt the user to select a credential that should be presented to the server. The user can also choose to reject this request and not to show any presentation.

The second step is to verify the received credential. After the user chooses the credential, the extension will pass a response to the website which contains the credential presentation. The server of that website needs to ensure that this presentation is actually valid.

Request a Credential Presentation

Before the website can request a credential, it needs the type of credential (CType) that it wants to request. In this guide the website requests an email address that is owned by the DID. For that it uses the Email CType. You can search through existing CTypes in the CType Index.

const emailCType: Kilt.ICType = {
$id: 'kilt:ctype:0xae5bc64e500eb576b7b137288cec5d532094e103be46872f1ad54641e477d9fe',
$schema:
'ipfs://bafybeiah66wbkhqbqn7idkostj2iqyan2tstc4tpqt65udlhimd7hcxjyq/',
title: 'Email',
properties: {
Email: {
type: 'string'
}
},
type: 'object',
additionalProperties: false
}

After settled on a CType, the server can build the request for the visitor. Since we want to ensure that the presentation of the credential is fresh, the server first has to create a random challenge. The presentation must include this challenge and since it's random, the presentation must be created and signed from scratch. This ensures that it's not possible to record a presentation and just send this, pretending to be the owner of the DID. The challenge can be generated using the polkadot crypto utilities:

import { randomAsHex } from '@polkadot/util-crypto'

// Store somewhere in the backend.
export function generateRequestChallenge() {
return randomAsHex(24)
}

With the challenge the server can construct the request-credential message. The request is sent to the light DID (claimerSessionDid) that is used to encrypt the messages (see Session for more information).

import * as Kilt from '@kiltprotocol/sdk-js'

export function main({
verifierDidUri,
session,
requestChallenge
}: {
verifierDidUri: Kilt.DidUri
session: {
encryptionKeyUri: Kilt.DidResourceUri
}
requestChallenge: string
}): {
message: Kilt.IMessage
} {
// The `session` was created earlier in your frontend. Only the session DID URI is sent to your backend.
const { did: claimerSessionDidUri } = Kilt.Did.parse(session.encryptionKeyUri)

// The message is constructed in your backend
const message = Kilt.Message.fromBody(
{
content: {
cTypes: [
{
// the hash of the email CType
cTypeHash:
'0x3291bb126e33b4862d421bfaa1d2f272e6cdfc4f96658988fbcffea8914bd9ac',
requiredProperties: ['Email']
}
],
challenge: requestChallenge
},
type: 'request-credential'
},
verifierDidUri,
claimerSessionDidUri
)

return { message }
}
Privacy

The credential itself doesn't need to be issued to this DID since the light DID is only used to encrypt the messages. We don't use the full DID of the claimer to establish the encrypted communication, so that the claimer first can ensure the origin of the request-credential message.

After the server has built the message object, it must encrypt the message for the claimer. Once the message is encrypted the server can pass on the message to the extension.

import * as Kilt from '@kiltprotocol/sdk-js'

export async function main({
message,
verifierDidUri,
verifierKeys,
session
}: {
message: Kilt.IMessage
verifierDidUri: Kilt.DidUri
verifierKeys: {
authentication: Kilt.KiltKeyringPair
encryption: Kilt.KiltEncryptionKeypair
attestation: Kilt.KiltKeyringPair
delegation: Kilt.KiltKeyringPair
}
session: {
encryptionKeyUri: Kilt.DidResourceUri
send: (message: Kilt.IEncryptedMessage) => Promise<void>
}
}) {
const { document: verifierDidDoc } = await Kilt.Did.resolve(verifierDidUri)
if (!verifierDidDoc) {
throw new Error('The verifier DID must exist')
}
const verifierEncryptionKey = verifierDidDoc.keyAgreement?.[0]
if (!verifierEncryptionKey) {
throw new Error('The verifier DID must have a key agreement key')
}

// Create a callback that uses the DID encryption key to encrypt the message.
const encryptCallback: Kilt.EncryptCallback = async ({
data,
peerPublicKey
}) => {
const { box, nonce } = Kilt.Utils.Crypto.encryptAsymmetric(
data,
peerPublicKey,
verifierKeys.encryption.secretKey
)
return {
data: box,
nonce,
keyUri: `${verifierDidDoc.uri}${verifierEncryptionKey.id}`
}
}

const encryptedMessage = await Kilt.Message.encrypt(
message,
encryptCallback,
session.encryptionKeyUri
)

// Finally, send the encrypted message to the extension.
// While the above code will be executed on the server, this must happen in
// the frontend since it's dispatching the message to the browser extension.
await session.send(encryptedMessage)
}

Verify the Presentation

After sending the request-credential message to the extension, the verifier listens for a message of type submit-credential in response.

After the response from the extension is received, forwarded to the server and decrypted, the verifier must check that it has the expected CType and that it contains a valid credential. Since everyone can run an attestation service, you need to make sure that you also verify that the attester is trusted.

async function processInBackend(message: Kilt.IEncryptedMessage) {
// Create a callback that uses the DID encryption key to decrypt the message.
const decryptCallback: Kilt.DecryptCallback = async ({
data,
nonce,
peerPublicKey
}) => {
const result = Kilt.Utils.Crypto.decryptAsymmetric(
{ box: data, nonce },
peerPublicKey,
verifierKeys.encryption.secretKey
)
if (!result) {
throw new Error('Cannot decrypt')
}
return {
data: result
}
}

const decryptedMessage = await Kilt.Message.decrypt(
message,
decryptCallback
)

if (decryptedMessage.body.type !== 'submit-credential') {
throw new Error('Unexpected message type')
}
const credential = decryptedMessage.body.content[0]

const { revoked, attester } =
await Kilt.Credential.verifyPresentation(credential)

if (revoked) {
throw new Error("Credential has been revoked and hence it's not valid.")
}
if (isTrustedAttester(attester)) {
console.log(
"The claim is valid. Claimer's email:",
credential.claim.contents.Email
)
}
}

// In the frontend we wait for messages from the browser extension and forward them to the server.
await session.listen(async (message: Kilt.IEncryptedMessage) => {
processInBackend(message)
})

That's it! Your verifier has successfully requested and verified a credential.