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. 🧑🤝🧑🏢🤖
- 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. 🗃️
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 🛠️
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.
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
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 thekeypair
pairs. - Finally, we access the
generateAccount
method we exported from the account creation code we wrote on the previous page.
generateKeyAgreement
Function
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 ourmnemonic
key into themnemonicToMiniSecret()
function. This function allows us to obtain another mini key from our key. The newly formed key becomes thesecret
andpublic
key pair structure we want with thesr25519PairFromSeed()
function.path
: After creating our key pair, the next operation is to determine where this key will be stored to create thesecretKey
. 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 akey agreement
.secretKey
: This operation takes the two variables we introduced earlier and the wallet type - for us, this value issr25519
for now - to create a secret key. This process is implemented with thekeyFromPath
method.makeEncryptionKeypairFromSeed
: Finally, there's the task of outputting the key pairs. We return this operation with theUtils.Crypto.makeEncryptionKeypairFromSeed()
method in KILT SDK and the function is completed.
- Security: When creating a decentralized identity system, key agreements are necessary for secure communication.
- User Control: By creating their keys, users can control their identities and data.
- Flexibility: This function is designed to support different key types, making it suitable for various applications.
- 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.
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.
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.
Now, we can produce our four key pairs.
Authentication Key Pair
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.
We've prepared our Authentication key pair. Now, with this key, we can prepare claims
and present verified credentials
.
Assertion Key Pair
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
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
const keyAgreement = generateKeyAgreement(mnemonic);
Finally, the keyAgreement
key pair is produced. This key is used for encrypting and decrypting messages.
Returning the Results
return {
authentication: authentication,
keyAgreement: keyAgreement,
assertionMethod: assertionMethod,
capabilityDelegation: capabilityDelegation
};
Finally, we return the key pairs we've created as the function output.
For the DID we'll create for the Attester, we needed four key pairs. We derived these key pairs with the generateKeypairs
function.
If we need to see the code we wrote as a whole:
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
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
andgenerateKeypairs
: They allow us to take the functions exported from the codes we previously created in the attester folder.
createFullDid
Function
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
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
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
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
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
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.
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
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.
if (require.main === module) {
;(async () => {
envConfig()
After defining the function, we retrieve the contents of the .env
file with the envConfig()
function.
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.
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.
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.
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.
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.
We wrote our code, but if we need to take a general look at what the generateDid.ts
file's entire code does:
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 thegenerateKeypairs()
function from thegenerateKeypairs.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
anduri
value. - We run the code file, call the
createFullDid()
function for theattester 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
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.
After running our code, we need to save the resulting Mnemonic and URI values to the .env
file!