Ana içeriğe geç

Decentralized Identifiers (DIDs) 🌐

Now that we've set up our account as explained in the previous section, we can start creating a DID for our Attester. 🚀

DIDs can represent an individual, organization, or machine. 🧑‍🤝‍🧑🏢🤖

What are DIDs? 🤔
  • In our current system, we have to rely on a central authority (e.g., the government 👮‍♂️) or monopolized services (like Google, Facebook, etc. 🏢) for our digital identity.
  • DID stands for Decentralized Identifier. With DIDs, you can be the captain of your digital ship! ⚓️🔗
  • It enhances privacy and security. 🕵️‍♂️🔒

KILT DIDs are unique identifiers assigned to each user. 🆔 You can store your DIDs within the KILT chain. 🗃️

KILT DID Encryption Support 🔐
  • Authentication Keypair 🛡️
  • Key-agreement Keypair 🤝
  • Assertion-method Keypair
  • Capability-delegation Keypair 📝

Account vs DID 🤷‍♂️

DIDs need to be recorded on the chain. 📝 There's always an account that pays the deposit and initiates the DID verification process. 💰

Creating a DID 🛠️

Are All DIDs the Same?

There are 2 types of DIDs we use in KILT. One is the Light DID, while the other is the Full DID.

Light DID

Below is an example of a light KILT DID:

did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS

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

Light DIDs support one of the supported key types as an encryption key and optionally services. These are serialized, encoded, and appended to the DID, as follows:

did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z1ERkVVjngcarMbJn6YssB1PYULescQneSSEfCTJwYbzT2aK8fzH5WPsp3G4UVuLWWfsTayketnFV74YCnyboHBUvqEs6J8jdYY5dK2XeqCCs653Sf9XVH4RN2WvPrDFZXzzKf3KigvcaE7kkaEwLZvcas3U1M2ZDZCajDG71winwaRNrDtcqkJL9V6Q5yKNWRacw7hJ58d

Full DID

As mentioned above, creating a full DID requires interaction with the KILT blockchain. Therefore, the DID creation transaction should be sent by a KILT address that has sufficient funds to pay the transaction fees and the required deposit.

Below is an example of a full KILT DID:

did:kilt:4rp4rcDHP71YrBNvDhcH5iRoM3YzVoQVnCZvQPwPom9bjo2e

There's no light: component here, indicating that the DID is a full DID and the keys associated with this DID are not derived from the DID identifier but need to be fetched from the KILT blockchain.

In addition to an authentication key, an encryption key, and services, a full DID also supports an assertion key that needs to be used to write CTypes and attestations on the blockchain and a delegation key that needs to be used to write delegations on the blockchain.

alternative text

Since we'll be interacting with the chain for our Attester, we'll be creating a full DID.

To create a DID, we can reuse the keyring values we used to create the account. 🔄 For the Attester, we need four keys. 🔑


Generating Key Pairs

Importing Modules

attester/generateKeypairs.ts
import * as Kilt from '@kiltprotocol/sdk-js'
import {
blake2AsU8a,
keyExtractPath,
keyFromPath,
mnemonicGenerate,
mnemonicToMiniSecret,
sr25519PairFromSeed
} from '@polkadot/util-crypto'
import { generateAccount } from './generateAccount'

As we started with other codes, we first integrated the packages into our project. Looking at our packages one by one:

  • First, we integrate the sdk-js package into our code to access KILT SDK functions.
  • Then, we access several methods from the polkadot library to create the keypair pairs.
  • Finally, we access the generateAccount method we exported from the account creation code we wrote on the previous page.

generateKeyAgreement Function

attester/generateKeypairs.ts
function generateKeyAgreement(mnemonic: string) {
const secretKeyPair = sr25519PairFromSeed(mnemonicToMiniSecret(mnemonic))
const { path } = keyExtractPath('//did//keyAgreement//0')
const { secretKey } = keyFromPath(secretKeyPair, path, 'sr25519')
return Kilt.Utils.Crypto.makeEncryptionKeypairFromSeed(blake2AsU8a

(secretKey))
}

After loading our packages, we can start creating our key pairs using these packages.

We can start by writing a function to perform this operation. This function will take our mnemonic account key under the name mnemonic. Looking at the operations inside one by one:

  • secretKeyPair: We entered the mnemonic key into the function, but we need to obtain both the public and private keys using this key. First, we enter our mnemonic key into the mnemonicToMiniSecret() function. This function allows us to obtain another mini key from our key. The newly formed key becomes the secret and public key pair structure we want with the sr25519PairFromSeed() function.
  • path: After creating our key pair, the next operation is to determine where this key will be stored to create the secretKey. At the same time, this operation indicates for what purpose the key will be used. In our code, with the //did//keyAgreement//0 structure, it is stated that the key will be used as a key agreement.
  • secretKey: This operation takes the two variables we introduced earlier and the wallet type - for us, this value is sr25519 for now - to create a secret key. This process is implemented with the keyFromPath method.
  • makeEncryptionKeypairFromSeed: Finally, there's the task of outputting the key pairs. We return this operation with the Utils.Crypto.makeEncryptionKeypairFromSeed() method in KILT SDK and the function is completed.
Why Did We Write This Code?
  1. Security: When creating a decentralized identity system, key agreements are necessary for secure communication.
  2. User Control: By creating their keys, users can control their identities and data.
  3. Flexibility: This function is designed to support different key types, making it suitable for various applications.
  4. Integration: Provides a key creation mechanism compatible with existing blockchain technologies like Kilt and Polkadot.

generateKeypairs Function

Having previously set up the mechanism to create key pairs with the functions we've defined, we can now define the function required to generate these key pairs. This function will produce four key pairs for us using the secret keys we input.

attester/generateKeypairs.ts
export function generateKeypairs(mnemonic = mnemonicGenerate()) {

We can start by defining our function. This function takes a mnemonic password. However, the structure mnemonicGenerate() might be confusing. This structure creates a new mnemonic password if no mnemonic password input is provided.

attester/generateKeypairs.ts
const { account } = generateAccount(mnemonic);

When we enter the code, we first create an account from the mnemonic password using the generateAccount() function. We will use this account to produce the key pairs later.

Let's Start Creating the Key Pairs

Now, we can produce our four key pairs.

Authentication Key Pair
attester/generateKeypairs.ts
const authentication = {
...account.derive('//did//0'),
type: 'sr25519'
} as Kilt.KiltKeyringPair;

Going line by line:

  • account.derive('//did//0'): Derives an authentication pair from the account.
  • type: 'sr25519': Specifies the type of the key pair.
  • as Kilt.KiltKeyringPair: Specifies that the type of the key pair is in Kilt's key pair type.
Authentication Keypair Ready!

We've prepared our Authentication key pair. Now, with this key, we can prepare claims and present verified credentials.

Assertion Key Pair
attester/generateKeypairs.ts
const assertionMethod = {
...account.derive('//did//assertion//0'),
type: 'sr25519'
} as Kilt.KiltKeyringPair;

As in the function above, an Assertion key pair of type sr25519 is produced from the account for verifying claims.

Capability Delegation Key Pair
attester/generateKeypairs.ts
const capabilityDelegation = {
...account.derive('//did//delegation//0'),
type: 'sr25519'
} as Kilt.KiltKeyringPair;

With our next variable, a key pair is created for delegating capabilities or permissions to another party. This key is used to write delegations on the KILT chain.

Key Agreement Key Pair
attester/generateKeypairs.ts
const keyAgreement = generateKeyAgreement(mnemonic);

Finally, the keyAgreement key pair is produced. This key is used for encrypting and decrypting messages.

Returning the Results
attester/generateKeypairs.ts
return {
authentication: authentication,
keyAgreement: keyAgreement,
assertionMethod: assertionMethod,
capabilityDelegation: capabilityDelegation
};

Finally, we return the key pairs we've created as the function output.

Key Pair Generation Function Completed!

For the DID we'll create for the Attester, we needed four key pairs. We derived these key pairs with the generateKeypairs function.

Let's Review the Entire Code

If we need to see the code we wrote as a whole:

attester/generateKeypairs.ts
import * as Kilt from '@kiltprotocol/sdk-js'
import {
blake2AsU8a,
keyExtractPath,
keyFromPath,
mnemonicGenerate,
mnemonicToMiniSecret,
sr25519PairFromSeed
} from '@polkadot/util-crypto'
import { generateAccount } from './generateAccount'

// Due to the lack of first-class support for this class of keys,
// we use a workaround to generate a key usable for encryption/decryption.
function generateKeyAgreement(mnemonic: string) {
const secretKeyPair = sr25519PairFromSeed(mnemonicToMiniSecret(mnemonic))
const { path } = keyExtractPath('//did//keyAgreement//0')
const { secretKey } = keyFromPath(secretKeyPair, path, 'sr25519')
return Kilt.Utils.Crypto.makeEncryptionKeypairFromSeed(blake2AsU8a(secretKey))
}

export function generateKeypairs(mnemonic = mnemonicGenerate()) {
const { account } = generateAccount(mnemonic)

const authentication = {
...account.derive('//did//0'),
type: 'sr25519'
} as Kilt.KiltKeyringPair

const assertionMethod = {
...account.derive('//did//assert

ion//0'),
type: 'sr25519'
} as Kilt.KiltKeyringPair

const capabilityDelegation = {
...account.derive('//did//delegation//0'),
type: 'sr25519'
} as Kilt.KiltKeyringPair

const keyAgreement = generateKeyAgreement(mnemonic)

return {
authentication: authentication,
keyAgreement: keyAgreement,
assertionMethod: assertionMethod,
capabilityDelegation: capabilityDelegation
}
}

Looking at the operations in order:

  • First, we define the necessary libraries.
  • Then, we extract some secret keys from the mnemonic password.
  • Finally, we define the generateKeypairs function and specify all key pairs as the function output.

Now that we've gathered all the keys necessary for creating our DID, we can start creating our DIDs on the chain.

Creating DID Using Key Pairs

To create a DID, we first need to initiate all processes. Then, we can pull the account we created in the previous section (Attester Account) into our code. This account was created to pay the DID registration fees. Finally, we can announce the necessary transfer to save the DID.

Importing Modules

attester/generateDid.ts
import { config as envConfig } from 'dotenv'
import * as Kilt from '@kiltprotocol/sdk-js'

import { generateAccount } from './generateAccount'
import { generateKeypairs } from './generateKeypairs'

As we did before, we add the libraries to our code. Looking at the new and different libraries here:

  • dotenv: It allows us to load the environment variables we've set (in our case, the mnemonic password, etc.) into our code.
  • generateAccount and generateKeypairs: They allow us to take the functions exported from the codes we previously created in the attester folder.

createFullDid Function

attester/generateDid.ts
export async function createFullDid(
submitterAccount: Kilt.KiltKeyringPair
): Promise<{
mnemonic: string
fullDid: Kilt.DidDocument
}> {

We can start the processes by defining the function for the DID we will create on the chain. This asynchronous function takes a submitterAccount of type Kilt.KiltKeyringPair and returns a Promise. This Promise contains a mnemonic and a Kilt.DidDocument.

Connecting to Kilt API and Generating Mnemonic
attester/generateDid.ts
const api = Kilt.ConfigService.get('api')
const mnemonic = Kilt.Utils.Crypto.mnemonicGenerate()

Upon entering the function, we are greeted with connecting to the API and generating a mnemonic password. DIDs have their unique mnemonic passwords. That's why this password is created in this section.

Creating Key Pairs
attester/generateDid.ts
const {
authentication,
keyAgreement,
assertionMethod,
capabilityDelegation
} = generateKeypairs(mnemonic)

In the code file we wrote earlier, we had written the generateKeypairs function that allows us to create the necessary key pairs for DIDs. This function outputs four keys and takes a mnemonic key. We can call the generateKeypairs() function by entering the mnemonic key we created in the previous line. As a result of this process, we can save our four passwords to variables in order.

Executing the DID Creation Transfer
attester/generateDid.ts
  const fullDidCreationTx = await Kilt.Did.getStoreTx(
{
authentication: [authentication],
keyAgreement: [keyAgreement],
assertionMethod: [assertionMethod],
capabilityDelegation: [capabilityDelegation]
},
submitterAccount.address,
async ({ data }) => ({
signature: authentication.sign(data),
keyType: authentication.type
})
)

We need to make a transfer to create our DIDs. We can perform this transfer using the getStoreTx() function found in the KILT SDK. Looking at the parameters this function takes:

  • Key Pairs: The first parameter takes four keys as an object.
  • Account: The next parameter indicates the address to which this account belongs.
  • Signature: It performs the necessary signature processes for the transfer.

In this way, the information about the transfer we will send is prepared.

Sending the Transaction
attester/generateDid.ts
await Kilt.Blockchain.signAndSubmitTx(fullDidCreationTx, submitterAccount)

After preparing our transfer, we can send our transaction for approval using the signAndSubmitTx function.

Retrieving Information of the Created DID
attester/generateDid.ts
const didUri = Kilt.Did.getFullDidUriFromKey(authentication)
const encodedFullDid = await api.call.did.query(Kilt.Did.toChain(didUri))
const { document } = Kilt.Did.linkedInfoFromChain(encodedFullDid)

After sending the transfer to create our DID, we can now save the information of the DID created as a result of this transfer to variables. These processes, in order, are:

  • Create a URI for the created DID.
  • Query the DID document using this URI.
What is URI?

In DID (Decentralized Identifier) systems, URI (Uniform Resource Identifier) typically allows a DID to be uniquely identified. DID URIs consist of parts such as a "scheme" (e.g., did:), a "method" (e.g., kilt:), and a "method-specific identifier" (e.g., a blockchain address or another unique identity).

For example, a Kilt DID URI might look like:

did:kilt:4uJ7uq1Nj4kZ4qHv3yzZRBuW9D2b3ZRF

The parts of this URI are:

  • did:: This indicates that the URI is a DID.
  • kilt:: This indicates that the DID uses the Kilt method.
  • 4uJ7uq1Nj4kZ4qHv3yzZRBuW9D2b3ZRF: This is the unique identifier of the DID, usually a blockchain address or another unique identity.

Using this URI, you can typically find out about the keys, capabilities, and other features that the DID has.

Error Check and Return Value
attester/generateDid.ts
if (!document) {
throw new Error('Full DID was not successfully created.')
}
return { mnemonic, fullDid: document }

As we continue our processes, a mechanism that checks whether the DID was created successfully or not greets us. If the DID is created correctly, this URI is returned with the return statement.

Main Program

Now, we can write the function that indicates what we need to do when the main program runs.

attester/generateDid.ts
if (require.main === module) {
;(async () => {
envConfig()

After defining the function, we retrieve the contents of the .env file with the envConfig() function.

ipucu

The code file we wrote can both be called by different code files and run on its own. When it's to be used by different files, functions with the export structure can be used, but when running on its own, they need the require.main structure.

attester/generateDid.ts
try {

.
.
.

} catch (e) {
console.log("Error while creating attester DID")
throw e
}

We set up a try-catch structure to call the functions. This way, if the codes we write inside the try structure don't work, the catch statement catches the error and tells us.

Now we can enter the try structure.

attester/generateDid.ts
const accountMnemonic = process.env.ATTESTER_ACCOUNT_MNEMONIC as string

First, we add the attester mnemonic key we previously created from the .env file to our code.

attester/generateDid.ts
const { account } = generateAccount(accountMnemonic)

const { mnemonic, fullDid } = await createFullDid(account)

Then, from this mnemonic key, we sequentially call the generateAccount and createFullDid methods we defined earlier to obtain the account and fullDid values from this account.

attester/generateDid.ts
console.log('\nsave following to .env to continue\n');
console.error(`ATTESTER_DID_MNEMONIC="${mnemonic}"\n`);
console.error(`ATTESTER_DID_URI="${fullDid.uri}"\n`);

Finally, we provide this information to the user and complete the processes. Our code is completed in this way.

Let's Take a General Look

We wrote our code, but if we need to take a general look at what the generateDid.ts file's entire code does:

attester/generateDid.ts
import { config as envConfig } from 'dotenv'

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

import { generateAccount } from './generateAccount'
import { generateKeypairs } from './generateKeypairs'

export async function createFullDid(
submitterAccount: Kilt.KiltKeyringPair
): Promise<{
mnemonic: string
fullDid: Kilt.DidDocument
}> {
const api = Kilt.ConfigService.get('api')

const mnemonic = Kilt.Utils.Crypto.mnemonicGenerate()
const {
authentication,
keyAgreement,
assertionMethod,
capabilityDelegation
} = generateKeypairs(mnemonic)
// Get tx that will create the DID on chain and DID-URI that can be used to resolve the DID Document.
const fullDidCreationTx = await Kilt.Did.getStoreTx(
{
authentication: [authentication],
keyAgreement: [keyAgreement],
assertionMethod: [assertionMethod],
capabilityDelegation: [capabilityDelegation]
},
submitterAccount.address,
async ({ data }) => ({
signature: authentication.sign(data),
keyType: authentication.type
})
)

await Kilt.Blockchain.signAndSubmitTx(fullDidCreationTx, submitterAccount)

const didUri = Kilt.Did.getFullDidUriFromKey(authentication)
const encodedFullDid = await api.call.did.query(Kilt.Did.toChain(didUri))
const { document } = Kilt.Did.linkedInfoFromChain(encodedFullDid)

if (!document) {
throw new Error('Full DID was not successfully created.')
}

return { mnemonic, fullDid: document }
}

// Don't execute if this is imported by another file.
if (require.main === module) {
;(async () => {
envConfig()

try {
await Kilt.connect(process.env.WSS_ADDRESS as string)

// Load attester account
const accountMnemonic = process.env.ATTESTER_ACCOUNT_MNEMONIC as string
const { account } = generateAccount(accountMnemonic)
const { mnemonic, fullDid } = await createFullDid(account)

console.log('\nsave following to .env to continue\n')
console.error(`ATTESTER_DID_MNEMONIC="${mnemonic}"\n`)
console.error(`ATTESTER_DID_URI="${fullDid.uri}"\n`)
} catch (e) {
console.log('Error while creating attester DID')
throw e
}
})()
}

Looking at what this code does in general:

  • We integrate the packages and the codes we wrote earlier into the file.
  • We define the createFullDid() function and create our key pairs with the generateKeypairs() function from the generateKeypairs.ts code file.
  • With our key pairs, we approve the transfer of our DID to the chain.
  • After the transfer, we create the DID mnemonic key and uri value.
  • We run the code file, call the createFullDid() function for the attester mnemonic key we wrote in the .env file, and print the results to the screen.

Running the Code

To run our code, make sure you are in the kilt-rocks location in the terminal, and then you can run the code below.

yarn ts-node ./attester/generateDid.ts
Did you get an error?

You might have encountered many errors while running the code, but a commonly encountered error is the user not getting PILT coins from the faucet for their attester account. Make sure you have enough amount in your account and continue that way!

Let's Get the Results!

When our code runs, we can see that a new mnemonic password and uri value have been created for our DID.

Don't Forget

After running our code, we need to save the resulting Mnemonic and URI values to the .env file!

alternative text