Skip to main content

Kilt DIDs

A KILT Decentralised Identifier (DID) is a string uniquely identifying each KILT user. A DID can be thought of as a container of different keys that are all under the control of the same DID subject (see the DID Core spec for more information).

The simplest type of KILT DID is a light DID, called this way because it can be generated and used offline without requiring any Internet connection (hence any connection with the KILT blockchain at all). Although very cheap, light DIDs are not very flexible and are suitable for lower-security use cases. In more complex use cases, a full DID is more indicated, which allows the subject to store several different keys (and key types) and replace them over time, with the help of the KILT blockchain.

Light DIDs

An example of a light KILT DID is the following:

did:kilt:light:014sxSYXakw1ZXBymzT9t3Yw91mUaqKST5bFUEjGEpvkTuckar

Beyond the standard prefix did:kilt:, the light: component indicates that this DID is a light DID, hence it can be resolved and utilized offline.

Light DIDs optionally support the specification of an encryption key (of one of the supported key types) and some service endpoints, which are both serialised, encoded and added at the end of the DID, like the following:

did:kilt:light:014sxSYXakw1ZXBymzT9t3Yw91mUaqKST5bFUEjGEpvkTuckar:oWFlomlwdWJsaWNLZXlYILu7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7ZHR5cGVmeDI1NTE5

Creating a light DID

To create a light DID, there needs to be a keystore instance that conforms to the Keystore interface. For the sake of ease of use, this package includes a demo keystore which can be used to generate key pairs that are kept in memory and disappear at the end of the program execution.

warning

Using the demo keystore in production is highly discouraged as all the keys are kept in the memory and easily retrievable by malicious actors.

The following is an example of how to create a light DID after creating an instance of the demo keystore.

import {
DemoKeystore,
LightDidDetails,
SigningAlgorithms,
} from '@kiltprotocol/did'

export async function main() {
// Instantiate the demo keystore.
const keystore = new DemoKeystore()

// Generate seed for the authentication key.
// For random mnemonic generation, refer to the `UUID` module of the `@kiltprotocol/utils` package.
const authenticationSeed = '0x123456789'

// Ask the keystore to generate a new keypair to use for authentication with the generated seed.
const authenticationKeyPublicDetails = await keystore.generateKeypair({
alg: SigningAlgorithms.Ed25519,
seed: authenticationSeed,
})

// Create a light DID from the generated authentication key.
const lightDID = new LightDidDetails({
authenticationKey: {
publicKey: authenticationKeyPublicDetails.publicKey,
type: DemoKeystore.getKeypairTypeForAlg(
authenticationKeyPublicDetails.alg
),
},
})
// Will print `did:kilt:light:014sxSYXakw1ZXBymzT9t3Yw91mUaqKST5bFUEjGEpvkTuckar`.
console.log(lightDID.did)
}

For cases in which also an encryption key and some service endpoints need to be added to a light DID:

import {
DemoKeystore,
LightDidDetails,
SigningAlgorithms,
EncryptionAlgorithms,
} from '@kiltprotocol/did'
import type { IDidServiceEndpoint } from '@kiltprotocol/types'

export async function main() {
const keystore = new DemoKeystore()

const authenticationSeed = '0x123456789'

const authenticationKeyPublicDetails = await keystore.generateKeypair({
alg: SigningAlgorithms.Ed25519,
seed: authenticationSeed,
})

// Generate the seed for the encryption key.
const encryptionSeed = '0x987654321'

// Use the keystore to generate a new keypair to use for encryption.
const encryptionKeyPublicDetails = await keystore.generateKeypair({
alg: EncryptionAlgorithms.NaclBox,
seed: encryptionSeed,
})

const serviceEndpoints: IDidServiceEndpoint[] = [
{
id: 'my-service',
types: ['CollatorCredential'],
urls: ['http://example.domain.org'],
},
]

// Generate the KILT light DID with the information generated.
const lightDID = new LightDidDetails({
authenticationKey: {
publicKey: authenticationKeyPublicDetails.publicKey,
type: DemoKeystore.getKeypairTypeForAlg(
authenticationKeyPublicDetails.alg
),
},
encryptionKey: {
publicKey: encryptionKeyPublicDetails.publicKey,
type: DemoKeystore.getKeypairTypeForAlg(encryptionKeyPublicDetails.alg),
},
serviceEndpoints,
})

// Will print `did:kilt:light:014sxSYXakw1ZXBymzT9t3Yw91mUaqKST5bFUEjGEpvkTuckar:omFlomlwdWJsaWNLZXlYILu7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7ZHR5cGVmeDI1NTE5YXOBo2JpZHNteS1zZXJ2aWNlLWVuZHBvaW50ZXR5cGVzgnZDb2xsYXRvckNyZWRlbnRpYWxUeXBlbVNvY2lhbEtZQ1R5cGVkdXJsc4J1aHR0cHM6Ly9teV9kb21haW4ub3JnbXJhbmRvbV9kb21haW4`.
console.log(lightDID.did)
}

Full DIDs

As mentioned above, the creation of a full DID requires interaction with the KILT blockchain. Therefore, it is necessary for the DID creation operation to be submitted by a KILT address with enough funds to pay the transaction fees and the required deposit. While transaction fees cannot be refunded, the deposit is returned when the DID is deleted from the blockchain: this is to incentivise users to clean the data from the blockchain once such data is not needed anymore.

By design, DID signatures and Substrate signatures are decoupled, meaning that the encoded and signed DID creation operation can then be signed and submitted by a different KILT account than the DID subject. This opens the path for a wider range of use cases in which, for instance, a service provider might be willing to offer a DID-as-a-Service option for its customers.

An example of a full DID is the following:

did:kilt:4rp4rcDHP71YrBNvDhcH5iRoM3YzVoQVnCZvQPwPom9bjo2e

Here, there is no light: component, which indicates that the DID is a full DID and that the keys associated with it must not be derived from the DID identifier but must be retrieved from the KILT blockchain.

Beyond an authentication key, an encryption key, and service endpoints, a full DID also supports an attestation key, which must be used to write CTypes and attestations on the blockchain, and a delegation key, which must be used to write delegations on the blockchain.

Creating and anchoring a full DID

The following is an example of how to create and write on blockchain a full DID that specifies only an authentication key.

import { KeyringPair } from '@polkadot/keyring/types'

import { BlockchainUtils } from '@kiltprotocol/chain-helpers'
import { init, disconnect } from '@kiltprotocol/core'
import { DefaultResolver, DemoKeystore, DidUtils, SigningAlgorithms } from '@kiltprotocol/did'
import { KeyRelationship, SubscriptionPromise, IDidResolvedDetails } from '@kiltprotocol/types'

export async function main(
keystore: DemoKeystore,
kiltAccount: KeyringPair,
resolveOn: SubscriptionPromise.ResultEvaluator,
// Generate seed for the authentication key.
authenticationSeed: string
): Promise<IDidResolvedDetails> {
// Initialise connection to the public KILT test network.
await init({ address: 'wss://peregrine.kilt.io' })

// Ask the keystore to generate a new keypair to use for authentication.
const authenticationKeyPublicDetails = await keystore.generateKeypair({
seed: authenticationSeed,
alg: SigningAlgorithms.Ed25519,
})

// Generate the DID-signed creation extrinsic.
// The extrinsic is unsigned and contains the DID creation operation signed with the DID authentication key.
// The second argument, the submitter account, ensures that only an entity authorised by the DID subject
// can submit the extrinsic to the KILT blockchain.
const { extrinsic, did } = await DidUtils.writeDidFromPublicKeys(keystore, kiltAccount.address, {
[KeyRelationship.authentication]: {
publicKey: authenticationKeyPublicDetails.publicKey,
type: DemoKeystore.getKeypairTypeForAlg(authenticationKeyPublicDetails.alg),
},
})
// Will print `did:kilt:4sxSYXakw1ZXBymzT9t3Yw91mUaqKST5bFUEjGEpvkTuckar`.
console.log(did)

// Submit the DID creation tx to the KILT blockchain after signing it with the KILT account specified in the creation operation.
await BlockchainUtils.signAndSubmitTx(extrinsic, kiltAccount, {
resolveOn,
})

// Retrieve the newly created DID from the KILT blockchain.
const fullDid = await DefaultResolver.resolveDoc(did)

await disconnect()
if (fullDid === null) {
throw "Could not find DID document for the given identifier"
}
return fullDid
}

If additional keys or service endpoints are to be specified, then they can be included in the DID create operation.

import { KeyringPair } from '@polkadot/keyring/types'
import { BlockchainUtils } from '@kiltprotocol/chain-helpers'
import { init, disconnect } from '@kiltprotocol/core'
import { DefaultResolver, DemoKeystore, DidUtils, SigningAlgorithms, EncryptionAlgorithms } from '@kiltprotocol/did'
import { KeyRelationship, SubscriptionPromise, IDidResolvedDetails } from '@kiltprotocol/types'

export async function main(
keystore: DemoKeystore,
kiltAccount: KeyringPair,
resolveOn: SubscriptionPromise.ResultEvaluator,
// Generate seed for the authentication key.
authenticationSeed: string,
encryptionSeed: string
): Promise<IDidResolvedDetails> {
await init({ address: 'wss://peregrine.kilt.io' })

// Ask the keystore to generate a new keypair to use for authentication.
const authenticationKeyPublicDetails = await keystore.generateKeypair({
seed: authenticationSeed,
alg: SigningAlgorithms.Ed25519,
})

// Ask the keystore to generate a new keypar to use for encryption.
const encryptionKeyPublicDetails = await keystore.generateKeypair({
seed: encryptionSeed,
alg: EncryptionAlgorithms.NaclBox,
})

// Generate the DID-signed creation extrinsic with the provided keys.
const { extrinsic, did } = await DidUtils.writeDidFromPublicKeysAndServices(
keystore,
kiltAccount.address,
{
[KeyRelationship.authentication]: {
publicKey: authenticationKeyPublicDetails.publicKey,
type: DemoKeystore.getKeypairTypeForAlg(authenticationKeyPublicDetails.alg),
},
[KeyRelationship.keyAgreement]: {
publicKey: encryptionKeyPublicDetails.publicKey,
type: DemoKeystore.getKeypairTypeForAlg(encryptionKeyPublicDetails.alg),
},
},
[
{
id: 'my-service',
types: ['service-type'],
urls: ['https://www.example.com'],
},
]
)
// Will print `did:kilt:4sxSYXakw1ZXBymzT9t3Yw91mUaqKST5bFUEjGEpvkTuckar`.
console.log(did)

await BlockchainUtils.signAndSubmitTx(extrinsic, kiltAccount, {
resolveOn,
})

const fullDid = await DefaultResolver.resolveDoc(did)

await disconnect()
if (fullDid === null) {
throw 'Could not find DID document for the given identifier'
}
return fullDid
}

Updating a full DID

Once anchored on the KILT blockchain, a KILT full DID can be updated by signing the operation with a valid authentication key. For instance, the following snippet shows how to update the authentication key of a full DID and set it to a new sr25519 key.

import { KeyringPair } from '@polkadot/keyring/types'

import { BlockchainUtils } from '@kiltprotocol/chain-helpers'
import { DefaultResolver, DemoKeystore, DidChain, SigningAlgorithms, FullDidDetails } from '@kiltprotocol/did'
import { KeyRelationship, KeystoreSigner, SubscriptionPromise } from '@kiltprotocol/types'
import { init, disconnect } from '@kiltprotocol/core'

export async function main(
keystore: DemoKeystore,
kiltAccount: KeyringPair,
resolveOn: SubscriptionPromise.ResultEvaluator,
authenticationSeed: string,
fullDid: FullDidDetails
) {
await init({ address: 'wss://peregrine.kilt.io' })

// Ask the keystore to generate a new keypair to use for authentication.
const newAuthenticationKeyPublicDetails = await keystore.generateKeypair({
seed: authenticationSeed,
alg: SigningAlgorithms.Ed25519,
})

// Create a DID operation to replace the authentication key with the new one generated.
const didUpdateExtrinsic = await DidChain.getSetKeyExtrinsic(KeyRelationship.authentication, {
publicKey: newAuthenticationKeyPublicDetails.publicKey,
type: DemoKeystore.getKeypairTypeForAlg(newAuthenticationKeyPublicDetails.alg),
})

// Sign the DID operation using the old DID authentication key.
// This results in an unsigned extrinsic that can be then signed and submitted to the KILT blockchain by the account
// authorised in this operation, Alice in this case.
const didSignedUpdateExtrinsic = await fullDid.authorizeExtrinsic(
didUpdateExtrinsic,
keystore as KeystoreSigner<string>,
kiltAccount.address
)

// Submit the DID update tx to the KILT blockchain after signing it with the authorised KILT account.
await BlockchainUtils.signAndSubmitTx(didSignedUpdateExtrinsic, kiltAccount, {
resolveOn,
})

// Get the updated DID Doc
const updatedDidDetails = (await (await DefaultResolver.resolveDoc(fullDid.did))?.details) as FullDidDetails
if (updatedDidDetails === undefined) {
throw 'We just created the did'
}

// Remove the service endpoint with id `my-service` added upon creation in the previous section.
const didRemoveExtrinsic = await DidChain.getRemoveEndpointExtrinsic('my-service')

// Sign the DID operation using the new authentication key.
const didSignedRemoveExtrinsic = await updatedDidDetails.authorizeExtrinsic(
didRemoveExtrinsic,
keystore as KeystoreSigner<string>,
kiltAccount.address
)

// Submit the signed operation as before.
await BlockchainUtils.signAndSubmitTx(didSignedRemoveExtrinsic, kiltAccount, {
resolveOn,
})

await disconnect()
}

Deleting a full DID

Once not needed anymore, it is recommended to remove the DID details from the KILT blockchain. The following snippet shows how to do it:

import { KeyringPair } from '@polkadot/keyring/types'

import { BlockchainUtils } from '@kiltprotocol/chain-helpers'
import { KeystoreSigner, SubscriptionPromise } from '@kiltprotocol/types'
import { init, disconnect } from '@kiltprotocol/core'
import { DemoKeystore, DidChain, FullDidDetails } from '@kiltprotocol/did'

export async function main(
keystore: DemoKeystore,
kiltAccount: KeyringPair,
resolveOn: SubscriptionPromise.ResultEvaluator,
fullDid: FullDidDetails
) {
await init({ address: 'wss://peregrine.kilt.io' })

// Create a DID deletion operation. We specify the number of endpoints currently stored under the DID because
// of the upper computation limit required by the blockchain runtime.
const endpointsCountForDid = await DidChain.queryEndpointsCounts(fullDid.did)
const didDeletionExtrinsic = await DidChain.getDeleteDidExtrinsic(endpointsCountForDid)

// Sign the DID deletion operation using the DID authentication key.
// This results in an unsigned extrinsic that can be then signed and submitted to the KILT blockchain by the account
// authorised in this operation, Alice in this case.
const didSignedDeletionExtrinsic = await fullDid.authorizeExtrinsic(
didDeletionExtrinsic,
keystore as KeystoreSigner<string>,
kiltAccount.address
)

await BlockchainUtils.signAndSubmitTx(didSignedDeletionExtrinsic, kiltAccount, {
resolveOn,
})

await disconnect()
}

Claiming back a DID deposit

As the creation of a full DID requires a deposit that will lock from the balance of the creation tx submitter (which, once again, might differ from the DID subject), the deposit owner is allowed to claim the deposit back by deleting the DID associated with its deposit. This is the reason why full DID creation operations require the tx submitter to be included and signed by the DID subject: to make sure that only the DID subject themselves and the authorised account are ever able to delete the DID information from the chain.

Claiming back the deposit of a DID is semantically equivalent to deleting the DID, with the difference that the extrinsic to claim the deposit can only be called by the deposit owner and does not require a valid signature by the DID subject:

import { KeyringPair } from '@polkadot/keyring/types'

import { DidChain, FullDidDetails } from '@kiltprotocol/did'
import { BlockchainUtils } from '@kiltprotocol/chain-helpers'
import { SubscriptionPromise } from '@kiltprotocol/types'
import { init, disconnect } from '@kiltprotocol/core'

export async function main(
kiltAccount: KeyringPair,
resolveOn: SubscriptionPromise.ResultEvaluator,
fullDid: FullDidDetails
) {
await init({ address: 'wss://peregrine.kilt.io' })

// Generate the submittable extrinsic to claim the deposit back, by including the DID identifier for which the deposit needs to be returned and the count of service endpoints to provide an upper bound to the computation of the extrinsic execution.
const endpointsCountForDid = await DidChain.queryEndpointsCounts(fullDid.did)
const depositClaimExtrinsic = await DidChain.getReclaimDepositExtrinsic(fullDid.did.replace("did:kilt:", ""), endpointsCountForDid)

// The submission will fail if `aliceKiltAccount` is not the owner of the deposit associated with the given DID identifier.
await BlockchainUtils.signAndSubmitTx(depositClaimExtrinsic, kiltAccount, {
resolveOn,
})

await disconnect()
}

Migrating a light DID to a full DID

The migration of a DID means that a light, off-chain DID is anchored to the KILT blockchain, supporting all the features that full DIDs provide. In the current version (v1) of the KILT DID protocol, a light DID of the form did:kilt:light:004sxSYXakw1ZXBymzT9t3Yw91mUaqKST5bFUEjGEpvkTuckar would become a full DID of the form did:kilt:4sxSYXakw1ZXBymzT9t3Yw91mUaqKST5bFUEjGEpvkTuckar. Note that the identifier of the two DIDs, apart from the initial 00 sequence of the light DID, are equal since both DIDs are derived from the same KILT account.

Once a light DID is migrated, all the attested claims (i.e., attestations) generated using that light DID can only be presented using the migrated on-chain DID. This is by design, as it is assumed that the user had valid reasons to migrate the DID on chain, and as on-chain DIDs offer greater security guarantees, KILT will reject light DID signatures for presentations even in case the original claim in the attestation was generated with that off-chain DID.

The following code shows how to migrate a light DID to a full DID. Attested claim presentations and verifications remain unchanged as adding support for DID migration does not affect the public API that the SDK exposes.

import { KeyringPair } from '@polkadot/keyring/types'
import { hexToU8a } from '@polkadot/util'

import { DefaultResolver, DemoKeystore, LightDidDetails, DidUtils, SigningAlgorithms } from '@kiltprotocol/did'
import { init, disconnect } from '@kiltprotocol/core'
import { SubscriptionPromise, KeyRelationship, IDidResolvedDetails } from '@kiltprotocol/types'
import { BlockchainUtils } from '@kiltprotocol/chain-helpers'

export async function main(
keystore: DemoKeystore,
kiltAccount: KeyringPair,
resolveOn: SubscriptionPromise.ResultEvaluator,
authenticationSeed: string
): Promise<IDidResolvedDetails> {
await init({ address: 'wss://kilt-peregrine-k8s.kilt.io' })

// Ask the keystore to generate a new keypair to use for authentication.
const authenticationKeyPublicDetails = await keystore.generateKeypair({
seed: authenticationSeed,
alg: SigningAlgorithms.Ed25519,
})

// create a light DID
const lightDidDetails = new LightDidDetails({
authenticationKey: {
publicKey: authenticationKeyPublicDetails.publicKey,
type: DemoKeystore.getKeypairTypeForAlg(authenticationKeyPublicDetails.alg),
},
})

// Generate the DID creation extrinsic with the authentication key taken from the light DID.
const pubAuthKey = lightDidDetails.getKeys(KeyRelationship.authentication)[0]
if (pubAuthKey === undefined) {
throw 'We just created the did with an authentication key'
}

const { extrinsic, did } = await DidUtils.writeDidFromPublicKeys(keystore, kiltAccount.address, {
[KeyRelationship.authentication]: {
publicKey: hexToU8a(pubAuthKey.publicKeyHex),
type: DemoKeystore.getKeypairTypeForAlg(pubAuthKey.type),
},
})

// The extrinsic can then be submitted by the authorised account as usual.
await BlockchainUtils.signAndSubmitTx(extrinsic, kiltAccount, {
resolveOn,
})

// The full DID details can then be resolved after they have been stored on the chain.
const fullDid = await DefaultResolver.resolveDoc(did)

await disconnect()
if (fullDid === null) {
throw 'Could not find DID document for the given identifier'
}
return fullDid
}