β Anchored`: This shows the delegator's prefix and confirms that the delegation has been successfully anchored in the delegator's KEL.
- In the JSON event data:
- `"t": "dip"`: This signifies a Delegated Inception Event. This is the establishment event for the delegated AID.
- `"di"`: This field contains the prefix of the Delegator AID. It cryptographically links this delegated AID back to its delegator.
The prefix of a delegated AID is a SAID of its own `dip` event, which includes the delegator's AID. This creates the strong cryptographic binding characteristic of KERI delegation.
## Rotating Delegated Identifiers
Rotating the keys of a delegated AID also follows a cooperative, two-step process, similar to its inception. The delegate initiates the rotation, and the delegator must confirm it.
#### Delegated Rotation Steps:
1. Delegate performs a rotation with `kli rotate`.
2. Delegator approves the rotation with `kli delegate confirm`.
The delegate uses `kli rotate` with the`--proxy` parameter. This command is run in the background as it waits for the delegator's confirmation. The delegator confirms the delegated rotation with `kli delegate confirm`. This action creates a new anchoring event in the delegator's KEL for the delegate's rotation. The use of the `--interact` flag to `kli delegate confirm` instructs the delegator to anchor the approving seal in an interaction event.
```python
pr_title(f"Rotating delegated AID")
command = f"""
kli rotate --name {delegate_keystore} \
--alias {delegate_alias} \
--proxy {delegate_proxy_alias}
"""
exec_bg(command)
command = f"""
kli delegate confirm --name {delegator_keystore} \
--alias {delegator_alias} \
--interact \
--auto
"""
output = exec(command, True)
# Show the output of the background processes
pr_message(f"Rotation")
pr_message(output)
pr_continue()
```
Rotating delegated AID
Command
kli rotate --name delegate_keystore --alias delegate_alias --proxy delegate_proxy_alias
started with PID: 946
Rotation
['Delegagtor Prefix EHDW4TgdyYTkUwxtZlIt03poPBA4Ouk5w4LJ6MTJJRLB', 'Delegate EESIOsSAKBrCvozIIAKcj87hQvntj_wcWiTHuu7AZPI7 rotation Anchored at Seq. No. 2', 'Delegate EESIOsSAKBrCvozIIAKcj87hQvntj_wcWiTHuu7AZPI7 rotation event committed.']
You can continue β
Now, let's examine the status of the delegate AID after the rotation.
```python
pr_title(f"Delegated AID status")
!kli status --name delegate_keystore --alias delegate_alias --verbose
```
Delegated AID status
Alias: delegate_alias
Identifier: EESIOsSAKBrCvozIIAKcj87hQvntj_wcWiTHuu7AZPI7
Seq No: 1
Delegated Identifier
Delegator: EHDW4TgdyYTkUwxtZlIt03poPBA4Ouk5w4LJ6MTJJRLB β Anchored
Witnesses:
Count: 1
Receipts: 1
Threshold: 1
Public Keys:
1. DN4WorNlMd_93dpHTFMLZoKT2LUH2na3UyMy55JuXZvu
Witnesses:
1. BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha
{
"v": "KERI10JSON00018d_",
"t": "dip",
"d": "EESIOsSAKBrCvozIIAKcj87hQvntj_wcWiTHuu7AZPI7",
"i": "EESIOsSAKBrCvozIIAKcj87hQvntj_wcWiTHuu7AZPI7",
"s": "0",
"kt": "1",
"k": [
"DB5PKs2yTLWkgoaboz2rR0g_im9FBkQF2g8VWcjt6oUP"
],
"nt": "1",
"n": [
"EPnrEmgwqaIp50GHyR9jplHipD5mOwn7sG5QkD7p2adM"
],
"bt": "1",
"b": [
"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha"
],
"c": [],
"a": [],
"di": "EHDW4TgdyYTkUwxtZlIt03poPBA4Ouk5w4LJ6MTJJRLB"
}
{
"v": "KERI10JSON000160_",
"t": "drt",
"d": "EPMRGelfgPh4Nzt3EnvE00iIfqLz8Gvc2e8XV1Xq_8Sx",
"i": "EESIOsSAKBrCvozIIAKcj87hQvntj_wcWiTHuu7AZPI7",
"s": "1",
"p": "EESIOsSAKBrCvozIIAKcj87hQvntj_wcWiTHuu7AZPI7",
"kt": "1",
"k": [
"DN4WorNlMd_93dpHTFMLZoKT2LUH2na3UyMy55JuXZvu"
],
"nt": "1",
"n": [
"EGAdXMUkWeRBtcdMbQIJ--eNpbdktxX3UgC9D7-EaQqt"
],
"bt": "1",
"br": [],
"ba": [],
"a": []
}
Observations from the delegate's KEL after rotation:
- `Seq No: 1`: The sequence number has incremented.
- A new event has been added to the KEL with `"t": "drt"`. This signifies a Delegated Rotation Event. It's also an establishment event.
- The public keys `k` and next key digest `n` have changed, reflecting the rotation.
- The delegate's AID prefix `i` remains the same.
## Understanding the Delegator's KEL
Let's now examine the delegator's KEL to see how these delegation operations are recorded and anchored.
```python
pr_title(f"Delegator AID status")
!kli status --name delegator_keystore --alias delegator_alias --verbose
```
Delegator AID status
Alias: delegator_alias
Identifier: EHDW4TgdyYTkUwxtZlIt03poPBA4Ouk5w4LJ6MTJJRLB
Seq No: 2
Witnesses:
Count: 3
Receipts: 3
Threshold: 2
Public Keys:
1. DG7EhH42hjxj77O-InfYucbj7AacdEbZKnMw2qhKrarD
Witnesses:
1. BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha
2. BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM
3. BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX
{
"v": "KERI10JSON0001b7_",
"t": "icp",
"d": "EHDW4TgdyYTkUwxtZlIt03poPBA4Ouk5w4LJ6MTJJRLB",
"i": "EHDW4TgdyYTkUwxtZlIt03poPBA4Ouk5w4LJ6MTJJRLB",
"s": "0",
"kt": "1",
"k": [
"DG7EhH42hjxj77O-InfYucbj7AacdEbZKnMw2qhKrarD"
],
"nt": "1",
"n": [
"ECxpSF1SUwO0frr7yy_AiTwXgbHfMg16yy6c9_Uf7o0Q"
],
"bt": "2",
"b": [
"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha",
"BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM",
"BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX"
],
"c": [],
"a": []
}
{
"v": "KERI10JSON00013a_",
"t": "ixn",
"d": "EFkNaQOyxLhMcXSdK4Vb_d5_ze_xua9hM9YQ02LO_wZY",
"i": "EHDW4TgdyYTkUwxtZlIt03poPBA4Ouk5w4LJ6MTJJRLB",
"s": "1",
"p": "EHDW4TgdyYTkUwxtZlIt03poPBA4Ouk5w4LJ6MTJJRLB",
"a": [
{
"i": "EESIOsSAKBrCvozIIAKcj87hQvntj_wcWiTHuu7AZPI7",
"s": "0",
"d": "EESIOsSAKBrCvozIIAKcj87hQvntj_wcWiTHuu7AZPI7"
}
]
}
{
"v": "KERI10JSON00013a_",
"t": "ixn",
"d": "EGKBpLEeTiIelhhR79KwFUSUD5bwKpytkebovqwLCxXL",
"i": "EHDW4TgdyYTkUwxtZlIt03poPBA4Ouk5w4LJ6MTJJRLB",
"s": "2",
"p": "EFkNaQOyxLhMcXSdK4Vb_d5_ze_xua9hM9YQ02LO_wZY",
"a": [
{
"i": "EESIOsSAKBrCvozIIAKcj87hQvntj_wcWiTHuu7AZPI7",
"s": "1",
"d": "EPMRGelfgPh4Nzt3EnvE00iIfqLz8Gvc2e8XV1Xq_8Sx"
}
]
}
Key observations from the delegator's KEL:
- Sequence Number `s: "1"` (Interaction Event):
- This event was created when the delegator confirmed the delegated inception.
- The `a` (anchors/payload) array contains a delegated event seal:
- `"i"`: The prefix of the delegate AID.
- `"s": "0"`: The sequence number of the delegate's event being anchored (the `dip` event at sequence 0).
- `"d"`: The SAID (digest) of the delegate's `dip` event.
- Sequence Number `s: "2"` (Interaction Event):
- This event was created when the delegator confirmed the delegated rotation.
- The `a` array contains another delegated event seal:
- `"i"`: The prefix of the delegate AID.
- `"s": "1"`: The sequence number of the delegate's event being anchored (the drt event at sequence 1).
- `"d"`: The SAID (digest) of the delegate's drt event.
These seals embedded within interaction events, specifically the "a" attributes section of the interaction events, in the delegator's KEL are the cryptographic proof that the delegator authorized the delegate's inception and rotation events. Conversely, the delegated AID's `dip` event also contains a di field pointing to the delegator, and its establishment events (like `dip` and `drt`) implicitly include a delegating event location seal that refers back to the specific event in the delegator's KEL that authorized them (though not explicitly shown in the simplified `kli status` output for the delegate, this is part of the full event structure). This creates the verifiable, cooperative link between the two AIDs.
βΉοΈ NOTE
The security of KERI's cooperative delegation model is robust. To illicitly create or rotate a delegated AID, an attacker would generally need to compromise keys from both the delegator and the delegate (specifically, the delegate's pre-rotated keys and the delegator's current signing keys for the anchoring event). Furthermore, the delegator has mechanisms to recover a compromised delegation using something called "superseding delegated recovery rotation," covered in depth in a separate training.
π SUMMARY
KERI delegation allows an AID (delegator) to authorize another AID (delegate) for specific purposes. This is a cooperative process requiring actions from both parties.
- Delegated Inception (
dip): The delegate initiates a request (e.g., via a proxy AID). The delegator confirms this by creating an anchoring event in its KEL, which contains a seal pointing to the delegate's dip event. The delegate's dip event includes the delegator's AID in its di field. The delegate's AID prefix is a SAID of its dip event.
- Delegated Rotation (
drt): Similar to inception, the delegate initiates the rotation, and the delegator confirms with another anchoring event in its KEL. The delegate's KEL will show a drt event.
- Anchoring: The delegator's KEL contains seals (AID, sequence number, and digest of the delegate's event) that provide verifiable proof of the authorized delegation. This creates a strong, bi-directional cryptographic link.
- Security: The cooperative nature enhances security, as unauthorized delegation typically requires compromising keys from both entities.
[<- Prev (Connecting Controllers)](101_45_Connecting_controllers.ipynb) | [Next (Multisignature Identifiers) ->](101_48_Multisignature_Identifiers.ipynb)
# KERI Core: Multi-Signature Group Identifiers
π― OBJECTIVE
This notebook introduces the concept of multi-signature (multisig) group Autonomic Identifiers (AIDs) in KERI. It will demonstrate how to:
- Set up individual AIDs that will participate in a multisig group.
- Configure and incept a multisig group AID where actions require signatures from multiple participants.
- Perform interaction events with the multisig group AID.
- Rotate the keys for the multisig group AID.
- Understand the structure of inception (
icp), interaction (ixn), and rotation (rot) events for multisig group AIDs.
## Introduction to Multi-Signature Identifiers
Multi-signature (multisig) identifier schemes in KERI enhance security and enable collective control over an identifier by requiring signatures from multiple authorized keys to validate an event. This is particularly useful for organizations or groups where shared authority is necessary. A multi-signature identifier is also known as a group identifier, or multi-signature group identifier.
A multi-signature group AID is essentially an identifier whose controlling authority is distributed among a set of participating AIDs. Each key event for this group AID, such as inception, interaction, or rotation, must be authorized by a sufficient number of these participating AIDs according to the defined signing threshold. The signing threshold may be changed with a rotation event and group members may be added or removed as needed.
#### Illustration of end product
The result of this training will be the creation of a multisig group identifier illustrated in the middle of the diagram below.

## Initial Setup of Participant AIDs
The setup involves:
- Individual Participant AIDs: Each entity that will be part of the multisig group first has its own individual AID.
- Group Configuration: A configuration is defined specifying which AIDs are members of the group and the signing threshold (e.g., 2 out of 3 participants must sign).
- Group Inception: The multisig group AID is incepted. This is a cooperative process where participating AIDs signal their agreement to form the group. The resulting group AID prefix is a self-addressing identifier derived from its inception event data, which includes a list of member AID prefixes and the threshold policies for signing and rotation.
- Group Operations: Subsequent operations like interaction events or key rotations for the group AID also require the specified threshold of signatures from the participating AIDs.
For this notebook, we will create two AIDs, `party1_alias` and `party2_alias`, each in its own keystore. These will act as the initial members of our multisig group.
```python
# Imports and Utility functions
from scripts.utils import exec, exec_bg, clear_keri
from scripts.utils import pr_continue, pr_title, pr_message
import json
import time
clear_keri()
# Party 1 Keystore
party1_keystore = "party1_keystore"
party1_salt= "0AAW49QDCAuz0I-R1yCY8wa6" # Use hardcoded salt so that the AID stays the same. Swap with `exec("kli salt")` for a dynamic salt.
party1_alias = "party1_alias"
pr_title("Initializing keystores")
# The `keystore_init_config.json` file is used here
# to pre-configure the keystores with default witness information.
!kli init --name {party1_keystore} \
--nopasscode \
--salt {party1_salt} \
--config-dir ./config \
--config-file keystore_init_config.json
# multisig2 Keystore
party2_keystore = "party2_keystore"
party2_salt = "0AC_OfvFyZcKRspiawo-Pgwz" # Use hardcoded salt so that the AID stays the same. Swap with `exec("kli salt")` for a dynamic salt.
party2_alias = "party2_alias"
!kli init --name {party2_keystore} \
--nopasscode \
--salt {party2_salt} \
--config-dir ./config \
--config-file keystore_init_config.json
pr_title("Incepting multisig parts")
# multisig1 AID inception
!kli incept --name {party1_keystore} \
--alias {party1_alias} \
--icount 1 \
--isith 1 \
--ncount 1 \
--nsith 1 \
--wits BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha \
--wits BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM \
--wits BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX \
--toad 2 \
--transferable
# multisig2 AID Inception
!kli incept --name {party2_keystore} \
--alias {party2_alias} \
--icount 1 \
--isith 1 \
--ncount 1 \
--nsith 1 \
--wits BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha \
--wits BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM \
--wits BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX \
--toad 2 \
--transferable
pr_continue()
```
Proceeding with deletion of '/usr/local/var/keri/' without confirmation.
β
Successfully removed: /usr/local/var/keri/
Initializing keystores
KERI Keystore created at: /usr/local/var/keri/ks/party1_keystore
KERI Database created at: /usr/local/var/keri/db/party1_keystore
KERI Credential Store created at: /usr/local/var/keri/reg/party1_keystore
Loading 3 OOBIs...
http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness succeeded
http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wes&tag=witness succeeded
http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wil&tag=witness succeeded
KERI Keystore created at: /usr/local/var/keri/ks/party2_keystore
KERI Database created at: /usr/local/var/keri/db/party2_keystore
KERI Credential Store created at: /usr/local/var/keri/reg/party2_keystore
Loading 3 OOBIs...
http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness succeeded
http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wes&tag=witness succeeded
http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wil&tag=witness succeeded
Incepting multisig parts
Waiting for witness receipts...
Prefix ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY
Public key 1: DOKTEjSCgBNZLr62VaTUbQh1dpP7n6KUaIfZhak-aaXU
Waiting for witness receipts...
Prefix EOZIXcOdHPFkSEN_85QbeFC4GKo4nVqVx70RuDug-jBB
Public key 1: DDyva00Vg_tIbK9V15XiIodr0tS3JDLqQvQC23DTp-h6
You can continue β
## Creating the Multi-Signature Group AID
With the individual participant AIDs in place, now proceed to create the multisig group AID. This involves several steps:
- authorizing mailbox roles for discovery
- exchanging OOBIs
- configuring the multisig parameters
- cooperatively incepting the group AID.
### Adding Mailbox Role
To allow the cooperative inception process, each participant AID needs to authorize one of its witnesses to act as a `mailbox`. This allows other participants to send messages (like the group inception proposal) to them indirectly via this witness.

The `kli ends add` command is used to authorize an end role.
`--eid`: Specifies the prefix of the witness AID being authorized for the new role.
`--role`: Defines the role being assigned.
```python
# Add new endpoint role authorization.
pr_title("Adding mailbox role")
role = "mailbox"
!kli ends add --name {party1_keystore} \
--alias {party1_alias} \
--eid BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM \
--role {role}
# Add new endpoint role authorization.
!kli ends add --name {party2_keystore} \
--alias {party2_alias} \
--eid BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX \
--role {role}
pr_continue()
```
Adding mailbox role
End role authorization added for role mailbox
End role authorization added for role mailbox
You can continue β
### Resolving OOBIs
Next, the participants need to discover each other. This is done by generating and resolving Out-of-Band Introductions (OOBIs) that point to their newly authorized mailbox endpoints.
```python
# OOBI Generation
pr_title("Generating OOBIs")
# party1 generates mailbox OOBI for its AID
party1_oobi = exec(f"kli oobi generate --name {party1_keystore} --alias {party1_alias} --role {role}")
# party2 generates mailbox OOBI for its AID
party2_oobi = exec(f"kli oobi generate --name {party2_keystore} --alias {party2_alias} --role {role}")
pr_message("Party 1 OOBI: " + party1_oobi)
pr_message("Party 2 OOBI: " + party2_oobi)
# OOBI Exchange
pr_title("Resolving OOBIs")
!kli oobi resolve --name {party1_keystore} \
--oobi-alias {party2_alias} \
--oobi {party2_oobi}
!kli oobi resolve --name {party2_keystore} \
--oobi-alias {party1_alias} \
--oobi {party1_oobi}
pr_continue()
```
Generating OOBIs
Party 1 OOBI: http://witness-demo:5643/oobi/ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY/mailbox/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM
Party 2 OOBI: http://witness-demo:5644/oobi/EOZIXcOdHPFkSEN_85QbeFC4GKo4nVqVx70RuDug-jBB/mailbox/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX
Resolving OOBIs
http://witness-demo:5644/oobi/EOZIXcOdHPFkSEN_85QbeFC4GKo4nVqVx70RuDug-jBB/mailbox/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX resolved
http://witness-demo:5643/oobi/ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY/mailbox/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM resolved
You can continue β
### Configuring the Multi-Signature Group
Setting up the configuration for a multi-signature group identifier is similar to a single-signature identifier with the exception of the "aids" field where the participants are defined as a list of member AID prefixes. All of the other typical configuration including transferability, witnesses, TOAD, and signing and rotation thresholds still apply.
The parameters for the multisig group AID are defined in a JSON configuration file. This file specifies:
- `aids`: A list of the prefixes of the participating AIDs.
- `transferable`: Whether the group AID itself will be transferable (i.e., its keys can be rotated).
- `wits`: A list of witness AIDs for the group AID.
- `toad`: The Threshold of Accountable Duplicity for the group AID's events. This defines how many witness receipts are needed for an event to be considered accountable by the controller group.
- `isith`: The initial signing threshold for the group AID's inception event. This can be an integer (e.g., "2" for 2-of-N) or a list of weights for a weighted threshold scheme. For this example, "2" means both participants must sign.
- `nsith`: The signing threshold for the next key set (for future rotations). Similar to isith.
```python
# Multisig participants Configuration
pr_title(f"Building multisig config file")
# Multisig participants prefixes
party1_pre = exec(f"kli aid --name {party1_keystore} --alias {party1_alias}")
party2_pre = exec(f"kli aid --name {party2_keystore} --alias {party2_alias}")
pr_message("Party 1 prefix: " + party1_pre)
pr_message("Party 2 prefix: " + party1_pre)
# multisig configuration
multisig_inception_config = {
"aids": [
party1_pre,
party2_pre
],
"transferable": True,
"wits": [
"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha",
"BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM",
"BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX"
],
"toad": 2,
"isith": "2",
"nsith": "2"
}
# Specify the filename
file_path = './config/multisig/multisig_inception_config.json'
# Write the configuration data to the JSON file
with open(file_path, 'w') as f:
json.dump(multisig_inception_config, f, indent=2)
pr_message(f"Multisig config: {file_path}")
!cat {file_path}
pr_continue()
```
Building multisig config file
Party 1 prefix: ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY
Party 2 prefix: ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY
Multisig config: ./config/multisig/multisig_inception_config.json
{
"aids": [
"ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY",
"EOZIXcOdHPFkSEN_85QbeFC4GKo4nVqVx70RuDug-jBB"
],
"transferable": true,
"wits": [
"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha",
"BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM",
"BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX"
],
"toad": 2,
"isith": "2",
"nsith": "2"
}
You can continue β
### Incepting the Multi-Signature Group AID
The inception of a multisig group AID is a cooperative process. One participant (here, `party1_alias`) initiates the group inception using `kli multisig incept`.
- `--group`: Assigns a human-readable alias to the multisig group AID being created.
- `--file`: Points to the JSON configuration file created in the previous step.
This command will propose the inception event and wait for other members to join and sign. The other participant(s) (here, `party2_alias`) join the proposed inception using `kli multisig join`.
- `--group`: Specifies the alias of the group they are joining.
- `--auto`: Automatically approves the join request **(in a real scenario, this would be an interactive confirmation)**. This command will fetch the proposed event, sign it, and send its signature back.
Once all required signatures are gathered, the inception event is finalized and published to the witnesses.
These commands are run in the background here, as they would normally be interactive, waiting for each other.
```python
pr_title(f"Incepting multisig AID")
multisig_group = "multisig_group"
command = f"""
kli multisig incept --name {party1_keystore} \
--alias {party1_alias} \
--group {multisig_group} \
--file {file_path} > ./logs/multisig_event.log
"""
incept1_process = exec_bg(command)
command = f"""
kli multisig join --name {party2_keystore} \
--group {multisig_group} \
--auto > ./logs/multisig_join.log
"""
join_process = exec_bg(command)
while(incept1_process.poll() is None or join_process.poll() is None):
print("Waiting for multisig inception to complete...\n")
time.sleep(2)
# Show the output of the background processes
pr_message(f"Multisig Event")
!cat ./logs/multisig_event.log
pr_message(f"Multisig Join")
!cat ./logs/multisig_join.log
pr_continue()
```
Incepting multisig AID
Command
kli multisig incept --name party1_keystore --alias party1_alias --group multisig_group --file ./config/multisig/multisig_inception_config.json > ./logs/multisig_event.log
started with PID: 2386
Command
kli multisig join --name party2_keystore --group multisig_group --auto > ./logs/multisig_join.log
started with PID: 2387
Waiting for multisig inception to complete...
Waiting for multisig inception to complete...
Multisig Event
Group identifier inception initialized for EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-
Alias: multisig_group
Identifier: EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-
Seq No: 0
Group Identifier
Local Indentifier: ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY β Fully Signed
Witnesses:
Count: 3
Receipts: 3
Threshold: 2
Public Keys:
1. DOKTEjSCgBNZLr62VaTUbQh1dpP7n6KUaIfZhak-aaXU
2. DDyva00Vg_tIbK9V15XiIodr0tS3JDLqQvQC23DTp-h6
Multisig Join
Waiting for group multisig events...
Group Multisig Inception proposed:
Participants:
+-------+--------------+----------------------------------------------+
| Local | Name | AID |
+-------+--------------+----------------------------------------------+
| | party1_alias | ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY |
| * | party2_alias | EOZIXcOdHPFkSEN_85QbeFC4GKo4nVqVx70RuDug-jBB |
+-------+--------------+----------------------------------------------+
Configuration:
+---------------------+----------------------------------------------+
| Name | Value |
+---------------------+----------------------------------------------+
| Signature Threshold | 2 |
| Establishment Only | False |
| Do Not Delegate | False |
| Witness Threshold | 2 |
| Witnesses | BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha |
| | BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM |
| | BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX |
+---------------------+----------------------------------------------+
Alias: multisig_group
Identifier: EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-
Seq No: 0
Group Identifier
Local Indentifier: EOZIXcOdHPFkSEN_85QbeFC4GKo4nVqVx70RuDug-jBB β Fully Signed
Witnesses:
Count: 3
Receipts: 3
Threshold: 2
Public Keys:
1. DOKTEjSCgBNZLr62VaTUbQh1dpP7n6KUaIfZhak-aaXU
2. DDyva00Vg_tIbK9V15XiIodr0tS3JDLqQvQC23DTp-h6
You can continue β
### Multi-signature inception result
The diagram below represents the multi-signature identifier created. Each single signature identifier contributed both a signing key and a rotation key, shown with the arrows from the single sig keystores (party1_keystore, party2_keystore) to the virtual multisig group identifier in the middle of the diagram.

### Verifying Multi-Signature AID Status
After successful inception, you can check the status of the `multisig_group` AID using `kli status --verbose`.
The output shows the inception event (`icp`). Key fields for a multisig AID include:
- `i`: The prefix of the multisig group AID.
- `k`: A list of the public keys of the participating AIDs that form the current signing key set for the group.
- `kt`: The current signing threshold (e.g., "2", meaning 2 signatures are required).
- `n`: A list of digests of the public keys for the next rotation (pre-rotation).
- `nt`: The signing threshold for the next key set.
- `b`: The list of witness AIDs for this group AID.
- `bt`: The Threshold of Accountable Duplicity (TOAD) for this group AID.
This inception event (`icp`) is an establishment event that cryptographically binds the group AID to its initial set of controlling keys (the participants' keys) and the defined signing policies.
```python
pr_title(f"Multisig AID status")
!kli status --name party1_keystore --alias multisig_group --verbose
```
Multisig AID status
Alias: multisig_group
Identifier: EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-
Seq No: 0
Group Identifier
Local Indentifier: ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY β Fully Signed
Witnesses:
Count: 3
Receipts: 3
Threshold: 2
Public Keys:
1. DOKTEjSCgBNZLr62VaTUbQh1dpP7n6KUaIfZhak-aaXU
2. DDyva00Vg_tIbK9V15XiIodr0tS3JDLqQvQC23DTp-h6
Witnesses:
1. BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha
2. BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM
3. BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX
{
"v": "KERI10JSON000215_",
"t": "icp",
"d": "EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-",
"i": "EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-",
"s": "0",
"kt": "2",
"k": [
"DOKTEjSCgBNZLr62VaTUbQh1dpP7n6KUaIfZhak-aaXU",
"DDyva00Vg_tIbK9V15XiIodr0tS3JDLqQvQC23DTp-h6"
],
"nt": "2",
"n": [
"EDsRlPLnlcvZZVv-T1GoEZE5x0_QtRpSoSiAOurMmLz9",
"EG-jLWaS8gE9wuPCArQk_81XZkr-x3WWHuFNYt_LpZXE"
],
"bt": "2",
"b": [
"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha",
"BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM",
"BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX"
],
"c": [],
"a": []
}
## Signing and Anchoring Arbitrary data in an Interaction Event
When you want to sign and anchor arbitrary data to a key event log for a multisig identifier you use an interaction event. An interaction event (`ixn`) is a non-establishment event used to anchor arbitrary data to the Key Event Log (KEL) of an AID. For a multisig group AID, an interaction event must also be signed by the required threshold of participating AIDs.
The `kli multisig interact` command initiates an interaction event for the group, and `kli multisig join` is used by other participants to add their signatures.
```python
pr_title(f"Performing interaction event")
# Anchor data for the interaction event (ixn)
data = """'{"d": "arbitrary data"}'""" # Keep string format as is!
command = f"""
kli multisig interact --name {party1_keystore} \
--alias {multisig_group} \
--data {data} > ./logs/multisig_event.log
"""
interact1_process = exec_bg(command)
command = f"""
kli multisig join --name {party2_keystore} \
--group {multisig_group} \
--auto > ./logs/multisig_join.log
"""
join_process = exec_bg(command)
while(interact1_process.poll() is None or join_process.poll() is None):
print("Waiting for multisig interaction to complete...\n")
time.sleep(2)
# Show the output of the background processes
pr_message(f"Multisig Event")
!cat ./logs/multisig_event.log
pr_message(f"Multisig Join")
!cat ./logs/multisig_join.log
pr_continue()
```
Performing interaction event
Command
kli multisig interact --name party1_keystore --alias multisig_group --data '{"d": "arbitrary data"}' > ./logs/multisig_event.log
started with PID: 2399
Command
kli multisig join --name party2_keystore --group multisig_group --auto > ./logs/multisig_join.log
started with PID: 2401
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Waiting for multisig interaction to complete...
Multisig Event
Alias: multisig_group
Identifier: EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-
Seq No: 1
Group Identifier
Local Indentifier: ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY β Fully Signed
Witnesses:
Count: 3
Receipts: 3
Threshold: 2
Public Keys:
1. DOKTEjSCgBNZLr62VaTUbQh1dpP7n6KUaIfZhak-aaXU
2. DDyva00Vg_tIbK9V15XiIodr0tS3JDLqQvQC23DTp-h6
Multisig Join
Waiting for group multisig events...
Group Multisig Interaction for multisig_group (EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-) proposed:
Data:
[
{
"d": "arbitrary data"
}
]
Alias: multisig_group
Identifier: EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-
Seq No: 1
Group Identifier
Local Indentifier: EOZIXcOdHPFkSEN_85QbeFC4GKo4nVqVx70RuDug-jBB β Fully Signed
Witnesses:
Count: 3
Receipts: 3
Threshold: 2
Public Keys:
1. DOKTEjSCgBNZLr62VaTUbQh1dpP7n6KUaIfZhak-aaXU
2. DDyva00Vg_tIbK9V15XiIodr0tS3JDLqQvQC23DTp-h6
You can continue β
### Verifying Interaction Event
Let's examine the KEL for `multisig_group` again to see the `ixn` event.
The interaction event (`ixn`) does not change the establishment keys but anchors data (`a` field) to the KEL. It is signed by the current authoritative keys established by the preceding `icp` event. The `p` field contains the digest of the previous event (`icp` in this case), ensuring the chain's integrity.
```python
pr_title(f"Multisig AID status")
!kli status --name party1_keystore --alias multisig_group --verbose
```
Multisig AID status
Alias: multisig_group
Identifier: EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-
Seq No: 1
Group Identifier
Local Indentifier: ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY β Fully Signed
Witnesses:
Count: 3
Receipts: 3
Threshold: 2
Public Keys:
1. DOKTEjSCgBNZLr62VaTUbQh1dpP7n6KUaIfZhak-aaXU
2. DDyva00Vg_tIbK9V15XiIodr0tS3JDLqQvQC23DTp-h6
Witnesses:
1. BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha
2. BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM
3. BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX
{
"v": "KERI10JSON000215_",
"t": "icp",
"d": "EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-",
"i": "EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-",
"s": "0",
"kt": "2",
"k": [
"DOKTEjSCgBNZLr62VaTUbQh1dpP7n6KUaIfZhak-aaXU",
"DDyva00Vg_tIbK9V15XiIodr0tS3JDLqQvQC23DTp-h6"
],
"nt": "2",
"n": [
"EDsRlPLnlcvZZVv-T1GoEZE5x0_QtRpSoSiAOurMmLz9",
"EG-jLWaS8gE9wuPCArQk_81XZkr-x3WWHuFNYt_LpZXE"
],
"bt": "2",
"b": [
"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha",
"BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM",
"BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX"
],
"c": [],
"a": []
}
{
"v": "KERI10JSON0000e1_",
"t": "ixn",
"d": "ECFIsW1NE0jI3kd576KmSMSDqf4E_Z74Si1G9I6OdMrZ",
"i": "EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-",
"s": "1",
"p": "EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-",
"a": [
{
"d": "arbitrary data"
}
]
}
## Rotation Event for Multi-Signature Group AID
Rotating the keys for a multisig group AID also requires a cooperative process as each participant must first rotate the keys of its single signature identifier participating as a member of the multi-signature group prior to being able to rotate the group. This is required because each rotation of a multi-signature identifier must use new signing and rotation keys in each rotation event.
### Rotating Individual Participant Keys
First, each individual participant AID must rotate its own keys for the participating single signature identifier. This ensures that when they participate in the group rotation, they are using their new, updated keys.
- `kli rotate` is used for individual AID key rotation.
- `kli query` is used to ensure other participants are aware of these individual rotations.
Updating each participant on the latest key state via a key state refresh is required in order to create a multi-signature rotation event. This is because the rotation threshold check will fail until there are enough new keys available for a new rotation event to be created for the multi-signature identifier.
```python
pr_title(f"Rotating multisig participant single signature keys")
!kli rotate --name {party1_keystore} \
--alias {party1_alias}
!kli query --name {party2_keystore} \
--alias {party2_alias} \
--prefix {party1_pre}
!kli rotate --name {party2_keystore} \
--alias {party2_alias}
!kli query --name {party1_keystore} \
--alias {party1_alias} \
--prefix {party2_pre}
pr_continue()
```
Rotating multisig participant single signature keys
Waiting for witness receipts...
Prefix ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY
New Sequence No. 1
Public key 1: DED9rD7Mpbtjtej7Ueyc_R6894f1hQ-YhcZtdAI0WIIn
Checking for updates...
Identifier: ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY
Seq No: 1
Witnesses:
Count: 3
Receipts: 3
Threshold: 3
Public Keys:
1. DED9rD7Mpbtjtej7Ueyc_R6894f1hQ-YhcZtdAI0WIIn
Waiting for witness receipts...
Prefix EOZIXcOdHPFkSEN_85QbeFC4GKo4nVqVx70RuDug-jBB
New Sequence No. 1
Public key 1: DAz9zLvu7sXHI-YJVlNlS67B0A1SJJvG-MwlAsT2OOhF
Checking for updates...
Identifier: EOZIXcOdHPFkSEN_85QbeFC4GKo4nVqVx70RuDug-jBB
Seq No: 1
Witnesses:
Count: 3
Receipts: 3
Threshold: 3
Public Keys:
1. DAz9zLvu7sXHI-YJVlNlS67B0A1SJJvG-MwlAsT2OOhF
You can continue β
You want to make sure that the latest sequence number, which will be sequence number one, shows in the status for each key state query. This is required so that each participant in the multisig has a new key to contribute to the multisig rotation.
### Rotating the Multi-Signature Group Keys
Once the participants have rotated their individual keys, the group rotation can proceed.
The `kli multisig rotate` command initiates the rotation for the group.
- `--smids`: **Signing member identifiers**; specifies the list of participant AIDs that will contribute signing keys for the current rotation event. The contributed keys will be aauthorized to sign events after this rotation completes.
- `--rmids`: **Rotation member identifiers**; specifies the list of participant AIDs that will contribute rotation keys, as digests, to form the set of cryptographic pre-comittments of keys (pre-rotation) that will become the next set of signing keys after the next rotation.
- `--isith`: **Signing (current) threshold**; The signing threshold for the current rotation event. Here, `["1/2", "1/2"]` represents a weighted threshold where each of the two participants has a weight of 1/2, and a sum of 1 (i.e., both signatures) is required.
- `--nsith`: **Next (rotation) threshold**; The signing threshold for the next set of keys (pre-rotation).
Again, `kli multisig join` is used by the other participant to co-sign the group rotation event.
```python
pr_title(f"Rotating multisig group")
command = f"""
kli multisig rotate --name {party1_keystore} \
--alias {multisig_group} \
--smids {party1_pre} \
--smids {party2_pre} \
--rmids {party1_pre} \
--rmids {party2_pre} \
--isith '["1/2", "1/2"]' \
--nsith '["1/2", "1/2"]' > ./logs/multisig_event.log
"""
incept_process = exec_bg(command)
command = f"""
kli multisig join --name {party2_keystore} \
--group {multisig_group} \
--auto > ./logs/multisig_join.log
"""
join_process = exec_bg(command)
while(incept_process.poll() is None or join_process.poll() is None):
print("Waiting for multisig rotation to complete...\n")
time.sleep(2)
# Show the output of the background processes
pr_message(f"Multisig Event")
!cat ./logs/multisig_event.log
pr_message(f"Multisig Join")
!cat ./logs/multisig_join.log
pr_continue()
```
Rotating multisig group
Command
kli multisig rotate --name party1_keystore --alias multisig_group --smids ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY --smids EOZIXcOdHPFkSEN_85QbeFC4GKo4nVqVx70RuDug-jBB --rmids ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY --rmids EOZIXcOdHPFkSEN_85QbeFC4GKo4nVqVx70RuDug-jBB --isith '["1/2", "1/2"]' --nsith '["1/2", "1/2"]' > ./logs/multisig_event.log
started with PID: 2471
Command
kli multisig join --name party2_keystore --group multisig_group --auto > ./logs/multisig_join.log
started with PID: 2472
Waiting for multisig rotation to complete...
Waiting for multisig rotation to complete...
Multisig Event
Alias: multisig_group
Identifier: EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-
Seq No: 2
Group Identifier
Local Indentifier: ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY β Fully Signed
Witnesses:
Count: 3
Receipts: 3
Threshold: 3
Public Keys:
1. DED9rD7Mpbtjtej7Ueyc_R6894f1hQ-YhcZtdAI0WIIn
2. DAz9zLvu7sXHI-YJVlNlS67B0A1SJJvG-MwlAsT2OOhF
Multisig Join
Waiting for group multisig events...
Group Multisig Rotation proposed:
Signing Members
+-------+--------------+----------------------------------------------+-----------+
| Local | Name | AID | Threshold |
+-------+--------------+----------------------------------------------+-----------+
| | party1_alias | ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY | 1/2 |
| * | party2_alias | EOZIXcOdHPFkSEN_85QbeFC4GKo4nVqVx70RuDug-jBB | 1/2 |
+-------+--------------+----------------------------------------------+-----------+
Rotation Members
+-------+--------------+----------------------------------------------+-----------+
| Local | Name | AID | Threshold |
+-------+--------------+----------------------------------------------+-----------+
| | party1_alias | ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY | 1/2 |
| * | party2_alias | EOZIXcOdHPFkSEN_85QbeFC4GKo4nVqVx70RuDug-jBB | 1/2 |
+-------+--------------+----------------------------------------------+-----------+
Configuration:
+-------------------+-------+
| Name | Value |
+-------------------+-------+
| Witness Threshold | 3 |
+-------------------+-------+
Alias: multisig_group
Identifier: EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-
Seq No: 2
Group Identifier
Local Indentifier: EOZIXcOdHPFkSEN_85QbeFC4GKo4nVqVx70RuDug-jBB β Fully Signed
Witnesses:
Count: 3
Receipts: 3
Threshold: 3
Public Keys:
1. DED9rD7Mpbtjtej7Ueyc_R6894f1hQ-YhcZtdAI0WIIn
2. DAz9zLvu7sXHI-YJVlNlS67B0A1SJJvG-MwlAsT2OOhF
You can continue β
### Verifiying rotation event
Let's inspect the KEL of `multisig_group` one last time.
The `rot` event (type `rot`) is an establishment event that signifies a change in the controlling keys.
- `s`: The sequence number is incremented.
- `p`: Contains the digest of the previous event (the ixn event in this case).
- `kt`: The signing threshold for this rotation. It's now a list `["1/2", "1/2"]`, reflecting the weighted threshold specified.
- `k`: The list of public keys of the participants that are now the current authoritative signers for the group. These are the new keys from the individual participant rotations.
- `nt`: The signing threshold for the next rotation (pre-rotation).
- `n`: A list of digests of the public keys for the next rotation.
- `br`: List of witnesses to remove (empty in this case).
- `ba`: List of witnesses to add (empty in this case).
This `rot` event demonstrates how the control of the multisig group AID has been transferred to a new set of keys (derived from the participants' new keys) and how a new pre-rotation commitment has been made for the next cycle, all while maintaining the integrity of the KEL through cryptographic chaining and multi-signature authorization.
```python
pr_title(f"Multisig AID status")
!kli status --name party1_keystore --alias multisig_group --verbose
```
Multisig AID status
Alias: multisig_group
Identifier: EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-
Seq No: 2
Group Identifier
Local Indentifier: ELPvwxEI6nEv_Imtv89jgACiDvCFlgl50Ls6UatFtLyY β Fully Signed
Witnesses:
Count: 3
Receipts: 3
Threshold: 3
Public Keys:
1. DED9rD7Mpbtjtej7Ueyc_R6894f1hQ-YhcZtdAI0WIIn
2. DAz9zLvu7sXHI-YJVlNlS67B0A1SJJvG-MwlAsT2OOhF
Witnesses:
1. BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha
2. BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM
3. BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX
{
"v": "KERI10JSON000215_",
"t": "icp",
"d": "EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-",
"i": "EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-",
"s": "0",
"kt": "2",
"k": [
"DOKTEjSCgBNZLr62VaTUbQh1dpP7n6KUaIfZhak-aaXU",
"DDyva00Vg_tIbK9V15XiIodr0tS3JDLqQvQC23DTp-h6"
],
"nt": "2",
"n": [
"EDsRlPLnlcvZZVv-T1GoEZE5x0_QtRpSoSiAOurMmLz9",
"EG-jLWaS8gE9wuPCArQk_81XZkr-x3WWHuFNYt_LpZXE"
],
"bt": "2",
"b": [
"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha",
"BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM",
"BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX"
],
"c": [],
"a": []
}
{
"v": "KERI10JSON0000e1_",
"t": "ixn",
"d": "ECFIsW1NE0jI3kd576KmSMSDqf4E_Z74Si1G9I6OdMrZ",
"i": "EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-",
"s": "1",
"p": "EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-",
"a": [
{
"d": "arbitrary data"
}
]
}
{
"v": "KERI10JSON0001d2_",
"t": "rot",
"d": "EO2DzHkdFWFIkkwlmowdxKBDteybwf020WVgnofejPn6",
"i": "EEyK9sCty5qBjnAmc_-2fcsW-68sHfMgpH6bzc2vO0h-",
"s": "2",
"p": "ECFIsW1NE0jI3kd576KmSMSDqf4E_Z74Si1G9I6OdMrZ",
"kt": [
"1/2",
"1/2"
],
"k": [
"DED9rD7Mpbtjtej7Ueyc_R6894f1hQ-YhcZtdAI0WIIn",
"DAz9zLvu7sXHI-YJVlNlS67B0A1SJJvG-MwlAsT2OOhF"
],
"nt": [
"1/2",
"1/2"
],
"n": [
"EL3IL5wyj5koej_0Fcq1ZtVEDJR7ju5RDAUcIE2SZmrn",
"EBzqFJ1sxQx9f-H9XgnhXhhN-9dN2SFaiY2NINiknoVl"
],
"bt": "3",
"br": [],
"ba": [],
"a": []
}
π SUMMARY
This notebook demonstrated the creation and management of a multi-signature (multisig) group AID in KERI.
- Participant Setup: Individual AIDs for each participant were created and their mailboxes configured for discovery via OOBIs.
- Group Configuration: A JSON file defined the participating AIDs, witness set, TOAD, and signing thresholds (
isith, nsith) for the group.
- Group Inception (
icp): The multisig group AID was incepted cooperatively using kli multisig incept by one participant and kli multisig join by the other(s). The resulting icp event in the group's KEL lists the participants' public keys (k) and the signing threshold (kt).
- Group Interaction (
ixn): An interaction event was performed by the group, requiring signatures from the participants according to the current threshold. The ixn event anchored data to the group's KEL.
- Group Rotation (
rot):
- Individual participant AIDs first rotated their own keys.
- The multisig group AID then performed a rotation using
kli multisig rotate and kli multisig join. This involved specifying the new set of signing members (--smids), the pre-rotated members for the next rotation (--rmids), and potentially new signing thresholds (--isith, --nsith), which can include weighted schemes.
- The
rot event in the group's KEL updated the list of authoritative keys (k), the signing threshold (kt), and the pre-rotation commitment (n, nt).
Throughout this process, KERI's cryptographic chaining and signature verification ensure the integrity and authenticity of the multisig group's key events.
[<- Prev (Delegated AIDs)](101_47_Delegated_AIDs.ipynb) | [Next (ACDC) ->](101_50_ACDC.ipynb)
# Understanding ACDCs: Authentic Chained Data Containers
π― OBJECTIVE
Introduces the concept of Authentic Chained Data Containers (ACDCs). Explore what they are, their basic structure, how they are connected to key event logs (KELs), and why they are a secure way to share information.
## Defining ACDCs
In KERI, verifiable claims are exchanged using **Authentic Chained Data Containers**, **ACDCs** for short. An ACDC is like a digital envelope that's been cryptographically sealed and tied to the key event log (KEL) of an identity. You can put data inside (like a name, an authorization, or a membership status), and the seal guarantees who created it and that the contents haven't been tampered with since it was sealed. The tie to the KEL of an AID, also known as an anchor or anchoring, provides the cryptographic verifiability and authenticity (who) for a credential.
A feature of ACDCs is that they allow different parties (represented by their AIDs) to securely share and verify pieces of information or claims about themselves or others. They are KERI's secure implementation of the broader concept of Verifiable Credentials (VCs).
βΉοΈ NOTE: We don't sign credentials, and you shouldn't either!
The security model of an ACDC is provided by a tie to a key event in a key event log, also known as a seal. This seal is then placed in a key event, whether an interaction or rotation event, in a process called "anchoring."
What this means is that ACDCs are not signed directly, rather the seal is signed, and the seal that anchors the ACDC to a particular key event provides the cryptographic verifiability for that ACDC. Critically, this seal allows verification of the ACDC even after rotating the keys of the underlying identifier that issued the ACDC.
In fact, signing an ACDC would not be terribly useful because a signature of an ACDC that is not anchored to a KEL would only be useful until the keys of the underlying signing identifier were rotated. This means that a bare signature of an ACDC would not, on its own, provide a secure, reproducible verification of that ACDC because it is missing a key index and thus, when the underlying issuing identifier rotates keys, would become unverifiable, assuming that the verifier always demands the use of the most current keys for an issuer that it knows about.
If this doesn't make a lot of sense yet that's okay. The note is included here for reference and to stimluate your thinking and further questioning.
## SAIDs: Self-Addressing Identifiers
ACDCs rely on the concept of **Self-Addressing Identifiers (SAIDs)** to create identifiers for data. The SAID is generated by hashing the content it represents, after placing '###' characters as a placeholder for the SAID attribute in the data, and finally embedding the SAID within the data it is a digest of, making the data and its identifier mutually tamper-evident. SAIDs are a crucial part of ACDCs and will be presented later in the definition of credential schemas.
## ACDC Structure: Envelope and Payload
Conceptually, an ACDC has two parts, the envelope and the payload. Both are shown together in the following example of a vLEI QVI credential.
### Sample ACDC - a QVI credential
This sort of credential is issued to an organization who has been approved to issue vLEI credentials. In this example the rule attributes are abbreviated.
```json
{
"v": "ACDC10JSON000521_",
"d": "EAj8mVqmr-mb6_sSagoy-GwEQdBlkPUHVkXjilAFBe1p",
"i": "EN6zbvE2f8-FWP9bcYOknYXnZrCnmMS6Ot2ctYEtXIV7",
"ri": "EBYWGQLsUB1q1MaD5Ub9eVP10LIk071FoxSvZPYyKVDu",
"s": "EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao",
"a": {
"d": "EEaFWpwcg78La7agmlpklLlNADl4emfS9J71WFIxId8W",
"i": "EOc_QXByf6e-4_q80tG4Kay-MOw2GYqkbiifvepIYmKi",
"dt": "2025-06-11T21:26:59.963634+00:00",
"LEI": "254900OPPU84GM83MG36"
},
"r": {
"d": "EDIai3Wkd-Z_4cezz9nYEcCK3KNH5saLvZoS_84JL6NU",
"usageDisclaimer": {
"l": "Usage of a valid, unexpired, and non-revoked vLEI Credential..."
},
"issuanceDisclaimer": {
"l": "All information in a valid, unexpired, and non-revoked..."
}
}
}
```
Let's break this down.
### Envelope
**The Envelope** consists of a few top-level fields in the ACDC object *about* the ACDC itself.
```json
{
"v": "ACDC10JSON000521_",
"d": "EAj8mVqmr-mb6_sSagoy-GwEQdBlkPUHVkXjilAFBe1p",
"i": "EN6zbvE2f8-FWP9bcYOknYXnZrCnmMS6Ot2ctYEtXIV7",
"ri": "EBYWGQLsUB1q1MaD5Ub9eVP10LIk071FoxSvZPYyKVDu",
"s": "EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao",
...payload fields
}
```
These fields are:
* `v`: Specifies the version and serialization format (like JSON or CBOR).
* `d`: The unique identifier for *this specific ACDC instance*. This is a SAID.
* `i`: The AID (Autonomic Identifier) of the entity that *issued* the ACDC.
* `s`: The SAID of the *Schema* that defines the structure and rules for the data inside this ACDC.
### Payload
**The Payload** contains the data of the credential in three main parts. In this sample the rules attributes are abbreviated.
```json
{
...metadata fields
"a": {
"d": "EEaFWpwcg78La7agmlpklLlNADl4emfS9J71WFIxId8W",
"i": "EOc_QXByf6e-4_q80tG4Kay-MOw2GYqkbiifvepIYmKi",
"dt": "2025-06-11T21:26:59.963634+00:00",
"LEI": "254900OPPU84GM83MG36"
},
"r": {
"d": "EDIai3Wkd-Z_4cezz9nYEcCK3KNH5saLvZoS_84JL6NU",
"usageDisclaimer": {
"l": "Usage of a valid, unexpired, and non-revoked vLEI Credential..."
},
"issuanceDisclaimer": {
"l": "All information in a valid, unexpired, and non-revoked..."
}
}
}
```
The fields are:
* `a` (Attributes): The core data or claims being made (e.g., name: "Alice", role: "Admin")
* `e` (Edges): Optional links to *other* ACDCs, creating verifiable chains of credentials
* `r` (Rules): Optional machine-readable rules or references to legal agreements (like Ricardian Contracts) associated with the credential
The example shown above does not have any edges, links to other ACDCs, and thus has no "e" section. A credential with any chain to other credentials would have edges and thus data in the "e" section.
## ACDCs Security and Verifiability
ACDCs leverage KERI's core security principles to provide strong guarantees:
* **Authenticity:** Every ACDC is digitally signed by its issuer (`i` field). Using the issuer's KEL (Key Event Log), anyone can verify that the issuer actually created and authorized that specific ACDC.
* **Integrity:** The ACDC's own identifier (`d` field) is a SAID. This means the identifier is a cryptographic hash (digest) of the ACDC's contents. If anything in the ACDC changes (even a single character), the SAID will no longer match, proving it has been tampered with.
* **Schema Verification:** The schema defining the ACDC's structure is also identified by a SAID (`s` field). This ensures that everyone agrees on the structure and rules the credential must follow, and that the schema itself hasn't been tampered with.
* **End-Verifiability:** Like all things in KERI, ACDCs are designed to be verifiable by anyone who receives them, relying only on the ACDC itself and the issuer's KEL, without needing to trust intermediaries.
These features make ACDCs a robust foundation for building trustable digital interactions, from simple claims to complex authorization workflows.
Now that you understand the basic concept of an ACDC, the next step is to learn how to define its structure using **schemas**. In the next notebook, we'll dive into creating ACDC schemas and making them verifiable with SAIDs.
## How an ACDC is Connected to an AID
An ACDC connects to an AID by anchoring events from a transaction event log, a TEL, for the creation of the registry a credential is issued from as well as the issuance of the credential itself. This is shown in the below diagram.

### Connecting regstries and ACDCs to the KEL
First, before issuing an ACDC the issuer must create an ACDC registry. Since each ACDC has a reference back to the registry the ACDC was issued from then the registry must exist first. Following registry creation the issuer may issue credentials as shown in the diagram above. Transaction Event Logs (TELs) are used to anchor both registries and ACDCs to a KEL. This process is called "anchoring" and provides cryptographic verifiability to the registry and any ACDCs issued.
### ACDC Registries and its Anchor
This registry will have a SAID identifier like `ELh3eYC2W_Su1izlvm0xxw01n3XK8bdV2Zb09IqlXB7A` like is shown in the below registry inception event, a `vcp` event. The below example shows both a registry creation event (`vcp` for verifiable registry inception) and its anchoring seal, the CESR event stream that looks like `-GAB0AAAAAAAAAAAAAAAAAAAAABwEOWdT7a7fZwRz0jiZ0DJxZEM3vsNbLDPEUk-ODnif3O0`. The CESR stream below is spaced out and annotated for readability. All cryptographic signatures in KERI and ACDC are expressed as CESR streams.
```json
{
"v" : "KERI10JSON00011c_",
"i" : "ELh3eYC2W_Su1izlvm0xxw01n3XK8bdV2Zb09IqlXB7A",
"ii": "EJJR2nmwyYAfSVPzhzS6b5CMZAoTNZH3ULvaU6Z-i0d8",
"s" : "0",
"t" : "vcp",
"b" : ["BbIg_3-11d3PYxSInLN-Q9_T2axD6kkXd3XRgbGZTm6s"],
"c" : []
"a" : {
"d": "EEBp64Aw2rsjdJpAR0e2qCq3jX7q7gLld3LjAwZgaLXU"
}
}
-GAB # CESR attachment group code for the TEL anchor
0AAAAAAAAAAAAAAAAAAAAABw # KEL seq. no. of the anchoring interaction event. All 'A's = 0 in Base64, the first 'B' means "1".
EOWdT7a7fZwRz0jiZ0DJxZEM3vsNbLDPEUk-ODnif3O0 # SAID of KEL event anchroing this "vcp" event.
```
### ACDC Issuance and its Anchor
An issuance in a TEL includes the digest of the ACDC that was issued in the "i" field and also points to the registry the ACDC was issued from in the "ri" field, what [will become](https://trustoverip.github.io/tswg-acdc-specification/#top-level-fields) the "rd" field.
```json
{
"v" : "KERI10JSON00011c_",
"i" : "Ezpq06UecHwzy-K9FpNoRxCJp2wIGM9u2Edk-PLMZ1H4",
"s" : "0",
"t" : "iss",
"dt": "2021-05-27T19:16:50.750302+00:00",
"ri": "ELh3eYC2W_Su1izlvm0xxw01n3XK8bdV2Zb09IqlXB7A"
}
-GAB
0AAAAAAAAAAAAAAAAAAAAAAw
ELvaU6Z-i0d8JJR2nmwyYAZAoTNZH3UfSVPzhzS6b5CM
```
### ACDC Revocation and its Anchor
Somewhat different from an issuance, the TEL event for a revocation points back to the "iss" issuance event with the "p" property, to the ACDC with the "i", property, and contains a date and timestamp for when the revocation occurred.
```json
{
"v" : "KERI10JSON00011c_",
"i" : "Ezpq06UecHwzy-K9FpNoRxCJp2wIGM9u2Edk-PLMZ1H4",
"s" : "1",
"t" : "rev",
"dt": "2021-05-27T19:16:50.750302+00:00",
"p" : "EY2L3ycqK9645aEeQKP941xojSiuiHsw4Y6yTW-PmsBg"
}
-GAB
0AAAAAAAAAAAAAAAAAAAAABA
ELvaU6Z-i0d8JJR2nmwyYAZAoTNZH3UfSVPzhzS6b5CM
```
π SUMMARY
Authentic Chained Data Containers (ACDCs) are KERI's version of verifiable credentials, acting as cryptographically sealed envelopes for sharing verifiable claims. They rely on Self-Addressing Identifiers (SAIDs)βunique IDs derived from the content itselfβto ensure integrity. In order to issue an ACDC a registry must be created. Both the creation of a registry and the creation of an ACDC involve creation of separate transaction event logs (TELs). A registry has its own TEL to record when the registry was created and the changing of any registry backers. Each ACDC has its own TEL to record the issuance and revocation state of the ACDC.
An ACDC consists of an 'Envelope' with metadata (like version v, its own SAID d, issuer AID i, and schema SAID s) and a 'Payload' containing the actual data (attributes a), optional links to other ACDCs (edges e), and optional rules (r). ACDCs provide authenticity via the issuer's signature, integrity through their SAID, schema verification via the schema's SAID, and are end-verifiable using the issuer's KEL.
[<- Prev (Multisignature Identifiers)](101_48_Multisignature_Identifiers.ipynb) | [Next (Schemas) ->](101_55_Schemas.ipynb)
# ACDC Schemas: Defining Verifiable Structures
π― OBJECTIVE
Explain the role of schemas in defining ACDC structures, how they leverage Self-Addressing Identifiers (SAIDs) for verifiability, discover the structure of an ACDC schema, and learn how to create and process a basic schema. Understand that ACDCs are ordered field maps which means that order of attributes in any produced JSON must use a deterministic order that is canonically defined in the JSON schema.
## Purpose of Schemas
Before we can issue or verify an Authentic Chained Data Container (ACDC) we need a blueprint that describes exactly what information it should contain and how that information should be structured. This blueprint is called a **Schema**.
Schemas serve several purposes:
* **Structure and Validation:** They define the names, data types, and constraints for the data within an ACDC. This allows recipients to validate that a received ACDC contains the expected information in the correct format.
* **Interoperability:** When different parties agree on a common schema, they can reliably exchange and understand ACDCs for a specific purpose (e.g., everyone knows what fields to expect in a "Membership Card" ACDC).
* **Verifiability:** As we'll see, ACDC schemas themselves are cryptographically verifiable, ensuring the blueprint hasn't been tampered with.
π Security Note
Security is a major reason why ACDC schemas are necessary and also why ACDC schemas must be immutable. Using immutable schemas to describe all ACDCs prevents any type of malleability attack and ensures that recipients always know precisely the kind of data to expect from an ACDC.
### Ordering of attributes
It is essential to understand that ACDCs are **ordered field maps**, which means that the order in which fields appear in the JSON of an ACDC must be specific and deterministic. This design constraint is non-existent in much of the Javascript world and many other credential formats, and its an essential part of what makes ACDC secure. A deterministic ordering of fields must be used in order to enable cryptographic verifiability. A non-deterministic field order would mean digest (hashing) verification would fail because attribute order would be unpredictable. So, while initially seeming inconvenient, the ordered field maps provide predictability and cryptographic verifiability.
π Security Note
It also happens that using ordered field maps protects against data malleability attacks. If strict insertion order was not preserved or required then an attacker could inject JSON into an ACDC being shared and possibly cause undefined, unknown, or unintended behavior for the recipient of an ACDC.
#### Canonical ordering of ACDC attributes
This order is set by the JSON schema document, as in the **canonical ordering** of data attributes in an ACDC is defined by the JSON schema document, **not lexicographical order**. Admittedly, ordering of attributes in JSON is not yet standard practice in the JSON and Javascript worlds, yet is essential from a security perspective.
##### Python and ordered dicts
Also, as of Python 3.7 the [`json`](https://docs.python.org/3/library/json.html) built-in package preserves input (insertion) and output order of `dict` structs used for JSON serialization and deserialization, meaning insertion order is preserved.
##### Javascript and ordered Maps
As of ECMAScript 2015 the [Map implementation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map), used for JSON serialization and deserialization, uses insertion order to create a predictable ordering of fields, meaning any modern Javascript implementation will preserve insertion order.
#### Other languages
If you use a different language implementation of KERI, ACDC, or CESR then you must ensure it preserves insertion order of attributes for ACDC validation to succeed.
β οΈ Validation Warning
ACDCs must have ordered field maps in order to be reliably verifiable. Any change to the order of fields that is not also consistent with the schema will result in a validation failure.
## Writing ACDC Schemas
ACDC schemas are written using the **JSON Schema** specification. If you're familiar with JSON Schema, you'll find ACDC schemas very similar, with a few KERI-specific conventions.
The main parts of a typical ACDC schema include metadata, properties, metadata of the properties, attributes, edges, and rules. Each of these main parts are previewed below in an abbreviated schema document.
### Sample Schema for an ACDC
As a demonstration the below schema is titled "Sample Schema" which is a label of the type of credential that this schema describes. This particular schema does not have any edges, the "e" section, or rules, the "r" section. An upcoming training will explore those sections. Whitespace below was added for readability. Actual JSON Schemas use no extra whitespace.
```json
{
"$id" : "EJgBEKtba5ewUgG3k268YadY2eGBRrsVF6fF6tLyoRni",
"$schema" : "http://json-schema.org/draft-07/schema#",
"title" : "Sample Schema",
"description" : "A very basic credential schema for demonstration.",
"type" : "object",
"credentialType": "SampleCredential",
"version": "1.0.0",
"properties": {
"v" : {...},
"d" : {...},
"u" : {...},
"i" : {...},
"ri": {...},
"s" : {...},
"a" : {...}
},
"additionalProperties": false,
"required": ["v","d","i","ri","s","a"]
}
```
#### Schema Metadata (Top Level)
These attributes describes the schema document itself.
* `$id`: This field holds the SAID of the entire schema file once processed. It's not a URL like in standard JSON Schema. It's computed after all internal SAIDs are calculated.
* `$schema`: Specifies the JSON Schema version (e.g., `"http://json-schema.org/draft-07/schema#"`)
* `title`, `description`: Human-readable name and explanation
* `type`: Usually `"object"` for the top level of an ACDC schema
* `credentialType`: A specific name for this type of credential
* `version`: A semantic version for this specific credential type (e.g., `"1.0.0"`) to manage schema evolution (Distinct from the ACDC instance's `v` field).
* `additionalProperties`: Controls whether the ACDC may have extra properties in addition to what is defined in the JSON Schema. The default is true. If false then adding any properties beyond those defined in the schema will cause a validation error.
* `required`: declares the attributes of the "properties" section that must have data values defined in the ACDC. If any of the required properties are missing in the resulting ACDC JSON, then validation will fail.
#### `properties` section (Top Level)
Inside the top level "properties" attribute there are two groups of fields including ACDC metadata and ACDC data attributes (payload). These fields define what appears in the ACDC's envelope and payload.
```json
{
...
"properties": {
"v": {"description": "Credential Version String","type": "string"},
"d": {"description": "Credential SAID", "type": "string"},
"u": {"description": "One time use nonce", "type": "string"},
"i": {"description": "Issuer AID", "type": "string"},
"rd": {"description": "Registry SAID", "type": "string"},
"s": {"description": "Schema SAID", "type": "string"},
"a": {...},
"e": {...},
"r": {...},
},
...
}
```
The metadata attributes include "v", "d", "u", "i", "rd", and "s" attributes.
The data attributes, or ACDC payload, include the "a", "e", and "r" attributes.
Each are explained below.
##### ACDC Metadata Fields
The ACDC metadata fields describe data that shows up at the top level of an ACDC and describe the ACDC itself, such as who issued the credential, what schema it has, and any privacy-preserving attributes.
* `v`: ACDC version/serialization - a CESR version string describing the version of the CESR and ACDC protocols used to encode this ACDC.
* `d`: ACDC SAID - The self-addressing identifier (digest) of the issued ACDC.
* `u`: salty nonce - an optional nonce used to blind the properties section during a privacy-preserving graduated disclosure negotiation.
* `i`: Issuer AID - The AID prefix of the identifier who issued this ACDC.
* `rd`: Registry SAID - Formerly the "ri" attribute; the SAID of the credential registry of the issuer who issued this ACDC.
* `s`: Schema SAID - The SAID of the JSON schema document that describes the data in this ACDC and that will be used to validate the data going into or being pulled out of this ACDC.
### ACDC Properties Payload Sections
The actual data stored inside of an ACDC including data attributes, chained credentials (edges), and any rules (legal language) defined for an ACDC. The reason chained credentials are stored in what is called an "edge" section is because chained ACDCs form a graph where the nodes are credentials and the edges are pointers between credential nodes in the graph.
```json
{
...
"properties": {
...
"a": {...},
"e": {...},
"r": {...},
},
...
}
```
* `a`: Defines the structure for the **attributes block**, which holds the actual data or claims being made by the credential.
* `e`: Defines any links to chained credentials, known as edges.
* `r`: Defines any legal rules for a credential such as a terms of service or a legal disclaimer for a credential. This is where **Ricardian Contracts** enter in to an ACDC.
The attribute section is where most of the action happens and is typically the largest section of a credential. We break it down next.
#### ACDC Attributes Payload Section
The "a" or attributes section of an ACDC payload is where the data for a credential is stored. This data may be stored in one of two ways, in the "compacted" and blinded form as a SAID, or in the "un-compacted" form where the data attribute names and values are un-blinded and visible. The blinding and un-blinding process are used to control negotiation of information disclosure during the graduated disclosure process, ACDC's form of selective disclosure.
```json
{
...
"properties": {
...
"a": {
"oneOf": [
{ "description": "Attributes block SAID", "type": "string"},
{ "$id": "ED614TseulOlXWhFNsOcKIKt9Na0gCByugqyKVsva-gl",
"description": "Attributes block", "type": "object",
...
}
]
},
...
},
...
}
```
The attributes of the "a" section ACDC are as follows:
* **`oneOf`**: This standard JSON Schema keyword indicates that the value for the `a` block in an actual ACDC instance can be *one of* the following two formats:
1. **Compacted Form (String):**
* `{"description": "Attributes block SAID", "type": "string"}`
* This option defines the *compact* representation. Instead of including the full attributes object, the ACDC can simply contain a single string value: the SAID of the attributes block itself. This SAID acts as a verifiable reference to the full attribute data, which might be stored elsewhere. **(We won't cover compact ACDCs in this material.)**
2. **Un-compacted Form (Object):**
* `{"$id": "", "description": "Attributes block", "type": "object", ...}`
* This option defines the full or un-compacted representation, where the ACDC includes the complete attributes object directly.
##### Inside an un-compacted ACDC attributes section
You can easily identify an un-compacted ACDC attributes section because it has both an "$id" and a "properties" attribute where all the data is stored. A few metadata attributes go along with this.
```json
{
...
"properties": {
...
"a": {
"oneOf": [
{
"$id": "ED614TseulOlXWhFNsOcKIKt9Na0gCByugqyKVsva-gl",
"description": "Attributes block",
"type": "object",
"properties": {
"d": {"description": "Attributes data SAID", "type": "string"},
"i": {"description": "Issuee AID", "type": "string"},
"dt": {"description": "Issuance date time", "type": "string", "format": "date-time"},
"claim": {"description": "The simple claim being made","type": "string"}
},
"additionalProperties": false,
"required": ["d","i","dt","claim"]
}
]
},
...
},
...
}
```
This schema describes a JSON object that looks like the following:
```json
{
"d": "ENSOVw2kLhPSNbCWlOir8BEB2N2NDskgBNDbx7L1qJsk",
"i": "EOc_QXByf6e-4_q80tG4Kay-MOw2GYqkbiifvepIYmKi",
"dt": "2025-06-11T21:29:49.537000+00:00",
"claim": "some claim value"
}
```
A lot of schema definition for a simple credential!
Each of the attributes are defined as follows:
* **`$id`**: This field will hold the SAID calculated for *this specific attributes block structure* after the schema is processed (`SAIDified`). Initially empty `""` when writing the schema.
* **`description`**: Human-readable description of this block.
* **`type`: `"object"`**: Specifies that this form is a JSON object.
* **`properties`**: Defines the fields contained within the attributes object:
* **`d`**: Holds the SAID calculated from the *actual data* within the attributes block
* **`i`**: The AID of the **Issuee** or subject of the credential β the entity the claims are *about*.
* **`dt`**: An ISO 8601 date-time string indicating when the credential was issued.
* **`claim`** (and other custom fields): These are the specific data fields defined by your schema. In this example, `"claim"` is a string representing the custom information this credential conveys. You would define all your specific credential attributes here.
* **`additionalProperties`, `required`:** Standard JSON Schema fields controlling whether extra properties are allowed and which defined properties must be present. (see the complete schema [here](config/schemas/sample_schema.bak.json))
βΉοΈ NOTE
The ACDC schema definition allows for optional payload blocks called
e (edges) and
r (rules).
- The
e section defines links (edges) to other ACDCs, creating verifiable chains of related credentials. For more details see edges.
- The
r section allows embedding machine-readable rules or legal prose, such as Ricardian Contracts, directly into the credential. For more details see rules.
### Writing your ACDC Schema
To write your schema, most of the customization will happen inside the payload attributes block (`a`). Here you can add claims according to specific needs. When you chain credentials you will use the "e" section. And when you set rules for your credentials you will use the "r" section. We get into each of these subjects in upcoming trainings.
### Full Schema Example
The below sample schema illustrates a complete, sample credential that only has attributes, no edges, and no rules. Whitespace has been somewhat trimmed and in some places added for readability and conciseness. When you use JSON schemas then the formatting will significantly expand the line count of a schema beyond what is shown below.
```json
{
"$id": "EJgBEKtba5ewUgG3k268YadY2eGBRrsVF6fF6tLyoRni",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Sample Schema",
"description": "A very basic credential schema for demonstration.",
"type": "object",
"credentialType": "SampleCredential",
"version": "1.0.0",
"properties": {
"v": { "description": "Credential Version String", "type": "string" },
"d": { "description": "Credential SAID", "type": "string" },
"u": { "description": "One time use nonce", "type": "string" },
"i": { "description": "Issuer AID", "type": "string" },
"ri": { "description": "Registry SAID", "type": "string" },
"s": { "description": "Schema SAID", "type": "string" },
"a": {
"oneOf": [
{ "description": "Attributes block SAID", "type": "string" },
{
"$id": "ED614TseulOlXWhFNsOcKIKt9Na0gCByugqyKVsva-gl",
"description": "Attributes block", "type": "object",
"properties": {
"d": { "description": "Attributes data SAID", "type": "string" },
"i": { "description": "Issuee AID", "type": "string" },
"dt": { "description": "Issuance date time", "type": "string", "format": "date-time" },
"claim": { "description": "The simple claim being made", "type": "string" }
},
"additionalProperties": false,
"required": [ "d", "i", "dt", "claim" ]
}
]
}
},
"additionalProperties": false,
"required": [ "v", "d", "i", "ri", "s", "a" ]
}
```
If you want to see a production-grade credential schema that has both edges and rules, you may review the [GLEIF vLEI Official Organizational Role (OOR)](https://github.com/WebOfTrust/schema/blob/main/vLEI/legal-entity-official-organizational-role-vLEI-credential.schema.json) credential schema.
π SUMMARY
An ACDC Schema acts as an ordered, verifiable blueprint defining the structure, data types, rules, and canonical ordering for attributes within an Authentic Chained Data Container (ACDC). Written using the JSON Schema specification, they ensure ACDCs have the expected format (validation) and enable different parties to understand exchanged credentials (interoperability).
Key components include:
top-level metadata (like the schema's SAID in $id, title, credentialType, version)
a properties section defining the ACDC envelope fields (v, d, i, s, etc.)
a payload section. The main payload section is attributes (a), containing issuer/issuee info and custom claims, with optional sections for edges (e) linking other ACDCs, and rules (r).
**Remember**, all fields contained within an ACDC must be ordered according to **insertion order**, not lexicographic (alphabetical) order. This is essential for both cryptographic verifiability and security.
[<- Prev (ACDC)](101_50_ACDC.ipynb) | [Next (Saidify schema) ->](101_60_Saidify_schema.ipynb)
# SAIDifying ACDC Schemas
π― OBJECTIVE
Explain the purpose and benefits of using Self-Addressing Identifiers (SAIDs) for ACDC schemas and demonstrate the practical, recursive process of calculating and embedding these SAIDs into an ACDC schema file ("SAIDifying").
## Role of Schema SAIDs
A key feature of KERI and ACDCs is the use of SAIDs (Self-Addressing Identifiers) for schemas. The SAID in a schema's `$id` field is a digest of the canonical form of that schema block.
Why it matters:
- **Lookup:** SAIDs provide a universal, unique identifier to retrieve a specific, verified version of a schema.
- **Immutability:** Once a schema version is SAIDified and published, it's cryptographically locked. New versions require a new SAID.
- **Integrity:** If anyone modifies the schema file after its SAID has been calculated and embedded, the SAID will no longer match the content, making tampering evident.
Calculating and embedding these SAIDs requires a specific process, often called **"SAIDifying"**. This involves calculating the SAIDs for the innermost blocks (like attributes, edges, rules) first, embedding them, and then calculating the SAID for the next level up, until the top-level schema SAID is computed.
## The SAIDification Process
We have provided a sample schema and a utility function to help you SAIDfy the schema. Here are the steps.
### Step 1: Write Schema JSON
First, you will create a JSON file with the basic structure as seen in the previous notebook. Since we provide the schema, you don't need to worry about it. But review the file **[sample_credential_schema.json](config/schemas/sample_schema.bak.json)**, since it is the schema you will use, initially unprocessed. Notice the `$id` fields are initially empty strings `""`.
### Step 2: Process and Embed SAIDs
Now, you need to process the schema to embed the Self-Addressing Identifiers (SAIDs). For this, we use the provided **[Python script](scripts/saidify.py)** available in the scripts folder. It calculates the required digests from the content and adds the SAIDs to the file.
```python
from scripts.saidify import process_schema_file
# Run the saidify script
process_schema_file("./config/schemas/sample_schema.bak.json", "./config/schemas/sample_schema.json", True)
# Displays all "$id" values from any JSON objects found recursively in the schema file
print("\ncalculated saids ($id):")
!jq '.. | objects | .["$id"]? // empty' ./config/schemas/sample_schema.json
```
Successfully wrote processed data to ./config/schemas/sample_schema.json
calculated saids ($id):
"EJgBEKtba5ewUgG3k268YadY2eGBRrsVF6fF6tLyoRni"
"ED614TseulOlXWhFNsOcKIKt9Na0gCByugqyKVsva-gl"
After running this command, if you inspect output **[sample_credential_schema.json](config/schemas/sample_schema.json)**, you will see that the previously empty `"id": ""` fields (both the top-level one and the one inside the a block) have been populated with SAID strings (long Base64-like identifiers).
You now have a cryptographically verifiable schema identified by its top-level SAID!
π‘ TIP
The KERI command-line tool (`kli`) provides the kli saidify command. When used like kli saidify --file <filename> --label '<label>', it calculates a SAID for the specified file content and can embed it into the field matching the label (e.g., "$id" at the top level).
However, automatically processing nested structures and dependencies within complex ACDC schemas typically requires helper tools or custom scripts to ensure all inner SAIDs are calculated and embedded correctly before the final outer SAID is generated.
## Making Schemas Discoverable
For an issuer to issue an ACDC using this schema, and for a recipient/verifier to validate it, they need access to this exact, SAIDified schema definition.
How do they get it? The SAID acts as a universal lookup key. Common ways to make schemas available include:
- Simple Web Server: Host the SAIDified JSON file on a basic web server. Controllers can be configured (often via OOBIs, covered later) to fetch the schema from that URL using its SAID. Β
- Content-Addressable Network: Store the schema on a network like IPFS, where the SAID naturally aligns with the content digest used for retrieval.
- Direct Exchange: For specific interactions, the schema could potentially be exchanged directly between parties (though less common for widely used schemas).
The key point is that the schema, identified by its SAID, must be retrievable by parties needing to issue or verify credentials based on it.
In the next notebook, we'll use our SAIDified schema to set up a Credential Registry and issue our first actual ACDC.
π SUMMARY
KERI uses Self-Addressing Identifiers (SAIDs) as unique, verifiable identifiers for ACDC schemas, embedded in the $id field. A schema's SAID is a cryptographic digest of its content, guaranteeing integrity (tamper-evidence) and immutability (specific to that version). This process, called "SAIDifying," involves calculating and embedding SAIDs recursively from inner blocks outwards. Practically, tools or scripts (like the example process_schema_file or kli saidify) are used to populate the initially empty $id fields in the schema JSON. Once SAIDified, the schema must be accessible (e.g., hosted on a server) so others can retrieve and verify it using its SAID.
[<- Prev (Schemas)](101_55_Schemas.ipynb) | [Next (ACDC Issuance) ->](101_65_ACDC_Issuance.ipynb)
# ACDC Issuance with KLI: Using the IPEX Protocol
π― OBJECTIVE
Demonstrate the process of issuing an Authentic Chained Data Container (ACDC), also known as a Verifiable Credential (VC), from an Issuer to a Holder using the Issuance and Presentation Exchange (IPEX) protocol.
## Prerequisites: Issuer and Holder Setup
Authentic Chained Data Containers (ACDCs) are KERI's implementation of verifiable credentials. They allow entities (controllers) to issue cryptographically verifiable statements (credentials) about subjects (often other controllers) according to predefined structures (schemas). These credentials can then be presented to verifiers to prove claims.
This notebook focuses on the issuance part of the workflow: how an Issuer creates an ACDC and securely delivers it to a Holder. We will use the Issuance and Presentation Exchange (IPEX) protocol, which defines a standard way to handle the offering and acceptance of ACDCs between KERI controllers.
Before any credential issuance can happen, both the entity that will issue the credential (the Issuer) and the entity that will receive it (the Holder) need to have their own KERI Autonomic Identifiers established. This involves initializing their respective keystores and creating their AIDs.
### Holder AID Setup
This should be familiar by now; you initialize a keystore and incept the AID for the holder of the credential.
βΉοΈ NOTE:
The exec function executes a shell command within an IPython/Jupyter session and returns its output as a string. This is useful for assigning output values to variables for later use.
```python
from scripts.utils import exec
from scripts.utils import clear_keri
clear_keri()
holder_keystore_name = "holder_ks"
holder_keystore_passcode = exec("kli passcode generate")
holder_keystore_salt = exec("kli salt")
# Alias for our non-transferable AID
holder_aid = "holder_aid"
# Initialize the keystore
!kli init --name {holder_keystore_name} --passcode {holder_keystore_passcode} --salt {holder_keystore_salt}\
--config-dir ./config \
--config-file keystore_init_config.json
!kli incept --name {holder_keystore_name} --alias {holder_aid} --passcode {holder_keystore_passcode} \
--file ./config/aid_inception_config.json
```
Proceeding with deletion of '/usr/local/var/keri/' without confirmation.
β
Successfully removed: /usr/local/var/keri/
KERI Keystore created at: /usr/local/var/keri/ks/holder_ks
KERI Database created at: /usr/local/var/keri/db/holder_ks
KERI Credential Store created at: /usr/local/var/keri/reg/holder_ks
aeid: BHvBPC-9NFPCVZ62dVYDc-tikZXjhaR1-wY05iyFqYVK
Loading 3 OOBIs...
http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness succeeded
http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wes&tag=witness succeeded
http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wil&tag=witness succeeded
Waiting for witness receipts...
Prefix EPqFKtQQ8kzLxPD943ytdj6a3JcXV_IDgYGu88PWNli0
Public key 1: DF-m7fQxP0BBo68dcFs9FTB1bSyxjWN7ROOLDKCergQ-
### Issuer AID Setup
Perform the same initialization and inception process for the Issuer. The Issuer is the entity that will create and sign the credential.
```python
# Issuer setup
issuer_keystore_name = "issuer_ks"
issuer_keystore_passcode = exec("kli passcode generate")
issuer_keystore_salt = exec("kli salt")
# Alias for our non-transferable AID
issuer_aid = "issuer_aid"
# Initialize the keystore
!kli init --name {issuer_keystore_name} --passcode {issuer_keystore_passcode} --salt {issuer_keystore_salt}\
--config-dir ./config \
--config-file keystore_init_config.json
!kli incept --name {issuer_keystore_name} --passcode {issuer_keystore_passcode} --alias {issuer_aid} \
--file ./config/aid_inception_config.json
```
KERI Keystore created at: /usr/local/var/keri/ks/issuer_ks
KERI Database created at: /usr/local/var/keri/db/issuer_ks
KERI Credential Store created at: /usr/local/var/keri/reg/issuer_ks
aeid: BBisjjBRgSA3zmR5u4BajDVT53zzHuCVyuzwCGk7n6Kr
Loading 3 OOBIs...
http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness succeeded
http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wes&tag=witness succeeded
http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wil&tag=witness succeeded
Waiting for witness receipts...
Prefix EHlIlnBBlmKA08zLU4RDuYCkVs1k7-Jqdavd52P2HQ5h
Public key 1: DDV2pfRjMtXJiHWatJdEeI_CCjV7mhwGz0PK4DOWCS5p
### Establishing Issuer-Holder Connection (OOBI)
For the Issuer to send a credential to the Holder, they first need to discover each other's key state (KEL - Key Event Log). This is done using Out-of-Band Introductions (OOBIs) using witness URLs. This works because a controller's witness maintains a copy of the controller's KEL that may be retrieved by other controllers. Each controller generates an OOBI URL pointing to one of its witnesses. They then exchange these OOBIs (typically through a separate channel, hence "out-of-band") and resolve them. Resolving an OOBI allows a controller to securely fetch and verify the KEL of the other controller via the specified witness. Β
For brevity we skip the challenge/response step which may be used to further authenticate each controller's possession of the private keys associated with their AIDs.
```python
holder_oobi_gen = f"kli oobi generate --name {holder_keystore_name} --alias {holder_aid} --passcode {holder_keystore_passcode} --role witness"
holder_oobi = exec(holder_oobi_gen)
issuer_oobi_gen = f"kli oobi generate --name {issuer_keystore_name} --alias {issuer_aid} --passcode {issuer_keystore_passcode} --role witness"
issuer_oobi = exec(issuer_oobi_gen)
!kli oobi resolve --name {holder_keystore_name} --passcode {holder_keystore_passcode} --oobi-alias {issuer_aid} \
--oobi {issuer_oobi}
!kli oobi resolve --name {issuer_keystore_name} --passcode {issuer_keystore_passcode} --oobi-alias {holder_aid}\
--oobi {holder_oobi}
```
http://witness-demo:5642/oobi/EHlIlnBBlmKA08zLU4RDuYCkVs1k7-Jqdavd52P2HQ5h/witness resolved
http://witness-demo:5642/oobi/EPqFKtQQ8kzLxPD943ytdj6a3JcXV_IDgYGu88PWNli0/witness resolved
### Creating the Issuer's Credential Registry
To issue ACDCs, the Issuer needs a Credential Registry. Think of this as a dedicated log, managed by the Issuer's AID, specifically for tracking the status (like issuance and revocation) of the credentials it manages. The registry itself has an identifier (a SAID, derived from its inception event) and its history is maintained in a Transaction Event Log (TEL). Anchoring events from the TEL into the Issuer's main KEL ensures the registry's state changes are secured by the Issuer's controlling keys. Β
Use `kli vc registry incept` to create a new registry named `issuer_registry` controlled by the Issuer's AID (issuer_aid).
```python
issuer_registry_name="issuer_registry"
!kli vc registry incept --name {issuer_keystore_name} \
--passcode {issuer_keystore_passcode} \
--alias {issuer_aid} \
--registry-name {issuer_registry_name}
```
Waiting for TEL event witness receipts
Sending TEL events to witnesses
Registry: issuer_registry(EEmDzzrlo010uNzZTBcP7aAIH01Dg4ADVWjGFucPe4Oj)
created for Identifier Prefix: EHlIlnBBlmKA08zLU4RDuYCkVs1k7-Jqdavd52P2HQ5h
To query the status of a registry, use the command `kli vc registry status`. This shows the registry's SAID, its current sequence number (how many events have occurred in its TEL), and the controlling AID.
```python
!kli vc registry status --name {issuer_keystore_name} \
--passcode {issuer_keystore_passcode} \
--registry-name {issuer_registry_name}
```
Registry: EEmDzzrlo010uNzZTBcP7aAIH01Dg4ADVWjGFucPe4Oj
Seq No. 0
Controlling Identifier: EHlIlnBBlmKA08zLU4RDuYCkVs1k7-Jqdavd52P2HQ5h
Backers: Not supported
Events: Interaction Allowed
## Schema preparation
As seen before, you need a schema to issue credentials. A schema defines the structure and data types for a specific kind of credential, ensuring consistency and enabling automated validation.
For this example, we have prepared a schema to simulate an access pass for a GLEIF Summit event. It defines the expected attributes for such a pass
```json
"eventName": {
"description": "The event name",
"type": "string"
},
"accessLevel": {
"description": "staff/speaker/attendee",
"type": "string",
"enum": [
"staff",
"speaker",
"attendee"
]
},
"validDate": {
"description": "Valid date yyyy-mm-dd",
"type": "string"
}
```
To see the full schema, click **[here](config/schemas/event_pass_schema.bak.json)**.
### SAIDifying the Credential Schema
You might notice the **[schema file](config/schemas/event_pass_schema.bak.json)** doesn't yet have SAIDs embedded within it. As done before, use the helper script to perform this process, taking the `event_pass_schema.bak.json` file as input and outputting the SAIDified version to `event_pass_schema.json`.
Additionally, capture the top-level SAID of the schema using the function `get_schema_said`
```python
# Imports and Utility functions
from scripts.saidify import process_schema_file, get_schema_said
process_schema_file("config/schemas/event_pass_schema.bak.json", "config/schemas/event_pass_schema.json")
schema_said = get_schema_said("config/schemas/event_pass_schema.json")
print(schema_said)
```
Successfully wrote processed data to config/schemas/event_pass_schema.json
EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK
You can view the complete, SAIDified schema definition **[here](config/schemas/event_pass_schema.json)**. Notice the `$id` fields are now populated with SAIDs.
### Making the Schema Discoverable
After the schema has been SAIDified, it needs to be made available so that any party needing it (like the Issuer and Holder) can retrieve and verify it. In KERI, discovery often happens via OOBIs.
For this tutorial, we use a simple service called **vLEI-server**, which acts as a basic schema cache. It's essentially an HTTP file server pre-loaded with the SAIDified schema. It exposes an OOBI URL endpoint for each schema it holds, allowing controllers to resolve the schema using its SAID.
This has already been prepared, so you can query the schema directly from the vLEI-server using its SAID via a simple HTTP request.
```python
!curl -s http://vlei-server:7723/oobi/{schema_said}
```
{"$id":"EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK","$schema":"http://json-schema.org/draft-07/schema#","title":"EventPass","description":"Event Pass Schema","type":"object","credentialType":"EventPassCred","version":"1.0.0","properties":{"v":{"description":"Credential Version String","type":"string"},"d":{"description":"Credential SAID","type":"string"},"u":{"description":"One time use nonce","type":"string"},"i":{"description":"Issuer AID","type":"string"},"ri":{"description":"Registry SAID","type":"string"},"s":{"description":"Schema SAID","type":"string"},"a":{"oneOf":[{"description":"Attributes block SAID","type":"string"},{"$id":"ELppbffpWEM-uufl6qpVTcN6LoZS2A69UN4Ddrtr_JqE","description":"Attributes block","type":"object","properties":{"d":{"description":"Attributes data SAID","type":"string"},"i":{"description":"Issuee AID","type":"string"},"dt":{"description":"Issuance date time","type":"string","format":"date-time"},"eventName":{"description":"The event name","type":"string"},"accessLevel":{"description":"staff/speaker/attendee","type":"string","enum":["staff","speaker","attendee"]},"validDate":{"description":"Valid date yyyy-mm-dd","type":"string"}},"additionalProperties":false,"required":["d","i","dt","eventName","accessLevel","validDate"]}]}},"additionalProperties":false,"required":["v","d","i","ri","s","a"]}
### Resolving the Schema OOBI
Both the Issuer and the Holder need to know the schema definition to understand the structure of the credential being issued/received. They achieve this by resolving the schema's OOBI URL provided by the `vLEI-server`.
```python
schema_oobi_alias = "schema_oobi"
schema_oobi = f"http://vlei-server:7723/oobi/{schema_said}"
!kli oobi resolve --name {holder_keystore_name} --passcode {holder_keystore_passcode} --oobi-alias {schema_oobi_alias} \
--oobi {schema_oobi}
!kli oobi resolve --name {issuer_keystore_name} --passcode {issuer_keystore_passcode} --oobi-alias {schema_oobi_alias}\
--oobi {schema_oobi}
```
http://vlei-server:7723/oobi/EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK resolved
http://vlei-server:7723/oobi/EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK resolved
## Credential Creation Process
With the Issuer and Holder identities established, connected via OOBI, the Issuer registry created, and the schema SAIDified and resolved, we are now ready to actually create the credential.
### Step 1: Defining Credential Attributes
To create a specific credential instance, you must provide the actual values for the attributes defined in the schema (eventName, accessLevel, validDate). These values are typically provided in a separate data file (e.g., JSON), which is then referenced by the `kli vc create command`.
Here's the data for the specific event pass we're issuing:
```python
!cat config/credential_data/event_pass_cred_data.json
```
{
"eventName":"GLEIF Summit",
"accessLevel":"staff",
"validDate":"2026-10-01"
}
### Step 2: Creating the Credential
The `kli vc create` command is used by the Issuer to generate the ACDC. Let's break down the parameters: Β
- `--name`, `--passcode`, and `--alias`: Identify the Issuer's keystore and the specific AID within that keystore that will act as the credential issuer.
- `--registry-name`: Specifies the credential registry the Issuer will use to manage this credential's lifecycle (issuance/revocation). Β
- `--schema`: Provides the SAID of the ACDC schema (event_pass_schema.json in this case) that defines the structure of this credential. Β
- `--recipient`: Specifies the AID of the entity the credential is about (the Holder/subject). Β
- `--data`: Points to the file containing the specific attribute values for this credential instance (@ indicates it's a file path). Β
- `--time`: Provides an issuance timestamp for the credential.
Executing this command creates the credential, generates its SAID, and records an issuance event in the specified registry's TEL, anchoring it to the Issuer's KEL.
```python
time = exec("kli time")
!kli vc create --name {issuer_keystore_name} --passcode {issuer_keystore_passcode} \
--alias {issuer_aid} \
--registry-name {issuer_registry_name} \
--schema {schema_said} \
--recipient {holder_aid} \
--data "@./config/credential_data/event_pass_cred_data.json" \
--time {time}
```
Waiting for TEL event witness receipts
Sending TEL events to witnesses
ELHVOxhpwnhi1fFPVaKx0z4Wgq-a6vRhAlkQcti9VM7b has been created.
### Viewing the Issued Credential
The kli vc list command allows the Issuer to see the credentials they have issued. The `--issued` flag specifies listing issued credentials, and `--verbose` shows the full credential content.
```python
!kli vc list --name {issuer_keystore_name} \
--passcode {issuer_keystore_passcode} \
--issued \
--verbose
```
Current issued credentials for issuer_aid (EHlIlnBBlmKA08zLU4RDuYCkVs1k7-Jqdavd52P2HQ5h):
Credential #1: ELHVOxhpwnhi1fFPVaKx0z4Wgq-a6vRhAlkQcti9VM7b
Type: EventPass
Status: Issued β
Issued by EHlIlnBBlmKA08zLU4RDuYCkVs1k7-Jqdavd52P2HQ5h
Issued on 2025-09-12T04:08:04.132496+00:00
Full Credential:
{
"v": "ACDC10JSON0001c4_",
"d": "ELHVOxhpwnhi1fFPVaKx0z4Wgq-a6vRhAlkQcti9VM7b",
"i": "EHlIlnBBlmKA08zLU4RDuYCkVs1k7-Jqdavd52P2HQ5h",
"ri": "EEmDzzrlo010uNzZTBcP7aAIH01Dg4ADVWjGFucPe4Oj",
"s": "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
"a": {
"d": "ECeG0MVFv5MPNmGU-WyCsibchyOj4mpObklXmF7-wncC",
"i": "EPqFKtQQ8kzLxPD943ytdj6a3JcXV_IDgYGu88PWNli0",
"dt": "2025-09-12T04:08:04.132496+00:00",
"eventName": "GLEIF Summit",
"accessLevel": "staff",
"validDate": "2026-10-01"
}
}
Notice the structure of the credential JSON: Β
- `v`: Version string for the ACDC format and serialization.
- `d`: The SAID of this specific credential instance.
- `i`: The Issuer's AID.
- `ri`: The SAID of the credential registry used.
- `s`: The SAID of the schema used.
- `a`: The attributes block, containing:
- `d`: The SAID of the attributes block itself.
- `i`: The Issuee's (Holder's) AID.
- `dt`: The issuance date/time.
- `eventName`, `accessLevel`, and `validDate`: The specific data for this credential.
### Retrieving the Credential's SAID
You need to retrieve the credential's SAID (`d` field) to use it as the identifier in the subsequent IPEX steps. Use `kli vc list` again, but add the `--said` flag to return only the SAID of the matching credential(s).
```python
get_credential_said = f"kli vc list --name {issuer_keystore_name}\
--passcode {issuer_keystore_passcode} --alias {issuer_aid}\
--issued --schema {schema_said}\
--said"
credential_said=exec(get_credential_said)
print(credential_said)
```
ELHVOxhpwnhi1fFPVaKx0z4Wgq-a6vRhAlkQcti9VM7b
## Issuing the Credential to the Holder via IPEX
At this point, the ACDC has been created and recorded in the Issuer's registry, but it still needs to be provided to the Holder. To do so, we use the **Issuance and Presentation Exchange (IPEX) protocol**. IPEX defines a message-based workflow for offering, accepting, or rejecting credentials.
### Step 1: Issuer Sends Grant Message
The Issuer (acting as the "Discloser" in IPEX terms) initiates the issuance by sending a grant message to the Holder (the "Disclosee"). The `kli ipex grant` command facilitates this. Β
The parameters are:
- `--name`, `--passcode`, and `--alias`: Identify the Issuer's keystore and AID.
- `--said`: The SAID of the credential being granted (which we retrieved in the previous step).
- `--recipient`: The AID of the Holder who should receive the credential offer.
- `--time`: A timestamp for the grant message.
This command sends an IPEX grant message containing the credential data to the Holder's KERI mailbox (managed via witnesses).
```python
time = exec("kli time")
!kli ipex grant \
--name {issuer_keystore_name} \
--passcode {issuer_keystore_passcode} \
--alias {issuer_aid} \
--said {credential_said} \
--recipient {holder_aid} \
--time {time}
```
Sending message EMZJcWSDJA62-XyjVChSXBDXoAcDNV_qWlxxZ2NV1m23 to EPqFKtQQ8kzLxPD943ytdj6a3JcXV_IDgYGu88PWNli0
... grant message sent
**Issuer Views Sent Messages (Optional)**
This step isn't strictly required for the workflow but allows the Issuer to view the IPEX messages they have sent using `kli ipex list --sent`. This can be useful for debugging or tracking the state of exchanges. It shows the GRANT message SAID and details about the credential offered.
```python
!kli ipex list --name {issuer_keystore_name} \
--passcode {issuer_keystore_passcode} \
--alias {issuer_aid} \
--sent
```
Sent IPEX Messages:
GRANT - SAID: EMZJcWSDJA62-XyjVChSXBDXoAcDNV_qWlxxZ2NV1m23
Credential ELHVOxhpwnhi1fFPVaKx0z4Wgq-a6vRhAlkQcti9VM7b:
Type: EventPass
Status: Issued β
Issued by EHlIlnBBlmKA08zLU4RDuYCkVs1k7-Jqdavd52P2HQ5h
Issued on 2025-09-12T04:08:04.132496+00:00
Already responded? No β
### Step 2: Holder Receives Grant Message
The Holder needs to check their KERI mailbox for incoming messages. The `kli ipex list --poll` command checks for new IPEX messages. Use `--said` again to extract just the SAID of the received grant message, which is needed for the next step (accepting the credential).
```python
get_ipex_said=f"kli ipex list --name {holder_keystore_name}\
--passcode {holder_keystore_passcode} --alias {holder_aid}\
--poll\
--said"
ipex_said=exec(get_ipex_said)
print(ipex_said)
```
EMZJcWSDJA62-XyjVChSXBDXoAcDNV_qWlxxZ2NV1m23
**Holder Views Received Messages (Optional)**
Similar to the Issuer checking sent messages, the Holder can use `kli ipex list` (without `--sent`) to view received IPEX messages. This confirms receipt of the GRANT offer from the Issuer.
```python
!kli ipex list --name {holder_keystore_name} \
--passcode {holder_keystore_passcode} \
--alias {holder_aid}
```
Received IPEX Messages:
GRANT - SAID: EMZJcWSDJA62-XyjVChSXBDXoAcDNV_qWlxxZ2NV1m23
Credential ELHVOxhpwnhi1fFPVaKx0z4Wgq-a6vRhAlkQcti9VM7b:
Type: EventPass
Status: Issued β
Issued by EHlIlnBBlmKA08zLU4RDuYCkVs1k7-Jqdavd52P2HQ5h
Issued on 2025-09-12T04:08:04.132496+00:00
Already responded? No β
### Step 3: Holder Admits Credential
Now that the Holder has received the `grant` message (identified by `ipex_said`), they can choose to accept the credential using the `kli ipex admit` command. Β
- `--name`, `--passcode`, and `--alias`: Identify the Holder's keystore and AID.
- `--said`: The SAID of the grant message being admitted.
- `--time`: A timestamp for the admit message.
This sends an `admit` message back to the Issuer, confirming acceptance. The Holder's KERI controller automatically verifies the credential against its schema and the Issuer's KEL upon admitting it, and then stores the credential securely.
```python
time = exec("kli time")
!kli ipex admit \
--name {holder_keystore_name} \
--passcode {holder_keystore_passcode} \
--alias {holder_aid} \
--said {ipex_said} \
--time {time}
```
Sending admit message to EHlIlnBBlmKA08zLU4RDuYCkVs1k7-Jqdavd52P2HQ5h
... admit message sent
**Holder Views Sent Admit Message (Optional)**
The Holder can optionally check their sent IPEX messages to confirm the `ADMIT` message was sent.
```python
!kli ipex list --name {holder_keystore_name} \
--passcode {holder_keystore_passcode} \
--alias {holder_aid} \
--sent
```
Sent IPEX Messages:
ADMIT - SAID: EID3UguZFjg_YVUvDBOgrfmRlFOyVDIgCs7Hri91iREB
Admitted message SAID: EMZJcWSDJA62-XyjVChSXBDXoAcDNV_qWlxxZ2NV1m23
Credential ELHVOxhpwnhi1fFPVaKx0z4Wgq-a6vRhAlkQcti9VM7b:
Type: EventPass
Status: Accepted β
### Step 4: Holder Verifies Possession
The issuance process is complete! The Holder now possesses the verifiable credential. They can view it using kli vc list --verbose. The output will be similar to when the Issuer listed the issued credential, confirming the Holder has successfully received and stored the ACDC.
```python
!kli vc list --name {holder_keystore_name} \
--passcode {holder_keystore_passcode} \
--verbose
```
Current received credentials for holder_aid (EPqFKtQQ8kzLxPD943ytdj6a3JcXV_IDgYGu88PWNli0):
Credential #1: ELHVOxhpwnhi1fFPVaKx0z4Wgq-a6vRhAlkQcti9VM7b
Type: EventPass
Status: Issued β
Issued by EHlIlnBBlmKA08zLU4RDuYCkVs1k7-Jqdavd52P2HQ5h
Issued on 2025-09-12T04:08:04.132496+00:00
Full Credential:
{
"v": "ACDC10JSON0001c4_",
"d": "ELHVOxhpwnhi1fFPVaKx0z4Wgq-a6vRhAlkQcti9VM7b",
"i": "EHlIlnBBlmKA08zLU4RDuYCkVs1k7-Jqdavd52P2HQ5h",
"ri": "EEmDzzrlo010uNzZTBcP7aAIH01Dg4ADVWjGFucPe4Oj",
"s": "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
"a": {
"d": "ECeG0MVFv5MPNmGU-WyCsibchyOj4mpObklXmF7-wncC",
"i": "EPqFKtQQ8kzLxPD943ytdj6a3JcXV_IDgYGu88PWNli0",
"dt": "2025-09-12T04:08:04.132496+00:00",
"eventName": "GLEIF Summit",
"accessLevel": "staff",
"validDate": "2026-10-01"
}
}
π SUMMARY
In the KERI and ACDC ecosystem credentials are created and issued as authentic chained data containers (ACDCs) within the Issuance and Presentation Exchange Protocol (IPEX).
Issuance of an ACDC using this protocol involves the following steps:
- Setup: Issuer and Holder established identities (AIDs) and connected via OOBI resolution.
- Registry: Issuer created a credential registry (managed via a TEL) to track credential status.
- Schema: An ACDC schema was defined (using JSON Schema) and made verifiable through SAIDification. It was made discoverable via a simple caching server (vLEI-server) and resolved by both parties using its OOBI.
- Creation: Issuer created the specific ACDC instance using kli vc create, providing data conforming to the schema and linking it to the registry.
- IPEX Issuance:
- Issuer offered the credential using kli ipex grant (sending a GRANT message).
- Holder received the offer (polling with kli ipex list --poll).
- Holder accepted the credential using kli ipex admit (sending an ADMIT message).
- Result: Holder successfully received and stored the verifiable credential (ACDC).
This process ensures that credentials are not only structured and verifiable against a schema but are also securely issued and provided to authenticated KERI identities.
[<- Prev (Saidify schema)](101_60_Saidify_schema.ipynb) | [Next (ACDC Presentation and Revocation) ->](101_70_ACDC_Presentation_and_Revocation.ipynb)
# ACDC Presentation and Revocation with KLI: Using the IPEX Protocol
π― OBJECTIVE
Demonstrate how a Holder presents a previously issued ACDC or Verifiable Credential (VC) to a Verifier using the Issuance and Presentation Exchange (IPEX) protocol.
Understand how IPEX allows credential holders (issuees) to sign that they agree with the terms of a credential.
Conduct a credential revocation and present a revoked credential as an issuer so the holder may learn that a credential they hold has been revoked. Learn that observer infrastructure may be used for pull-style monitoring of credential revocation state.
## Credential Presentation Overview
In the previous notebook, you saw how an Issuer creates and sends an ACDC to a Holder. Now, we'll focus on the next steps in the typical verifiable credential lifecycle: presentation and admittance.
After creating the credential the Issuer must present it to the Holder. In IPEX this presentation is called an IPEX Grant message. After receiving the IPEX Grant message the Holder can then accept the credential by performing an IPEX Admit message. In the prior training this Grant and Admit process were explained.
In this training, following the reception of a credential, the Holder will present it to another party (the Verifier) to prove certain claims or gain access to something. You will again use the IPEX protocol for this exchange, but this time initiated by the Holder. Finally, you will see how the original Issuer can revoke the credential.
### Recap: Issuing the Prerequisite Credential
To present a credential, you first need one! The following code block is a condensed recap of the ACDC Issuance workflow covered in the previous notebook. It quickly sets up an Issuer and a Holder, creates a Credential Registry, defines and resolves a schema, issues an `EventPass` credential from the Issuer to the Holder using IPEX, and ensures the Holder admits it.
βΉοΈ NOTE:
For a detailed explanation of these issuance steps, please refer to the previous notebook.
```python
from scripts.utils import exec
from scripts.saidify import process_schema_file, get_schema_said
from scripts.utils import pr_title, pr_message, pr_continue, clear_keri
clear_keri()
# Holder keystore init and AID inception
holder_keystore_name = "holder_presentation_ks"
holder_keystore_passcode = exec("kli passcode generate")
holder_keystore_salt = exec("kli salt")
holder_aid = "holder_aid"
!kli init --name {holder_keystore_name} --passcode {holder_keystore_passcode} --salt {holder_keystore_salt}\
--config-dir ./config \
--config-file keystore_init_config.json
!kli incept --name {holder_keystore_name} --alias {holder_aid} --passcode {holder_keystore_passcode} \
--file ./config/aid_inception_config.json
# Issuer keystore init and AID inception
issuer_keystore_name = "issuer_presentation_ks"
issuer_keystore_passcode = exec("kli passcode generate")
issuer_keystore_salt = exec("kli salt")
issuer_aid = "issuer_aid"
!kli init --name {issuer_keystore_name} --passcode {issuer_keystore_passcode} --salt {issuer_keystore_salt}\
--config-dir ./config \
--config-file keystore_init_config.json
!kli incept --name {issuer_keystore_name} --passcode {issuer_keystore_passcode} --alias {issuer_aid}\
--file ./config/aid_inception_config.json
# Issuer registry inception
issuer_registry_name="issuer_registry"
!kli vc registry incept --name {issuer_keystore_name} \
--passcode {issuer_keystore_passcode} \
--registry-name {issuer_registry_name} \
--alias {issuer_aid}
# Issuer and Holder oobi
holder_oobi_gen = f"kli oobi generate --name {holder_keystore_name} --alias {holder_aid}\
--passcode {holder_keystore_passcode} --role witness"
holder_oobi = exec(holder_oobi_gen)
issuer_oobi_gen = f"kli oobi generate --name {issuer_keystore_name} --alias {issuer_aid}\
--passcode {issuer_keystore_passcode} --role witness"
issuer_oobi = exec(issuer_oobi_gen)
!kli oobi resolve --name {holder_keystore_name} --passcode {holder_keystore_passcode}\
--oobi-alias {issuer_aid} --oobi {issuer_oobi}
!kli oobi resolve --name {issuer_keystore_name} --passcode {issuer_keystore_passcode}\
--oobi-alias {holder_aid} --oobi {holder_oobi}
# Issuer and Holder resolve schema oobis
schema_oobi_alias = "schema_oobi"
schema_said = get_schema_said("config/schemas/event_pass_schema.json")
schema_oobi = f"http://vlei-server:7723/oobi/{schema_said}"
!kli oobi resolve --name {holder_keystore_name} --passcode {holder_keystore_passcode}\
--oobi-alias {schema_oobi_alias} --oobi {schema_oobi}
!kli oobi resolve --name {issuer_keystore_name} --passcode {issuer_keystore_passcode}\
--oobi-alias {schema_oobi_alias} --oobi {schema_oobi}
# Issuer create VC
time = exec("kli time")
!kli vc create --name {issuer_keystore_name} --passcode {issuer_keystore_passcode} \
--alias {issuer_aid} \
--registry-name {issuer_registry_name} \
--schema {schema_said} \
--recipient {holder_aid} \
--data "@./config/credential_data/event_pass_cred_data.json" \
--time {time}
# Get credential said
get_credential_said = f"kli vc list --name {issuer_keystore_name}\
--passcode {issuer_keystore_passcode} --alias {issuer_aid}\
--issued --said --schema {schema_said}"
credential_said=exec(get_credential_said)
#Issuer grant credential
time = exec("kli time")
!kli ipex grant \
--name {issuer_keystore_name} --passcode {issuer_keystore_passcode} \
--alias {issuer_aid} \
--said {credential_said} \
--recipient {holder_aid} \
--time {time}
# Holder poll and admit credential
get_ipex_said=f"kli ipex list --name {holder_keystore_name} --passcode {holder_keystore_passcode}\
--alias {holder_aid} --poll --said"
ipex_said=exec(get_ipex_said)
time = exec("kli time")
!kli ipex admit \
--name {holder_keystore_name} \
--passcode {holder_keystore_passcode} \
--alias {holder_aid} \
--said {ipex_said} \
--time {time}
pr_continue()
```
Proceeding with deletion of '/usr/local/var/keri/' without confirmation.
β
Successfully removed: /usr/local/var/keri/
KERI Keystore created at: /usr/local/var/keri/ks/holder_presentation_ks
KERI Database created at: /usr/local/var/keri/db/holder_presentation_ks
KERI Credential Store created at: /usr/local/var/keri/reg/holder_presentation_ks
aeid: BCZkp5jCZSrAGst3Ih8NUlRXfPKIOgRm9ONrtVQ21xzf
Loading 3 OOBIs...
http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness succeeded
http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wes&tag=witness succeeded
http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wil&tag=witness succeeded
Waiting for witness receipts...
Prefix EDmofynK4DYITOe2ZN7BOSWigjqonV0MJurARWS_GqbG
Public key 1: DI1Wi9wD1vvFea1AucpkPiqPLYb2zQqnGksSJAf09eGE
KERI Keystore created at: /usr/local/var/keri/ks/issuer_presentation_ks
KERI Database created at: /usr/local/var/keri/db/issuer_presentation_ks
KERI Credential Store created at: /usr/local/var/keri/reg/issuer_presentation_ks
aeid: BHCyVyfwFhoNgQ1MCt1XjRuOUA1hAB2QNtsGyDwf-Zfn
Loading 3 OOBIs...
http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness succeeded
http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wes&tag=witness succeeded
http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wil&tag=witness succeeded
Waiting for witness receipts...
Prefix EH50FjEtKMgmuqmDK6ZePMlBuj486dONDgEQMOZHKPlp
Public key 1: DLE56BwLSekxGRapPG0gKiosIn6YCOqe6ha7xr2TrEfu
Waiting for TEL event witness receipts
Sending TEL events to witnesses
Registry: issuer_registry(EOye1znk5_zjIbFOD2K69vFyeILszmlaHg-rMmkfQs17)
created for Identifier Prefix: EH50FjEtKMgmuqmDK6ZePMlBuj486dONDgEQMOZHKPlp
http://witness-demo:5642/oobi/EH50FjEtKMgmuqmDK6ZePMlBuj486dONDgEQMOZHKPlp/witness resolved
http://witness-demo:5642/oobi/EDmofynK4DYITOe2ZN7BOSWigjqonV0MJurARWS_GqbG/witness resolved
http://vlei-server:7723/oobi/EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK resolved
http://vlei-server:7723/oobi/EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK resolved
Waiting for TEL event witness receipts
Sending TEL events to witnesses
EGV-i9sG9H28DXpLN2PGxWq0tfqJjvMRcvM8HQsDWAvv has been created.
Sending message EGzklyr7aJhhq9Fz0H_PKOZyfYf6LZAv5v6xtYNQ1C9G to EDmofynK4DYITOe2ZN7BOSWigjqonV0MJurARWS_GqbG
... grant message sent
Sending admit message to EH50FjEtKMgmuqmDK6ZePMlBuj486dONDgEQMOZHKPlp
... admit message sent
You can continue β
## The IPEX Presentation Flow
Now that the Holder (`holder_aid`) possesses the `EventPass` credential, you must present it to a Verifier (`verifier_aid`) to prove they have access.
First, confirm the Holder has the credential:
```python
!kli vc list --name {holder_keystore_name} \
--passcode {holder_keystore_passcode} \
--verbose
```
Current received credentials for holder_aid (EDmofynK4DYITOe2ZN7BOSWigjqonV0MJurARWS_GqbG):
Credential #1: EGV-i9sG9H28DXpLN2PGxWq0tfqJjvMRcvM8HQsDWAvv
Type: EventPass
Status: Issued β
Issued by EH50FjEtKMgmuqmDK6ZePMlBuj486dONDgEQMOZHKPlp
Issued on 2025-09-12T04:08:49.611720+00:00
Full Credential:
{
"v": "ACDC10JSON0001c4_",
"d": "EGV-i9sG9H28DXpLN2PGxWq0tfqJjvMRcvM8HQsDWAvv",
"i": "EH50FjEtKMgmuqmDK6ZePMlBuj486dONDgEQMOZHKPlp",
"ri": "EOye1znk5_zjIbFOD2K69vFyeILszmlaHg-rMmkfQs17",
"s": "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
"a": {
"d": "ENczl5MMVY5onNct_N70lQ1KFMj-D5cS2Cof0P91A92n",
"i": "EDmofynK4DYITOe2ZN7BOSWigjqonV0MJurARWS_GqbG",
"dt": "2025-09-12T04:08:49.611720+00:00",
"eventName": "GLEIF Summit",
"accessLevel": "staff",
"validDate": "2026-10-01"
}
}
### Verifier AID Setup
Just like the Issuer and Holder, the Verifier needs its own AID to participate in the protocol securely. Initialize its keystore and incept its AID.
```python
verifier_keystore_name="verifier_ks"
verifier_keystore_passcode = exec("kli passcode generate")
verifier_keystore_salt = exec("kli salt")
# Alias for our non-transferable AID
verifier_aid = "verifier_aid"
# Initialize the keystore
!kli init --name {verifier_keystore_name} --passcode {verifier_keystore_passcode} --salt {verifier_keystore_salt}\
--config-dir ./config \
--config-file keystore_init_config.json
!kli incept --name {verifier_keystore_name} --alias {verifier_aid} --passcode {verifier_keystore_passcode} \
--file ./config/aid_inception_config.json
```
KERI Keystore created at: /usr/local/var/keri/ks/verifier_ks
KERI Database created at: /usr/local/var/keri/db/verifier_ks
KERI Credential Store created at: /usr/local/var/keri/reg/verifier_ks
aeid: BNMnc4WKQ_mpXRhOwUjVHPiHnNJq-xm2oMHfE_U_I_Lf
Loading 3 OOBIs...
http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness succeeded
http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wes&tag=witness succeeded
http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wil&tag=witness succeeded
Waiting for witness receipts...
Prefix EAGnYBs-ystvZGmDO3rayU2iKSkKmfYIs0uwxFAm8gmy
Public key 1: DBeE9xOz1nrn4wEOjD2dY0mZ3qNACvw75iljIJ16wkbb
### Establishing Holder-Verifier Connection (OOBI)
Similar to the Issuer/Holder exchange, the Holder and Verifier must exchange and resolve OOBIs to establish a secure communication channel and verify each other's key states (KELs).
```python
holder_oobi_gen = f"kli oobi generate --name {holder_keystore_name} --alias {holder_aid}\
--passcode {holder_keystore_passcode} --role witness"
holder_oobi = exec(holder_oobi_gen)
verifier_oobi_gen = f"kli oobi generate --name {verifier_keystore_name} --alias {verifier_aid}\
--passcode {verifier_keystore_passcode} --role witness"
verifier_oobi = exec(verifier_oobi_gen)
!kli oobi resolve --name {holder_keystore_name} --passcode {holder_keystore_passcode}\
--oobi-alias {verifier_aid} --oobi {verifier_oobi}
!kli oobi resolve --name {verifier_keystore_name} --passcode {verifier_keystore_passcode}\
--oobi-alias {holder_aid} --oobi {holder_oobi}
```
http://witness-demo:5642/oobi/EAGnYBs-ystvZGmDO3rayU2iKSkKmfYIs0uwxFAm8gmy/witness resolved
http://witness-demo:5642/oobi/EDmofynK4DYITOe2ZN7BOSWigjqonV0MJurARWS_GqbG/witness resolved
### Verifier Resolves Schema OOBI
The Verifier also needs to resolve the OOBI for the ACDC's schema (`event_pass_schema`). This allows the Verifier to retrieve the schema definition and validate that the presented credential conforms to the expected structure and data types. Without the schema, the Verifier wouldn't know how to interpret or validate the credential's content.
```python
!kli oobi resolve --name {verifier_keystore_name} --passcode {verifier_keystore_passcode}\
--oobi-alias {schema_oobi_alias} --oobi {schema_oobi}
```
http://vlei-server:7723/oobi/EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK resolved
### Step 1: Holder Presents Credential (Grant)
Now, the Holder initiates the IPEX exchange to present the credential to the Verifier. The Holder acts as the "Discloser" in this context. The command used is `kli ipex grant`, just like in issuance, but the IPEX roles here are different so the Holder is the discloser and the Verifier is the disclosee.
- `--name`, `--passcode`, `--alias`: Identify the Holder's keystore and AID.
- `--said`: The SAID of the credential being presented.
- `--recipient`: The AID of the Verifier who should receive the presentation.
- `--time`: A timestamp for the grant message.
This sends an IPEX Grant message, effectively offering the credential presentation to the Verifier.
βΉοΈ NOTE: on --time
Including the time --time argument is only necessary when performing multisignature operations. It is shown below for illustrative purposes only.
This argument is necessary for multisignature operations because each participating controller must produce the exact same event, in this case an IPEX Grant message, as all the other members of a multisig group. Since a timestamp is one of the attributes in an IPEX Grant message then in order to produce the exact same event, and thereby the same event digest, the same value for a timestamp must be used by each controller when constructing the event. At the command line this is provided with the `--time` argument to the `kli ipex grant` command.
You will notice the output value of the `kli time` command is used in various places in these Jupyter notebooks. The necessity of the `--time` command is the same for each context; it is only applicable to multi-signature operations.
```python
time = exec("kli time")
!kli ipex grant \
--name {holder_keystore_name} \
--passcode {holder_keystore_passcode} \
--alias {holder_aid} \
--said {credential_said} \
--recipient {verifier_aid} \
--time {time}
```
Sending message EOnPhZZr-9D5E9lyr9dpSu9TonBePJfbhTuFdd4d_F8Q to EAGnYBs-ystvZGmDO3rayU2iKSkKmfYIs0uwxFAm8gmy
... grant message sent
Receiving the Grant message triggers the Verifier's KERI controller to perform several checks automatically:
- Schema Validation: Checks whether the credential structure and data types match the resolved schema.
- Issuer Authentication: Verifies the credential signature against the Issuer's KEL (previously retrieved via OOBI) and, importantly, checks the credential's status (e.g., not revoked) against the Issuer's registry (TEL).
If all checks pass, the Verifier may admit the ACDC, store the validated credential information, and send an IPEX Admit message back to the Holder.
### Step 2: Verifier Receives Presentation
The Verifier needs to check its KERI mailbox(es) for the incoming grant message containing the credential presentation.
Use `kli ipex list --poll` to check the mailbox(es) and extract the SAID of the IPEX Grant message.
```python
get_ipex_said=f"kli ipex list --name {verifier_keystore_name} --passcode {verifier_keystore_passcode}\
--alias {verifier_aid} --poll --said"
ipex_said=exec(get_ipex_said)
print(ipex_said)
pr_continue()
```
EOnPhZZr-9D5E9lyr9dpSu9TonBePJfbhTuFdd4d_F8Q
You can continue β
**Verifier displays credential (Optional)**
Before formally admitting the credential, the Verifier can inspect the received presentation using `kli ipex list --verbose`. This shows the credential details and the status of the IPEX exchange.
```python
!kli ipex list \
--name {verifier_keystore_name} \
--passcode {verifier_keystore_passcode} \
--alias {verifier_aid} \
--verbose
```
Received IPEX Messages:
GRANT - SAID: EOnPhZZr-9D5E9lyr9dpSu9TonBePJfbhTuFdd4d_F8Q
Credential EGV-i9sG9H28DXpLN2PGxWq0tfqJjvMRcvM8HQsDWAvv:
Type: EventPass
Status: Issued β
Issued by EH50FjEtKMgmuqmDK6ZePMlBuj486dONDgEQMOZHKPlp
Issued on 2025-09-12T04:08:49.611720+00:00
Already responded? No β
Full Credential:
{
"v": "ACDC10JSON0001c4_",
"d": "EGV-i9sG9H28DXpLN2PGxWq0tfqJjvMRcvM8HQsDWAvv",
"i": "EH50FjEtKMgmuqmDK6ZePMlBuj486dONDgEQMOZHKPlp",
"ri": "EOye1znk5_zjIbFOD2K69vFyeILszmlaHg-rMmkfQs17",
"s": "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
"a": {
"d": "ENczl5MMVY5onNct_N70lQ1KFMj-D5cS2Cof0P91A92n",
"i": "EDmofynK4DYITOe2ZN7BOSWigjqonV0MJurARWS_GqbG",
"dt": "2025-09-12T04:08:49.611720+00:00",
"eventName": "GLEIF Summit",
"accessLevel": "staff",
"validDate": "2026-10-01"
}
}
The status of the credential is shown by `Already responded? No β` meaning that an IPEX Admit from the Verifier to the Holder has not yet been sent.
### Step 3: Verifier Admits and Validates Presentation (Agreeing to Terms)
An admit is not strictly necessary between the verifier and the holder, though sending an admit is one way the Verifier signals to the holder that the verifier agrees to the terms of the credential presentation. The terms in the credential are specified in the rules section.
The Verifier uses the `kli ipex admit` command to accept the presentation.
```python
time = exec("kli time")
!kli ipex admit \
--name {verifier_keystore_name} \
--passcode {verifier_keystore_passcode} \
--alias {verifier_aid} \
--said {ipex_said} \
--time {time}
```
Sending admit message to EDmofynK4DYITOe2ZN7BOSWigjqonV0MJurARWS_GqbG
... admit message sent
**Verifier Confirms Admission**
Finally, the Verifier can check the status of the received IPEX message again. The Already responded? field should now show Yes β and indicate the response was Admit, confirming the successful presentation and validation.
```python
!kli ipex list \
--name {verifier_keystore_name} \
--passcode {verifier_keystore_passcode} \
--alias {verifier_aid} \
--verbose
```
Received IPEX Messages:
GRANT - SAID: EOnPhZZr-9D5E9lyr9dpSu9TonBePJfbhTuFdd4d_F8Q
Credential EGV-i9sG9H28DXpLN2PGxWq0tfqJjvMRcvM8HQsDWAvv:
Type: EventPass
Status: Issued β
Issued by EH50FjEtKMgmuqmDK6ZePMlBuj486dONDgEQMOZHKPlp
Issued on 2025-09-12T04:08:49.611720+00:00
Already responded? Yes β
Response: Admit (EJEDrSDzp8ZustZb2DWdVVTivei8caUc65k2NTobtbK8)
Full Credential:
{
"v": "ACDC10JSON0001c4_",
"d": "EGV-i9sG9H28DXpLN2PGxWq0tfqJjvMRcvM8HQsDWAvv",
"i": "EH50FjEtKMgmuqmDK6ZePMlBuj486dONDgEQMOZHKPlp",
"ri": "EOye1znk5_zjIbFOD2K69vFyeILszmlaHg-rMmkfQs17",
"s": "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
"a": {
"d": "ENczl5MMVY5onNct_N70lQ1KFMj-D5cS2Cof0P91A92n",
"i": "EDmofynK4DYITOe2ZN7BOSWigjqonV0MJurARWS_GqbG",
"dt": "2025-09-12T04:08:49.611720+00:00",
"eventName": "GLEIF Summit",
"accessLevel": "staff",
"validDate": "2026-10-01"
}
}
## Credential Revocation by Issuer
Credentials may need to be invalidated before their natural expiry (if any). This process is called revocation. In KERI/ACDC, revocation is performed by the original Issuer of the credential. The Issuer records a revocation event in the credential registry's Transaction Event Log (TEL), and that event is anchored to the Issuer's main KEL.
The `kli vc revoke` command is used by the Issuer:
- `--name`, `--passcode`, `--alias`: Identify the Issuer's keystore and AID.
- `--registry-name`: Specifies the registry where the credential's status is managed.
- `--said`: The SAID of the specific credential instance to be revoked.
- `--time`: Timestamp for the revocation event.
```python
!kli vc revoke --name {issuer_keystore_name} --passcode {issuer_keystore_passcode} \
--alias {issuer_aid} \
--registry-name {issuer_registry_name} \
--said {credential_said} \
--time {time}
```
Waiting for TEL event witness receipts
Sending TEL events to witnesses
Now, if the Issuer lists their issued credentials again, the status will reflect the revocation:
```python
!kli vc list --name {issuer_keystore_name} --passcode {issuer_keystore_passcode} \
--alias {issuer_aid} \
--issued
```
Current issued credentials for issuer_aid (EH50FjEtKMgmuqmDK6ZePMlBuj486dONDgEQMOZHKPlp):
Credential #1: EGV-i9sG9H28DXpLN2PGxWq0tfqJjvMRcvM8HQsDWAvv
Type: EventPass
Status: Revoked β
Issued by EH50FjEtKMgmuqmDK6ZePMlBuj486dONDgEQMOZHKPlp
Issued on 2025-09-12T04:09:27.999812+00:00
### Sharing the revoked credential status with the Holder.
Revoking a credential is an important event that should be shared with verifiers. One way to share a revocation with a verifier is to share the revocation of a credential with the Holder. After the Holder receives that revoked credential status then it can re-present the revoked credential to a verifier so that the verifier may know the credential is revoked.
To accomplish this sharing of revocation state the issuer may perform another IPEX Grant of the credential following revocation. Then the Holder must again perform an IPEX Admit in order to learn of this revocation state.
βΉοΈ NOTE: Observers for Learning of Revocation State
Use of an Observer node to learn of an ACDC credential state is another way for a verifier to learn of the revocation state of a credential. While standalone observers are under development, a witness of a controller may be used to query for credential state using the following request format:
`HTTP GET` to a witness host on the `/query` endpoint with URL parameters like so:
- `/query?typ=tel®=EHrbPfpRLU9wpFXTzGY-LIo2FjMiljjEnt238eWHb7yZ&vcid=EO5y0jMXS5XKTYBKjCUPmNKPr1FWcWhtKwB2Go2ozvr0`
A full query to a witness would look like so:
- `https://wit1.testnet.gleif.org:5641/query?typ=tel®=EHrbPfpRLU9wpFXTzGY-LIo2FjMiljjEnt238eWHb7yZ&vcid=EO5y0jMXS5XKTYBKjCUPmNKPr1FWcWhtKwB2Go2ozvr0`
### Presenting a revoked credential
Now the holder can present the revoked credential to the verifier and the verifier can understand that the credential is revoke.
#### Step 1: Issuer Sends revocation status with IPEX
An issuer may directly inform a holder using another IPEX Grant about the revocation status of any credential issued from itself.
βΉοΈ NOTE: Observers for querying credential status
Waiting for an issuer to send credential revocation status is not the only way a holder can learn about whether or not a credential has been revoked.
Observers are another way a verifier or a holder can learn of the credential status, issued or revoked, from an issuer. Currently, as of June 16, 2025, observers are in an early phase in their development and are deployed as a feature on an issuer's witness. Eventually observers will be standalone components.
```python
# Issuer grants the now revoked credential
time = exec("kli time")
!kli ipex grant \
--name {issuer_keystore_name} --passcode {issuer_keystore_passcode} \
--alias {issuer_aid} \
--said {credential_said} \
--recipient {holder_aid} \
--time {time}
```
Sending message EJGct9b8z9nBEYJy6geDzeJpJL1zDBkpgQtfa17kFQBo to EDmofynK4DYITOe2ZN7BOSWigjqonV0MJurARWS_GqbG
... grant message sent
#### Step 2: Holder Admits IPEX Grant of revoked credential
Now the holder admits the IPEX Grant from the issuer of the recently revoked credential.
```python
# Holder polls and admits the revoked credential
# The pipe to "tail -n 1" makes sure to get the last IPEX Grant which will be the grant sharing the revoked credential
get_ipex_said=f"kli ipex list --name {holder_keystore_name} --passcode {holder_keystore_passcode}\
--alias {holder_aid} --poll --said | tail -n 1 | tr -d '' "
ipex_said=exec(get_ipex_said)
print(f"Found grant {ipex_said} for revocation")
time = exec("kli time")
!kli ipex admit \
--name {holder_keystore_name} \
--passcode {holder_keystore_passcode} \
--alias {holder_aid} \
--said {ipex_said} \
--time {time}
pr_continue()
```
Found grant EJGct9b8z9nBEYJy6geDzeJpJL1zDBkpgQtfa17kFQBo for revocation
Sending admit message to EH50FjEtKMgmuqmDK6ZePMlBuj486dONDgEQMOZHKPlp
... admit message sent
You can continue β
The holder now sees the credential status as "Revoked" in their credential list shown by `kli vc list.`
```python
!kli vc list --name {holder_keystore_name} --passcode {holder_keystore_passcode} \
--alias {holder_aid}
```
Current received credentials for holder_aid (EDmofynK4DYITOe2ZN7BOSWigjqonV0MJurARWS_GqbG):
Credential #1: EGV-i9sG9H28DXpLN2PGxWq0tfqJjvMRcvM8HQsDWAvv
Type: EventPass
Status: Revoked β
Issued by EH50FjEtKMgmuqmDK6ZePMlBuj486dONDgEQMOZHKPlp
Issued on 2025-09-12T04:09:27.999812+00:00
Now that this credential is revoked it can similarly be presented to the verifier from either the issuer or the holder so that the verifier can learn of the revocation state of the credential. This would be a push-style workflow.
Arguably a pull-style approach is better for verifiers where they query the issuer, or some other infrastructure, to learn of the revocation state of credentials, similar to checking certificate revocation lists ([CRLs](https://en.wikipedia.org/wiki/Certificate_revocation_list)) in the x509 TLS certificate model. Using **observer** infrastructure is the best way to accomplish pull-style querying for credential state.
As of the writing of this training the only functional observer implementation is combined with witnesses as describe above in the note to the [Sharing the revoked credential status with the Holder](#Sharing-the-revoked-credential-status-with-the-Holder.) section.
π SUMMARY
This notebook demonstrated the ACDC presentation and revocation flows:
- Prerequisites: We started with a Holder possessing an issued credential from an Issuer (established via the recap section).
- Verifier Setup: A Verifier established its KERI identity (AID).
- Connectivity: The Holder and Verifier exchanged and resolved OOBIs. The Verifier also resolved the credential's schema OOBI to enable validation.
- Presentation (IPEX):
- Holder initiated the presentation using kli ipex grant, sending the credential to the Verifier.
- Verifier polled its mailbox (kli ipex list --poll) to receive the presentation.
- Verifier accepted and validated the presentation using kli ipex admit. Validation included schema checks, issuer authentication (KEL), and registry status checks (TEL).
- Revocation:
- The original Issuer revoked the credential using kli vc revoke, updating the status in the credential registry's TEL.
- The Issuer then presented via IPEX Grant the revoked credential to the Holder.
- The Holder then received the revoked credential via IPEX Admit.
Observers were mentioned as pull-style infrastructure for verifiers, or anyone else, to learn of credential revocation state.
This completes the basic lifecycle demonstration: issuance (previous notebook), presentation, and revocation, all handled securely using KERI identities and the IPEX protocol.
[<- Prev (ACDC Issuance)](101_65_ACDC_Issuance.ipynb) | [Next (ACDC Edges and Rules) ->](101_75_ACDC_Edges_and_Rules.ipynb)
# Advanced ACDC Features: Edges, Edge Operators, and Rules
π― OBJECTIVE
Introduce the concept of Edges in ACDCs, explain the Edge operators (I2I, NI2I, DI2I) that define relationships between chained ACDCs, illustrate their use with conceptual scenarios, and briefly introduce Rules as a component for embedding legal language and conditions within the ACDC.
## Understanding Edges and Rules
Authentic Chained Data Containers (ACDCs) are not always standalone credentials. One of their features is the ability to be cryptographically linked to other ACDCs forming verifiable chains of information. These links, or chains, are defined in the `e` (edges) section of an ACDC's payload.
### ACDC Edges Section
The term "edge" is used because a chain of ACDCs is a graph data structure, specifically a directed acyclic graph, a DAG. In a graph there are nodes and edges and parent nodes are pointed to by child nodes. A child is any node that points to another node. A parent is a node that is pointed to by at least one other node. ACDCs in a chain are the nodes and the edges are references included within an ACDC that point to another ACDC.
The diagram below shows the following set of credentials and their edges:
- ACDC A is issued by identifier One to Two and has no edges.
- ACDC B is issued by identifier Two to Three and has an edge pointing to the A credential.
- ACDC C is issued by identifier Three to Four and has an edge pointing to the B credential.

As you can see edges allow the chaining of credentials to previously issued credentials which allows a verifier to traverse the entire chain of credentials and perform cryptographic verifications and business logic validations on each link in the chain as well as the entire chain at once. This sort of credential stacking allows advanced issuance and verification workflows that can work across trust boundaries and across legal jurisdictions.
Credential chaining in this way is one of the **most powerful** features of the KERI and ACDC protocol stack.
Building on this power, edge operators enable different kinds of credential chains, or subchains to be created for a variety of purposes.
### Edge Operators
As shown above, edges allow an ACDC to point to one or more ACDCs establishing a verifiable relationship between them. Edges define the kind of relationship between a child and a parent ACDC. This relationship is expressed as an **Edge Operators** which dictates the rules between the issuer and issuee of the connected ACDCs.
Understanding these operators is crucial for designing ACDC ecosystems that model real-world authority, delegation, and contextual relationships. In this notebook we will focus on three unary edge operators and when to use each:
* **I2I (Issuer-To-Issuee)** - the default; issuer of child ACDC is issuee of parent ACDC.
* **NI2I (Not-Issuer-To-Issuee)** - useful for untargeted ACDCs; issuer has no relationship to parent credential issuee.
* **DI2I (Delegated-Issuer-To-Issuee)** - issuer of child ACDC may be issuee or KEL delegate of issuee of parent ACDC.
We will explore what each operator signifies and provide scenarios to illustrate their practical application.
Edge operators provide the logic for validating the link between two ACDCs. They answer questions like: "Does the issuer of this ACDC need to be the subject of the ACDC it's pointing to?" or "Can the issuer be someone delegated by the subject of the linked ACDC?"
### ACDC Rules Section
Beyond these structural links, ACDCs also feature a dedicated `r` (rules) section. This section allows for the embedding of machine-readable logic, conditions, or even legal prose directly within the credential. While edges define how ACDCs are connected, rules can define additional constraints or behaviors associated with the ACDC itself or its relationships.
The rules section is currently somewhat simple and mostly used for specification of legal prose declaring the terms of use for a credential. The rest of this training explores the use of edge operators.
## I2I Operator: Issuer-To-Issuee
The Issuer to Issuee (I2I) operator is the **implicit default** for any credential created by the reference implementation, KERIpy, and does not need to be specified explicitly during ACDC creation. As shown in the diagram below, this operator means that the *issuer* of a *child* credential MUST be referenced as the *issuee* of the *parent* credential. This is a **strict** constraint.

The core idea behind an I2I edge is to represent a direct chain of authority. The issuer of the current ACDC (the "near" ACDC containing the edge) is asserting its claims based on its status as the issuee of the ACDC it's pointing to (the "far" ACDC).
I2I signifies: "My authority to issue this current ACDC may come from or otherwise involve the fact that I am the subject of the ACDC I am pointing to."
### I2I Scenario Examples
1. **Endorsement for Building Access:**
* **Scenario:** A company (ACME) issues a "Manager role" ACDC to an Employee. The Employee (now acting as an issuer by virtue of their managerial role) then issues an "Access" ACDC for a specific building to a sub-contractor they hired. The "Access" ACDC would have an I2I edge pointing back to the employee's "Manager role" ACDC.
| **Issuer** | **Issuee** | **Credential** | **I2I Edge** |
|------------|----------------|----------------|--------------|
| ACME | Employee | Manager role | N/A |
| Employee | Sub-contractor | Access | Manager role |
* **Diagram representation:**
```mermaid
graph LR
subgraph "Credential Chain"
A[Access ACDC
Issuer: Employee
Issuee: Sub-contractor]
B[Manager Role ACDC
Issuer: ACME
Issuee: Employee]
A -->|I2I Edge| B
end
```
* **Significance:** The I2I edge ensures the Manager issuing access is verifiably the same member to whom ACME conferred managerial status.
In this case the door access management device acting as the **verifier** would check to ensure that the Access ACDC was issued with an I2I edge and that the *issuer* of the Access ACDC was indeed the *issuee* of the Manager role ACDC.
2. **Membership Level Endorsement for Event Access:**
* **Scenario:** An Organization issues a "Gold Member" ACDC to an member. This member (Gold Member) then wants to bring a guest to an exclusive event. The organization's policies (which are embedded in the `r` section of the "Gold Member" ACDC) allow Gold Members to issue "Guest Pass" ACDCs. The "Guest Pass" issued by the Gold Member would have an I2I edge pointing to their "Gold Member" ACDC.
| **Issuer** | **Isuee** | **Credential** | **I2I Edge** |
|--------------|-----------|----------------|--------------|
| Organization | Member | Gold Member | N/A |
| Member | Guest | Guest Pass | Gold Member |
* **Diagram representation:**
```mermaid
graph LR
subgraph "Credential Chain"
A[Gold Member ACDC
Issuer: Organization
Issuee: Member]
B[Guest Pass ACDC
Issuer: Member
Issuee: Guest]
B -->|I2I Edge| A
end
```
* **Significance:** The validity of the guest pass relies on the issuer being a verified Gold Member, as established by the I2I link, and governed by additional rules specified in the r section (e.g., limit on number of guest passes).
In this case the door access point verifier would verify that the edge type was I2I and that the Gold Member was both the *issuer* of the Guest Pass ACDC and the *issuee* of the Gold Member ACDC.
### Use Cases for I2I
1. **Delegation of Authority with Credentials:** When an entity that was the subject (issuee) of a credential (e.g., "Manager Role") now needs to issue a subsequent credential by leveraging that conferred authority (e.g., "Project Approval" issued by the manager).
2. **Endorsement:** If one credential's authority directly enables the issuance of another by the same entity acting in a new capacity.
3. **Hierarchical Relationships:** When representing a clear hierarchy where an entity's position (as an issuee of a credential defining that position) allows them to issue credentials further down the chain.
4. **Sequential Processes:** In workflows where an entity receives a credential (making them an issuee) and then, as a next step in the process, issues another credential related to that item. Supply chain or multi-step workflows are areas where this would apply.
5. **Default Expectation for Targeted ACDCs:** If the far node (the ACDC being pointed to) is a "Targeted ACDC" (i.e., has an issuee), the I2I operator is the default assumption unless otherwise specified. This implies a natural flow where the issuee of one ACDC becomes the issuer of the next related ACDC.
## NI2I Operator: Not-Issuer-To-Issuee
The NI2I operator is a **permissive** edge operator that allows any and all identifiers to chain a child ACDC onto a parent ACDC regardless of whether the issuer of the child ACDC is related to the parent ACDC in any way. The diagram below shows that Identifier Three can chain child ACDC B to parent ACDC A even though Three is not the issuee of parent ACDC A. Identifier Two is the recipient of both parent ACDC A and child ACDC B.

The purpose of the NI2I edge is to reference, associate, or link to another ACDC for context, support, or related information, where there isn't a direct delegation of authority or a requirement for the issuer of the current ACDC to be the subject of the referenced ACDC.
NI2I means that this ACDC I am pointing to provides relevant context, support, or related information for my current ACDC, but my authority to issue this current ACDC does not stem from my being the subject of that linked ACDC.
This sort of relationship is more decoupled than I2I and is more applicable to scenarios where the issuer of the parent credential is not necessarily related to or affiliated with the issuer of the child credential or does not want a strict relationship between the parent and child credentials.
### NI2I Scenario Examples
1. **Linking to an External Training Course Completion Certificate:**
* **Scenario:** A Company issues an "Skill Certified" ACDC to an employee after they complete an internal assessment. The employee also completed an external, third-party training course relevant to this skill. The "Skill Certified" ACDC could have an NI2I edge pointing to the "Course Completion" ACDC issued by the external Training Provider.
| **Issuer** | **Issuee** | **Credential** | **NI2I Edge** |
|-------------------|-----------|-------------------|-------------------|
| Training Provider | Employee | Course Completion | N/A |
| Company | Employee | Skill Certified | Course Completion |
* **Diagram representation:**
```mermaid
graph LR
subgraph "Credential Reference"
A[Course Completion ACDC
Issuer: Training Provider
Issuee: Employee]
B[Skill Certified ACDC
Issuer: Company
Issuee: Employee]
B -.->|NI2I Edge
references for context| A
end
```
* **Significance:** The company acknowledges the external training as supporting evidence. The `r` (rules) section of the "Skill Certified" ACDC could specify how this external certification contributes to the overall skill validation (e.g., "External certification X fulfills requirement Y").
2. **Proof of Insurance for a Rental Agreement:**
* **Scenario:** A Car Rental Agency issues a "Rental Agreement" ACDC to a Customer. The customer is required to have valid car insurance. The "Rental Agreement" ACDC could have an NI2I edge pointing to the Customer's "Proof of Insurance" ACDC, which was issued by an Insurance Company.
| **Issuer** | **Issuee** | **Credential** | **NI2I Edge** |
|-------------------|-----------|--------------------|--------------------|
| Insurance Company | Customer | Proof of Insurance | N/A |
| Rental Agency | Customer | Rental Agreement | Proof of Insurance |
* **Diagram representation:**
```mermaid
graph LR
subgraph "Credential Reference"
A[Proof of Insurance ACDC
Issuer: Insurance Company
Issuee: Customer]
B[Rental Agreement ACDC
Issuer: Rental Agency
Issuee: Customer]
B -.->|NI2I Edge
requires as evidence| A
end
```
* **Significance:** The rental agreement relies on the existence of an insurance policy. The `r` section of the "Rental Agreement" ACDC would likely contain critical rules defining the terms of the rental, insurance coverage requirements (e.g., minimum liability), and consequences of non-compliance, which are legally binding.
### Use Cases for NI2I
1. **Referencing External Information:** When an ACDC needs to point to an ACDC that was issued by a third party and not specifically to the issuer of the current ACDC. The `r` (rules) section of the referencing ACDC might contain specific clauses on how that external information applies.
2. **Providing Supporting Evidence from Third Parties:** Linking to credentials issued by other independent parties that support a claim in the current ACDC.
3. **Associating Related but Independent Credentials:** When linking credentials that are related but don't follow a direct chain of delegated authority from the same entity.
4. **Linking to Untargeted ACDCs:** If the ACDC being pointed to is an "Untargeted ACDC" (i.e., does not have a specific issuee, representing a thing, a concept, or a general statement), then NI2I is appropriate. The issuer of the current ACDC isn't the "issuee" of a general concept.
5. **Relaxing the I2I Constraint:** When you explicitly want to state that the issuer of the current ACDC is not necessarily the issuee of the linked ACDC, even if the linked ACDC is targeted.
## DI2I Operator: Delegated-Issuer-To-Issuee
The idea behind a DI2I edge is to allow delegates of the child ACDC issuer's parent AID to issue the child ACDC. This allows for additional security, flexibility, and scaling of ACDC issuance for the recipient of a targeted credential since the AID controller of the recipient can choose to issue further chained ACDC from delegates. Issuing credentials from delegates allows for a number of benefits including the single responsibility pattern, increased security due to less exposure of the parent AID's root signing keys, and scalability in the sense that many delegates can be authorized to issue ACDCs to avoid cluttering the parent AID's KEL.
In DI2I the issuer of the child ACDC is either the root AID or a delegated AID of the root AID that is specified as the issuee of the parent ACDC. The same **strict** rule from I2I applies to the DI2I operator with additional flexibility coming from the fact that a delegate of the issuee of the parent ACDC may issue the child ACDC.
DI2I signifies: My authority to issue this current ACDC comes from the fact that I am EITHER the subject of the ACDC I am pointing to OR I am a formally recognized delegate of that subject.
### DI2I Scenario Example
1. **Supply Chain: Quality Control Release by Delegated QC Supervisor**
* **Scenario:** A Manufacturing Conglomerate issues a "Plant Operations Authority" ACDC to the General Manager (GM) of Plant A. This grants the GM overall responsibility for Plant A's output.
* The GM of Plant A delegates the authority for final quality control (QC) release of specific product lines (e.g., "Widget Model X") to the QC Supervisor, via KERI AID delegation.
* QC Supervisor's team completes QC checks on a batch of Widget Model X, and the QC Supervisor issues a "Batch Quality Approved" ACDC.
| **Issuer** | **Issuee** | **Credential** | **DI2I Edge** |
|----------------------------|-----------|----------------------------|----------------------------|
| Manufacturing Conglomerate | GM | Plant Operations Authority | N/A |
| QC Supervisor | Batch | Batch Quality Approved | Plant Operations Authority |
* **Diagram representation:**
```mermaid
graph TD
subgraph "Credential Chain (ACDCs)"
A["Plant Operations Authority ACDC
Issuer: Manufacturing Conglomerate
Issuee: GM"]
B["Batch Quality Approved ACDC
Issuer:QC Supervisor
Issuee: Batch Recipient"]
B -- "DI2I Edge
(Points to Authority ACDC)" --> A
end
subgraph "Identifier Relationship"
GM_AID["General Manager's AID
(GM_PlantA_AID)"]
QC_AID["QC Supervisor's AID
(QC_Supervisor_AID)"]
GM_AID -- "KERI Delegation Event" --> QC_AID
end
style GM_AID fill:#cde4ff,stroke:#333
style QC_AID fill:#cde4ff,stroke:#333
```
* **Significance (Why DI2I?):** QC Supervisor's authority is a verifiable delegation from the GM. The `r` (rules) section of the "Batch Quality Approved" ACDC might further specify the exact QC standards that must be met for the approval to be valid, linking the delegated action to concrete operational requirements.
### Use Cases for DI2I
1. **Delegation:** When authority is passed down through an intermediary. The entity issuing the current credential isn't the direct subject of the credential conferring original authority but is operating under a valid delegation from that subject.
2. **Flexible Hierarchical Authority:** In complex organizations, the person signing off on something (issuing an ACDC) might not be the person or identifier who holds the primary credential for that domain but is acting on their behalf through a formal delegation chain verified via KERI's AID delegation mechanisms.
3. **When the Issuer is Part of a Group Authorized by the Issuee:** The issuer is one of several individuals or entities who have been delegated authority by the issuee of the referenced ACDC.
4. **Requires a Targeted Far Node:** Like I2I, the ACDC being pointed to (the "far node") by a DI2I edge MUST be a "Targeted ACDC" (i.e., it must have an issuee).
5. **Expands on I2I:** It is a *superset* of I2I. If an I2I relationship is valid, a DI2I relationship would also be valid. However, DI2I also allows for valid issuers who are delegates of the far node's issuee
π SUMMARY
ACDC Edges (
e section) link ACDCs, with Edge Operators defining relationship rules. The
r (rules) section adds another layer for embedding machine-readable logic or legal prose.
- I2I (Issuer-To-Issuee): Use when the current ACDC's issuer's authority stems directly from being the subject (issuee) of the linked ACDC. Signifies direct authority. Example: A manager (issuee of "Manager" ACDC) issues project access.
- NI2I (Not-Issuer-To-Issuee): Use for referencing contextual or supporting ACDCs where the current issuer's authority doesn't derive from being the subject of the linked ACDC. Example: An "Employee Skill" ACDC linking to an external "Training Certificate" ACDC.
- DI2I (Delegated-Issuer-To-Issuee): Use when the current ACDC's issuer is either the subject of the linked ACDC or a formally recognized delegate of that subject. Allows for flexible, multi-step delegation. Example: A QC Supervisor (delegate of Plant GM) issues a "Batch Approved" ACDC pointing to the GM's "Plant Authority" ACDC.
[<- Prev (ACDC Presentation and Revocation)](101_70_ACDC_Presentation_and_Revocation.ipynb) | [Next (ACDC Chained Credentials I2I) ->](101_80_ACDC_Chained_Credentials_I2I.ipynb)
# ACDC Issuance with KLI: Issuer-To-Issuee
π― OBJECTIVE
Demonstrate how to issue chained Authentic Chained Data Containers (ACDCs) using an Issuer-To-Issuee (I2I) edge relationship with the KERI Command Line Interface (KLI).
It also illustrates how to embed a simple rule within an ACDC. We will implement the "Endorsement for Building Access" scenario.
## Scenario Recap: Endorsement for Building Access
Remember, the I2I operator enforces successive parent-child relationships across a chain of credential holders where the parent issuer of the current credential must be the child of the prior credential, if it has a parent. This is a strict constraint. Who enforces this strict constraint? Verifiers do, and usually within a set of rules for a credential ecosystem similar to how the vLEI Ecosystem Governance Framework (EGF) specifies the kind of credentials and their relationships to one another.
This notebook focuses on the practical KLI commands for implementing an `I2I` chained credential scenario. For a detailed theoretical explanation of ACDC Edges, Edge Operators, and Rules, please refer to the "[Advanced ACDC Features: Edges, Edge Operators, and Rules](101_75_ACDC_Edges_and_Rules.ipynb)" notebook.
To summarize this scenario:
- **ACME Corp** issues a "Role Credential" to an Employee.
- The **Employee**, by virtue of their "Role Credential", issues an "Access Credential" to a **Sub-contractor**.
- The **Access Credential** contains an `I2I` edge linking back to the Employee's "Role Credential", signifying that the Employee's authority to grant access is derived from their managerial role.
- The **Access Credential** will also include a simple textual rule regarding its usage policy.
## Initial Setup: Keystores, AIDs, Registries, and OOBIs
As usual, it is necessary to set up our participants:
- Acme Corporation (`acme_aid`): The initial, or root, authority in this scenario, responsible for issuing the top level Role Credential.
- Employee (`employee_aid`): This participant will first receive the Role Credential from Acme and subsequently issue the Access Credential.
- Sub-contractor (`subcontractor_aid`): The recipient of the Access Credential.
For each participant:
- Initialize their respective keystores.
- Incept their Autonomic Identifiers (AIDs). These AIDs will be configured as transferable and will utilize the default witness setup from `keystore_init_config.json`.
- Establish OOBI connections. This involves generating OOBIs for each AID and resolving them to ensure all necessary participants (Acme-Employee, Employee-Sub-contractor) can securely discover each other.
For ACME and the Employee:
- Incept a credential registry
```python
# Imports and Utility functions
from scripts.utils import exec, clear_keri, pr_title, pr_message, pr_continue
from scripts.saidify import get_schema_said
import json, os
clear_keri()
# ACME Keystore and AID
acme_keystore_name = "acme_ks"
acme_salt = exec("kli salt")
acme_aid_alias = "acme"
acme_registry_name = "acme_mgr_registry"
# Employee Keystore and AID
employee_keystore_name = "employee_ks"
employee_salt = exec("kli salt")
employee_aid_alias = "employee"
employee_registry_name = "employee_access_registry"
# Sub-contractor Keystore and AID
subcontractor_keystore_name = "subcontractor_ks"
subcontractor_salt = exec("kli salt")
subcontractor_aid_alias = "subcontractor"
pr_title("Initializing keystores")
!kli init --name {acme_keystore_name} \
--nopasscode \
--salt {acme_salt} \
--config-dir ./config \
--config-file keystore_init_config.json
!kli init --name {employee_keystore_name} \
--nopasscode \
--salt {employee_salt} \
--config-dir ./config \
--config-file keystore_init_config.json
!kli init --name {subcontractor_keystore_name} \
--nopasscode \
--salt {subcontractor_salt} \
--config-dir ./config \
--config-file keystore_init_config.json
pr_title("Initializing AIDs")
!kli incept --name {acme_keystore_name} \
--alias {acme_aid_alias} \
--file ./config/aid_inception_config.json # Uses witnesses and transferable settings
!kli incept --name {employee_keystore_name} \
--alias {employee_aid_alias} \
--file ./config/aid_inception_config.json
!kli incept --name {subcontractor_keystore_name} \
--alias {subcontractor_aid_alias} \
--file ./config/aid_inception_config.json
pr_title("Initializing Credential Registries")
!kli vc registry incept --name {acme_keystore_name} \
--alias {acme_aid_alias} \
--registry-name {acme_registry_name}
!kli vc registry incept --name {employee_keystore_name} \
--alias {employee_aid_alias} \
--registry-name {employee_registry_name}
acme_aid_prefix = exec(f"kli aid --name {acme_keystore_name} --alias {acme_aid_alias}")
employee_aid_prefix = exec(f"kli aid --name {employee_keystore_name} --alias {employee_aid_alias}")
subcontractor_aid_prefix = exec(f"kli aid --name {subcontractor_keystore_name} --alias {subcontractor_aid_alias}")
pr_message(f"ACME AID: {acme_aid_prefix}")
pr_message(f"Employee AID: {employee_aid_prefix}")
pr_message(f"Sub-contractor AID: {subcontractor_aid_prefix}")
pr_title("Generating and resolving OOBIs")
# ACME and Employee OOBI Exchange
acme_oobi = exec(f"kli oobi generate --name {acme_keystore_name} --alias {acme_aid_alias} --role witness")
employee_oobi = exec(f"kli oobi generate --name {employee_keystore_name} --alias {employee_aid_alias} --role witness")
!kli oobi resolve --name {acme_keystore_name} \
--oobi-alias {employee_aid_alias} \
--oobi {employee_oobi}
!kli oobi resolve --name {employee_keystore_name} \
--oobi-alias {acme_aid_alias} \
--oobi {acme_oobi}
# Employee and Sub-contractor OOBI Exchange
subcontractor_oobi = exec(f"kli oobi generate --name {subcontractor_keystore_name} --alias {subcontractor_aid_alias} --role witness")
!kli oobi resolve --name {employee_keystore_name} \
--oobi-alias {subcontractor_aid_alias} \
--oobi {subcontractor_oobi}
!kli oobi resolve --name {subcontractor_keystore_name} \
--oobi-alias {employee_aid_alias} \
--oobi {employee_oobi}
pr_message("OOBI connections established.")
pr_continue()
```
Proceeding with deletion of '/usr/local/var/keri/' without confirmation.
β
Successfully removed: /usr/local/var/keri/
Initializing keystores
KERI Keystore created at: /usr/local/var/keri/ks/acme_ks
KERI Database created at: /usr/local/var/keri/db/acme_ks
KERI Credential Store created at: /usr/local/var/keri/reg/acme_ks
Loading 3 OOBIs...
http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness succeeded
http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wes&tag=witness succeeded
http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wil&tag=witness succeeded
KERI Keystore created at: /usr/local/var/keri/ks/employee_ks
KERI Database created at: /usr/local/var/keri/db/employee_ks
KERI Credential Store created at: /usr/local/var/keri/reg/employee_ks
Loading 3 OOBIs...
http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness succeeded
http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wes&tag=witness succeeded
http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wil&tag=witness succeeded
KERI Keystore created at: /usr/local/var/keri/ks/subcontractor_ks
KERI Database created at: /usr/local/var/keri/db/subcontractor_ks
KERI Credential Store created at: /usr/local/var/keri/reg/subcontractor_ks
Loading 3 OOBIs...
http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness succeeded
http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wes&tag=witness succeeded
http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wil&tag=witness succeeded
Initializing AIDs
Waiting for witness receipts...
Prefix EPRQCgop2CyHCC8tabtl9iJOw1ryr2eXofO6IF7NQtQ-
Public key 1: DH8BuO089xEDuARHaXAq4V9Uqv2w7Pz4wBnb4AM9ojSa
Waiting for witness receipts...
Prefix EGKtnumYK9aJ0aKtX6WS8TPcVt2W8tYrlTbFwvX9T-IB
Public key 1: DC3EVHqOWepm_r3wN33_IOlrNKsAuza6o0HLjMJdpdYn
Waiting for witness receipts...
Prefix ECfW_ag06odaG2HgGqggnP697j17ZU7nM3dRb4ixpc9B
Public key 1: DA9-YlRY7pXgaaKlhkppFTAZ88r4v68ooNc8s-FuW5hc
Initializing Credential Registries
Waiting for TEL event witness receipts
Sending TEL events to witnesses
Registry: acme_mgr_registry(EONNB-EvoWMm7aXTQpFDsN7hGCk-ZvRQEzdd0AZxox2r)
created for Identifier Prefix: EPRQCgop2CyHCC8tabtl9iJOw1ryr2eXofO6IF7NQtQ-
Waiting for TEL event witness receipts
Sending TEL events to witnesses
Registry: employee_access_registry(ECBgjw1OO2ZVr4U0_513HpnyRNtXmCBQ-UIY-ttC0n1Y)
created for Identifier Prefix: EGKtnumYK9aJ0aKtX6WS8TPcVt2W8tYrlTbFwvX9T-IB
ACME AID: EPRQCgop2CyHCC8tabtl9iJOw1ryr2eXofO6IF7NQtQ-
Employee AID: EGKtnumYK9aJ0aKtX6WS8TPcVt2W8tYrlTbFwvX9T-IB
Sub-contractor AID: ECfW_ag06odaG2HgGqggnP697j17ZU7nM3dRb4ixpc9B
Generating and resolving OOBIs
http://witness-demo:5642/oobi/EGKtnumYK9aJ0aKtX6WS8TPcVt2W8tYrlTbFwvX9T-IB/witness resolved
http://witness-demo:5642/oobi/EPRQCgop2CyHCC8tabtl9iJOw1ryr2eXofO6IF7NQtQ-/witness resolved
http://witness-demo:5642/oobi/ECfW_ag06odaG2HgGqggnP697j17ZU7nM3dRb4ixpc9B/witness resolved
http://witness-demo:5642/oobi/EGKtnumYK9aJ0aKtX6WS8TPcVt2W8tYrlTbFwvX9T-IB/witness resolved
OOBI connections established.
You can continue β
## Schema Definitions
We need two ACDC schemas as shown below. The non-metadata attributes are also shown below:
- Role Schema (`role_schema.json`): For the credential ACME issues to the Employee.
- Attributes
- `roleTitle`
- `department`
- Access Schema (`access_schema.json`): For the credential the Employee issues to the Sub-contractor. This schema will include definitions for an `e` (edges) section to specify the I2I link and an `r` (rules) section.
- Attributes
- `buildingId`
- `accessLevel`
- Edges
- `manager_endorsement` (points to Role Schema ACDC)
βΉοΈ NOTE
For this notebook,the schemas have been SAIDified and made available on a schema server (a simple webserver hosting schema files as JSON). The SAIDification process was covered in the "SAIDifying ACDC Schemas" notebook.
### Role Schema
This schema defines the structure of the "Role Credential." It has a structure that is rather similar to the other schemas presented so far during the training:
- Filename: `role_schema.json` (content shown SAIDified)
```python
role_schema_path = "config/schemas/role_schema.json"
pr_title(f"Schema: {role_schema_path}")
role_schema_said = get_schema_said(role_schema_path)
pr_message(f"Schema SAID: {role_schema_said}")
pr_message(f"Retrieving Role Schema from Server:")
!curl -s http://vlei-server:7723/oobi/{role_schema_said} | jq
pr_continue()
```
Schema: config/schemas/role_schema.json
Schema SAID: ENWatfUaeryBqvGnG7VdILVcqk84_eoxmiaJYguJXaRw
Retrieving Role Schema from Server:
{
"$id": "ENWatfUaeryBqvGnG7VdILVcqk84_eoxmiaJYguJXaRw",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "RoleCredential",
"description": "Credential signifying a role within an organization.",
"type": "object",
"credentialType": "RoleCredential",
"version": "1.0.0",
"properties": {
"v": {
"description": "Credential Version String",
"type": "string"
},
"d": {
"description": "Credential SAID",
"type": "string"
},
"u": {
"description": "One time use nonce",
"type": "string"
},
"i": {
"description": "Issuer AID",
"type": "string"
},
"ri": {
"description": "Registry SAID",
"type": "string"
},
"s": {
"description": "Schema SAID",
"type": "string"
},
"a": {
"oneOf": [
{
"description": "Attributes block SAID",
"type": "string"
},
{
"$id": "EFmgKWjhXaH2MYUmlNy5-t8Y6SHZ0InHriOkyAnI4777",
"description": "Attributes block",
"type": "object",
"properties": {
"d": {
"description": "Attributes data SAID",
"type": "string"
},
"i": {
"description": "Issuee AID (Employee's AID)",
"type": "string"
},
"dt": {
"description": "Issuance date time",
"type": "string",
"format": "date-time"
},
"roleTitle": {
"description": "The title of the role.",
"type": "string"
},
"department": {
"description": "The department the employee belongs to.",
"type": "string"
}
},
"additionalProperties": false,
"required": [
"d",
"i",
"dt",
"roleTitle",
"department"
]
}
]
}
},
"additionalProperties": false,
"required": [
"v",
"d",
"i",
"ri",
"s",
"a"
]
}
You can continue β
### Access Schema
This schema defines the "Access Credential". It includes an `e` (edges) section for the `I2I` link to the Role Credential and an `r` (rules) section for a usage policy.
Filename: `access_schema.json` (content shown SAIDified)
```python
access_schema_path = "config/schemas/access_schema.json"
pr_title(f"Schema: {access_schema_path}")
access_schema_said = get_schema_said(access_schema_path)
pr_message(f"Schema SAID: {access_schema_said}")
pr_message(f"Retrieving Access Schema from Server:")
!curl -s http://vlei-server:7723/oobi/{access_schema_said} | jq
pr_continue()
```
Schema: config/schemas/access_schema.json
Schema SAID: EF2zX3g5YDyHMSjgsK4OayZMFmLRMxcAJfW363JhBOfD
Retrieving Access Schema from Server:
{
"$id": "EF2zX3g5YDyHMSjgsK4OayZMFmLRMxcAJfW363JhBOfD",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AccessCredential",
"description": "Credential granting access to a specific building or area, endorsed by a manager.",
"type": "object",
"credentialType": "AccessCredential",
"version": "1.0.0",
"properties": {
"v": {
"description": "Credential Version String",
"type": "string"
},
"d": {
"description": "Credential SAID",
"type": "string"
},
"u": {
"description": "One time use nonce",
"type": "string"
},
"i": {
"description": "Issuer AID (Employee's AID)",
"type": "string"
},
"ri": {
"description": "Registry SAID",
"type": "string"
},
"s": {
"description": "Schema SAID",
"type": "string"
},
"a": {
"oneOf": [
{
"description": "Attributes block SAID",
"type": "string"
},
{
"$id": "EOxa7LAD2BoA9tk9n0CW4zH7nF91DP1g_Pjz1wC_FuNw",
"description": "Attributes block",
"type": "object",
"properties": {
"d": {
"description": "Attributes data SAID",
"type": "string"
},
"i": {
"description": "Issuee AID (Sub-contractor's AID)",
"type": "string"
},
"dt": {
"description": "Issuance date time",
"type": "string",
"format": "date-time"
},
"buildingId": {
"description": "Identifier for the building access is granted to.",
"type": "string"
},
"accessLevel": {
"description": "Level of access granted.",
"type": "string"
}
},
"additionalProperties": false,
"required": [
"d",
"i",
"dt",
"buildingId",
"accessLevel"
]
}
]
},
"e": {
"oneOf": [
{
"description": "Edges block SAID",
"type": "string"
},
{
"$id": "EI8RvTM23u-pQDK-KpDUBWOKbiOW8fpnzktVVBCLy55N",
"description": "Edges block",
"type": "object",
"properties": {
"d": {
"description": "Edges block SAID",
"type": "string"
},
"manager_endorsement": {
"description": "Link to the Manager Credential that endorses this access",
"type": "object",
"properties": {
"n": {
"description": "Issuer credential SAID",
"type": "string"
},
"s": {
"description": "SAID of required schema of the credential pointed to by this node",
"type": "string",
"const": "ENWatfUaeryBqvGnG7VdILVcqk84_eoxmiaJYguJXaRw"
},
"o": {
"description": "Operator indicating this node is the issuer",
"type": "string",
"const": "I2I"
}
},
"additionalProperties": false,
"required": [
"n",
"s",
"o"
]
}
},
"additionalProperties": false,
"required": [
"d",
"manager_endorsement"
]
}
]
},
"r": {
"oneOf": [
{
"description": "Rules block SAID",
"type": "string"
},
{
"$id": "EKDmqq14KgthMAV23sCbzgdFFjT-v9x01toUsyfyi2uU",
"description": "Rules governing the use of this access credential.",
"type": "object",
"properties": {
"d": {
"description": "Rules block SAID",
"type": "string"
},
"usageDisclaimer": {
"description": "Usage Disclaimer",
"type": "object",
"properties": {
"l": {
"description": "Associated legal language",
"type": "string",
"const": "This mock credential grants no actual access. For illustrative use only."
}
}
}
},
"additionalProperties": false,
"required": [
"d",
"usageDisclaimer"
]
}
]
}
},
"additionalProperties": false,
"required": [
"v",
"d",
"i",
"ri",
"s",
"a",
"e",
"r"
]
}
You can continue β
## Resolving Schema OOBIs
All parties need to resolve the OOBIs for these schemas from the schema server to be able to either issue, receive, present, or receive presentations of credentials using these schemas.
```python
pr_title("Resolving schema OOBIs")
role_schema_oobi = f"http://vlei-server:7723/oobi/{role_schema_said}"
access_schema_oobi = f"http://vlei-server:7723/oobi/{access_schema_said}"
# ACME Corp
!kli oobi resolve --name {acme_keystore_name} \
--oobi-alias "role_schema" --oobi {role_schema_oobi}
!kli oobi resolve --name {acme_keystore_name} \
--oobi-alias "access_schema" --oobi {access_schema_oobi}
# Employee
!kli oobi resolve --name {employee_keystore_name} \
--oobi-alias "role_schema" --oobi {role_schema_oobi}
!kli oobi resolve --name {employee_keystore_name} \
--oobi-alias "access_schema" --oobi {access_schema_oobi}
# Sub-contractor
!kli oobi resolve --name {subcontractor_keystore_name} \
--oobi-alias "role_schema" --oobi {role_schema_oobi}
!kli oobi resolve --name {subcontractor_keystore_name} \
--oobi-alias "access_schema" --oobi {access_schema_oobi}
pr_message("Schema OOBIs resolved.")
pr_continue()
```
Resolving schema OOBIs
http://vlei-server:7723/oobi/ENWatfUaeryBqvGnG7VdILVcqk84_eoxmiaJYguJXaRw resolved
http://vlei-server:7723/oobi/EF2zX3g5YDyHMSjgsK4OayZMFmLRMxcAJfW363JhBOfD resolved
http://vlei-server:7723/oobi/ENWatfUaeryBqvGnG7VdILVcqk84_eoxmiaJYguJXaRw resolved
http://vlei-server:7723/oobi/EF2zX3g5YDyHMSjgsK4OayZMFmLRMxcAJfW363JhBOfD resolved
http://vlei-server:7723/oobi/ENWatfUaeryBqvGnG7VdILVcqk84_eoxmiaJYguJXaRw resolved
http://vlei-server:7723/oobi/EF2zX3g5YDyHMSjgsK4OayZMFmLRMxcAJfW363JhBOfD resolved
Schema OOBIs resolved.
You can continue β
## Issuing credentials
Now that the setup is complete and the schemas are available, its necessary to create the credential chain.
### Step 1: Role Credential Issuance
The Keystores, AIDs, and Credential Registry for ACME Corporation were created during the initial setup. The next step is to create the credential that grants the "Engineering Manager" role to the employee.
**ACME Creates Role Credential Data**
Create a JSON file `role_cred_data.json` with the attributes for this specific credential
```python
pr_title("Creating role credential data")
!echo '{ \
"roleTitle": "Engineering Manager", \
"department": "Technology Innovations" \
}' > config/credential_data/role_cred_data.json
!cat config/credential_data/role_cred_data.json | jq
pr_continue()
```
Creating role credential data
{
"roleTitle": "Engineering Manager",
"department": "Technology Innovations"
}
You can continue β
**ACME Issues Role Credential to Employee**
Now that the credential data is in the file the next step is to create the credential with `!kli vc create`
```python
pr_title("Creating Role credential")
issue_time_acme = exec("kli time")
!kli vc create --name {acme_keystore_name} \
--alias {acme_aid_alias} \
--registry-name {acme_registry_name} \
--schema {role_schema_said} \
--recipient {employee_aid_prefix} \
--data "@./config/credential_data/role_cred_data.json" \
--time {issue_time_acme}
role_credential_said = exec(f"kli vc list --name {acme_keystore_name} --alias {acme_aid_alias} --issued --said --schema {role_schema_said}")
pr_message(f"Role Credential SAID: {role_credential_said}")
pr_continue()
```
Creating Role credential
Waiting for TEL event witness receipts
Sending TEL events to witnesses
EEy7jb0V3U8MucFutlRulbQ9eIYWNDrm7Jsbuzg144aZ has been created.
Role Credential SAID: EEy7jb0V3U8MucFutlRulbQ9eIYWNDrm7Jsbuzg144aZ
You can continue β
**IPEX Transfer: ACME Grants, Engineering Manager Employee Admits Role Credential**
Next, perform the IPEX transfer as done in previous ACDC issuance examples. Afterwards, the employee will have the role credential.
```python
pr_title("Transferring credential (ipex grant)")
time = exec("kli time")
!kli ipex grant --name {acme_keystore_name} \
--alias {acme_aid_alias} \
--said {role_credential_said} \
--recipient {employee_aid_prefix} \
--time {time}
pr_title("Admitting credential (ipex admit)")
# Employee polls for the grant and admits it
employee_grant_msg_said = exec(f"kli ipex list --name {employee_keystore_name} --alias {employee_aid_alias} --poll --said")
time = exec("kli time")
!kli ipex admit --name {employee_keystore_name} \
--alias {employee_aid_alias} \
--said {employee_grant_msg_said} \
--time {time}
# Employee lists the received credential
pr_message("\nEngineering Manager Employee received Role Credential:")
!kli vc list --name {employee_keystore_name} \
--alias {employee_aid_alias} \
--verbose
pr_continue()
```
Transferring credential (ipex grant)
Sending message ECQY9YW7oFmRqOi2AJ1Mw69xtuBLTQc4-W37jbvKStgQ to EGKtnumYK9aJ0aKtX6WS8TPcVt2W8tYrlTbFwvX9T-IB
... grant message sent
Admitting credential (ipex admit)
Sending admit message to EPRQCgop2CyHCC8tabtl9iJOw1ryr2eXofO6IF7NQtQ-
... admit message sent
Engineering Manager Employee received Role Credential:
Current received credentials for employee (EGKtnumYK9aJ0aKtX6WS8TPcVt2W8tYrlTbFwvX9T-IB):
Credential #1: EEy7jb0V3U8MucFutlRulbQ9eIYWNDrm7Jsbuzg144aZ
Type: RoleCredential
Status: Issued β
Issued by EPRQCgop2CyHCC8tabtl9iJOw1ryr2eXofO6IF7NQtQ-
Issued on 2025-09-12T04:10:33.313202+00:00
Full Credential:
{
"v": "ACDC10JSON0001c2_",
"d": "EEy7jb0V3U8MucFutlRulbQ9eIYWNDrm7Jsbuzg144aZ",
"i": "EPRQCgop2CyHCC8tabtl9iJOw1ryr2eXofO6IF7NQtQ-",
"ri": "EONNB-EvoWMm7aXTQpFDsN7hGCk-ZvRQEzdd0AZxox2r",
"s": "ENWatfUaeryBqvGnG7VdILVcqk84_eoxmiaJYguJXaRw",
"a": {
"d": "EJZ5Ab5kkrz-hGU7-e5K7mV_LCpEFn1e1t7aNttCZwWu",
"i": "EGKtnumYK9aJ0aKtX6WS8TPcVt2W8tYrlTbFwvX9T-IB",
"dt": "2025-09-12T04:10:33.313202+00:00",
"roleTitle": "Engineering Manager",
"department": "Technology Innovations"
}
}
You can continue β
### Step 2: Access Credential Data properties - edge, rules, and attributes
The Employee, now holding the "Role Credential", issues the "Access Credential" to the Sub-contractor. This new credential will link to the Role Credential via an `I2I` edge and include a "Usage Disclaimer" rule. For this it is necessary to create JSON files for the attributes (`access_cred_data.json`), the edge (`access_cred_edge.json`), and the rule (`access_cred_rule.json`). The attributes, edges, and rules properties are displayed below.
#### Attributes Data
The attributes of the Role Credential include generic mock data to represent an access claim such as `buildingId` and `accessLevel` with sample data provided below.
```python
pr_message("Acces Credential Attributes")
access_cred_data_file_path = "config/credential_data/access_cred_data.json"
access_data = {
"buildingId": "HQ-EastWing",
"accessLevel": "Level 2 - Common Areas & Labs"
}
with open(access_cred_data_file_path, 'w') as f:
json.dump(access_data, f, indent=4)
!cat {access_cred_data_file_path} | jq
```
Acces Credential Attributes
{
"buildingId": "HQ-EastWing",
"accessLevel": "Level 2 - Common Areas & Labs"
}
#### Edge Data and SAID Calculation
When creating the Edge Data, the `manager_endorsement` edge is defined to link to the Role Credential ACDC by using the SAID of the Role Credential said, stored in the `role_credential_said` variable. The schema SAID `s` for this edge is the schema identifier, or SAID, of the Role Credential schema and is set to `role_schema_said`. The operator `o` is set to `I2I`.
To make this edge block verifiable, the `!kli saidify --file` command is used. When this command is executed, KERI processes the JSON content of the specified file and calculates a Self-Addressing Identifier (SAID) for its entire content. Crucially, the command then modifies the input file in place:
- It adds (or updates, if already present) a top-level field named `d` within the JSON structure of the file.
- The value of this `d` field is set to the newly calculated SAID.
```python
pr_message("Access Credential Edges")
access_cred_edge_file_path = "config/credential_data/access_cred_edge.json"
access_edge = {
"d": "",
"manager_endorsement": {
"n": role_credential_said,
"s": role_schema_said,
"o": "I2I"
}
}
with open(access_cred_edge_file_path, 'w') as f:
json.dump(access_edge, f, indent=4)
!kli saidify --file {access_cred_edge_file_path}
!cat {access_cred_edge_file_path} | jq
```
Access Credential Edges
{
"d": "EKkr7dXYEw4HFBpppP36I4hNmR6F_0XOHwduQSMGYTRF",
"manager_endorsement": {
"n": "EEy7jb0V3U8MucFutlRulbQ9eIYWNDrm7Jsbuzg144aZ",
"s": "ENWatfUaeryBqvGnG7VdILVcqk84_eoxmiaJYguJXaRw",
"o": "I2I"
}
}
#### Rule Data
The rule section `usageDisclaimer` contains a simple legal disclaimer. Take notice that this data property is also SAIDified.
```python
pr_message("Access Credential Rules")
access_cred_rule_file_path = "config/credential_data/access_cred_rule.json"
access_rule = {
"d": "",
"usageDisclaimer": {
"l": "This mock credential grants no actual access. For illustrative use only."
}
}
with open(access_cred_rule_file_path, 'w') as f:
json.dump(access_rule, f, indent=4)
!kli saidify --file {access_cred_rule_file_path}
!cat {access_cred_rule_file_path} | jq
```
Access Credential Rules
{
"d": "EGVMk928-Fz4DK2NSvZgtG0JJrMlrpxvuxBKPvFxfPSQ",
"usageDisclaimer": {
"l": "This mock credential grants no actual access. For illustrative use only."
}
}
### Step 3: Employee Creates Access Credential for Sub-contractor
Now, the Employee uses `kli vc create` with the attributes, SAIDified edges, and SAIDified rules files to issue the Access Credential. Notice the additional parameters `--edges` and `rules` to supply the data properties to the command.
```python
time = exec("kli time")
!kli vc create --name {employee_keystore_name} \
--alias {employee_aid_alias} \
--registry-name {employee_registry_name} \
--schema {access_schema_said} \
--recipient {subcontractor_aid_alias} \
--data "@./config/credential_data/access_cred_data.json" \
--edges "@./config/credential_data/access_cred_edge.json" \
--rules "@./config/credential_data/access_cred_rule.json" \
--time {time}
access_credential_said = exec(f"kli vc list --name {employee_keystore_name} --alias {employee_aid_alias} --issued --said --schema {access_schema_said}")
pr_message(f"Access Credential SAID: {access_credential_said}")
pr_continue()
```
Waiting for TEL event witness receipts
Sending TEL events to witnesses
EA0-zXenxtqAZa5rjjCmxjAqhOVqFo-8WgqBmkvHUWEk has been created.
Access Credential SAID: EA0-zXenxtqAZa5rjjCmxjAqhOVqFo-8WgqBmkvHUWEk
You can continue β
### Step 4: Employee Grants, Sub-contractor Admits Access Credential
The commands below show using IPEX to both grant the Access Credential from the manager employee and to admit the Access Credential as the sub-contractor. Finally the sub-contractor's credentials are listed with `kli vc list` to show that the Access Credential has been received.
```python
pr_title("Transferring Access Credential from Employee to Sub-contractor")
time = exec("kli time")
!kli ipex grant --name {employee_keystore_name} \
--alias {employee_aid_alias} \
--said {access_credential_said} \
--recipient {subcontractor_aid_prefix} \
--time {time}
pr_title("Sub-contractor admitting Access Credential")
# Sub-contractor polls for the grant and admits it
subcontractor_grant_msg_said = exec(f"kli ipex list --name {subcontractor_keystore_name} \
--alias {subcontractor_aid_alias} --poll --said")
time = exec("kli time")
!kli ipex admit --name {subcontractor_keystore_name} \
--alias {subcontractor_aid_alias} \
--said {subcontractor_grant_msg_said} \
--time {time}
# Sub-contractor lists the received credential
pr_message("\nSub-contractor's received Access Credential:")
!kli vc list --name {subcontractor_keystore_name} \
--alias {subcontractor_aid_alias} \
--verbose
pr_continue()
```
Transferring Access Credential from Employee to Sub-contractor
Sending message EJ6irfCsL-VdfLeX2g2QlqfB8cljJJjAFiF65C0o7kin to ECfW_ag06odaG2HgGqggnP697j17ZU7nM3dRb4ixpc9B
... grant message sent
Sub-contractor admitting Access Credential
Sending admit message to EGKtnumYK9aJ0aKtX6WS8TPcVt2W8tYrlTbFwvX9T-IB
... admit message sent
Sub-contractor's received Access Credential:
Current received credentials for subcontractor (ECfW_ag06odaG2HgGqggnP697j17ZU7nM3dRb4ixpc9B):
Credential #1: EA0-zXenxtqAZa5rjjCmxjAqhOVqFo-8WgqBmkvHUWEk
Type: AccessCredential
Status: Issued β
Issued by EGKtnumYK9aJ0aKtX6WS8TPcVt2W8tYrlTbFwvX9T-IB
Issued on 2025-09-12T04:10:52.883270+00:00
Full Credential:
{
"v": "ACDC10JSON000320_",
"d": "EA0-zXenxtqAZa5rjjCmxjAqhOVqFo-8WgqBmkvHUWEk",
"i": "EGKtnumYK9aJ0aKtX6WS8TPcVt2W8tYrlTbFwvX9T-IB",
"ri": "ECBgjw1OO2ZVr4U0_513HpnyRNtXmCBQ-UIY-ttC0n1Y",
"s": "EF2zX3g5YDyHMSjgsK4OayZMFmLRMxcAJfW363JhBOfD",
"a": {
"d": "EEnD2_uJHltlMdRJ_XZYE9YtutyPIwdYWlErX9JUeMol",
"i": "ECfW_ag06odaG2HgGqggnP697j17ZU7nM3dRb4ixpc9B",
"dt": "2025-09-12T04:10:52.883270+00:00",
"buildingId": "HQ-EastWing",
"accessLevel": "Level 2 - Common Areas & Labs"
},
"e": {
"d": "EKkr7dXYEw4HFBpppP36I4hNmR6F_0XOHwduQSMGYTRF",
"manager_endorsement": {
"n": "EEy7jb0V3U8MucFutlRulbQ9eIYWNDrm7Jsbuzg144aZ",
"s": "ENWatfUaeryBqvGnG7VdILVcqk84_eoxmiaJYguJXaRw",
"o": "I2I"
}
},
"r": {
"d": "EGVMk928-Fz4DK2NSvZgtG0JJrMlrpxvuxBKPvFxfPSQ",
"usageDisclaimer": {
"l": "This mock credential grants no actual access. For illustrative use only."
}
}
}
You can continue β
The output for the Sub-contractor's received AccessCredential clearly shows:
- The attributes (`a` section) for building access.
- The edge (`e` section) with manager_endorsement linking to the RoleCredential's SAID (`n`) and using the `I2I` operator (`o`).
- The rule (`r` section) with the `usageDisclaimer`.
π SUMMARY
This notebook demonstrated the creation of a chained ACDC relationship using an Issuer-To-Issuee (I2I) edge and the inclusion of a rule:
- Initial Setup: Keystores, AIDs (ACME, Employee, Sub-contractor), and credential registries (for ACME and Employee) were initialized. OOBI connections were established between relevant parties.
- Schema Preparation: Two schemas, role_schema.json (for ACME to Employee) and access_schema.json (for Employee to Sub-contractor), were defined. The access_schema.json included definitions for an e (edges) section and an r (rules) section. Both schemas were assumed to be SAIDified and resolvable via a schema server.
- Role Credential Issuance (ACME to Employee):
- ACME created data for the Role Credential.
- ACME issued the Role Credential to the Employee's AID using
kli vc create.
- The Role Credential was transferred to the Employee via IPEX (
kli ipex grant from ACME, kli ipex admit by Employee).
- Access Credential Issuance (Employee to Sub-contractor):
- The Employee created data for the Access Credential attributes.
- A separate JSON file for the edge was created. This edge (
manager_endorsement) pointed to the SAID of the Role Credential received by the Employee (role_credential_said), specified the Role Credential's schema SAID, and used the "o": "I2I" operator. This edge file was SAIDified using kli saidify --file, which populates its d field.
- A separate JSON file for the rule (
usageDisclaimer) was created and SAIDified using kli saidify --file.
- The Employee issued the Access Credential to the Sub-contractor's AID using
kli vc create, referencing the attributes data file, the SAIDified edge file (--edges), and the SAIDified rule file (--rules).
- The Access Credential was transferred to the Sub-contractor via IPEX.
- Verification: The Sub-contractor's received Access Credential clearly displayed the attributes, the I2I edge linking to the Employee's Role Credential, and the embedded rule.
This process illustrates how KERI and ACDC can model real-world endorsement scenarios where the authority to issue a credential is derived from another verifiable credential held by the issuer and how additional conditions can be embedded using rules.
[<- Prev (ACDC Edges and Rules)](101_75_ACDC_Edges_and_Rules.ipynb) | [Next (ACDC Chained Credentials NI2I) ->](101_85_ACDC_Chained_Credentials_NI2I.ipynb)
# ACDC Issuance with KLI: Not-Issuer-To-Issuee
π― OBJECTIVE
Demonstrate how to issue an ACDC that utilizes a Not-Issuer-To-Issuee (NI2I) edge, illustrating how to reference another parent credential for context without implying the issuer of the child is the issuee of the linked parent ACDC. This notebook will also show how a rule can be embedded in the credential. We will implement the **"Linking to an External Training Course"** scenario.
βΉοΈβ οΈ BUG ALERT
Currently the NI2I operator does not work due to a bug. For more details on this and other issues, please see the Known Issues Section.
The material will be updated once the bug is resolved. For now you can skip this notebook.
## Scenario Recap: Linking to an External Training Course
This notebook focuses on the practical KLI commands for implementing an `NI2I` chained credentials. For a detailed theoretical explanation of ACDC Edges, Edge Operators, and Rules, please refer to the **[Advanced ACDC Features: Edges, Edge Operators, and Rules](101_75_ACDC_Edges_and_Rules.ipynb)** notebook. To summarize the scenario:
- A Company issues a **"Skill Certified"** ACDC to an employee, after the employee completes an internal assessment.
- To add verifiable, supporting context to this certification, the "Skill Certified" ACDC can contain an `NI2I` (Not-Issuer-To-Issuee) edge. This edge would point to a "Course Completion" ACDC that the employee had previously received from an external, third-party Training Provider.
- This `NI2I` link signifies that while the external training is acknowledged as relevant evidence, the Company's authority to issue its own skill certification is independent and does not derive from the Training Provider's credential.
- The "Employee Skill Certified" ACDC will also include a simple rule in its `r` section.
## Initial Setup: Keystores, AIDs, Registries, and OOBIs
We begin by setting up the three participants in our scenario:
- Training Provider (`training_provider_aid`): The entity issuing the course credential.
- Company (`company_aid`): The entity issuing the skill credential that references the course credential.
- Employee (`employee_aid`): The entity who is the subject (issuee) of both credentials.
For each participant, we will:
- Initialize a KERI keystore.
- Incept an AID, using a default witness configuration.
- Establish OOBI connections between the necessary parties (Training Provider β‘οΈ Employee, and Company β‘οΈ Employee) to enable secure communication.
- For the two issuers (Training Provider and Company), we will also incept a credential registry to manage the lifecycle of the credentials they issue.
```python
# Imports and Utility functions
from scripts.utils import exec, clear_keri, pr_title, pr_message, pr_continue
from scripts.saidify import get_schema_said
import json, os
clear_keri()
# Training Provider Keystore and AID
training_provider_keystore_name = "training_provider_ks"
training_provider_salt = exec("kli salt")
training_provider_aid_alias = "training_provider"
training_provider_registry_name = "training_provider_reg"
# Company Keystore and AID
company_keystore_name = "company_ks"
company_salt = exec("kli salt")
company_aid_alias = "company"
company_registry_name = "company_skill_reg"
# Employee Keystore and AID
employee_keystore_name = "employee_ks"
employee_salt = exec("kli salt")
employee_aid_alias = "employee"
pr_title("Initializing keystores")
!kli init --name {training_provider_keystore_name} \
--nopasscode \
--salt {training_provider_salt} \
--config-dir ./config \
--config-file keystore_init_config.json
!kli init --name {company_keystore_name} \
--nopasscode \
--salt {company_salt} \
--config-dir ./config \
--config-file keystore_init_config.json
!kli init --name {employee_keystore_name} \
--nopasscode \
--salt {employee_salt} \
--config-dir ./config \
--config-file keystore_init_config.json
pr_title("Initializing AIDs")
!kli incept --name {training_provider_keystore_name} \
--alias {training_provider_aid_alias} \
--file ./config/aid_inception_config.json
!kli incept --name {company_keystore_name} \
--alias {company_aid_alias} \
--file ./config/aid_inception_config.json
!kli incept --name {employee_keystore_name} \
--alias {employee_aid_alias} \
--file ./config/aid_inception_config.json
pr_title("Initializing Credential Registries")
!kli vc registry incept --name {training_provider_keystore_name} \
--alias {training_provider_aid_alias} \
--registry-name {training_provider_registry_name}
!kli vc registry incept --name {company_keystore_name} \
--alias {company_aid_alias} \
--registry-name {company_registry_name}
training_provider_aid_prefix = exec(f"kli aid --name {training_provider_keystore_name} --alias {training_provider_aid_alias}")
company_aid_prefix = exec(f"kli aid --name {company_keystore_name} --alias {company_aid_alias}")
employee_aid_prefix = exec(f"kli aid --name {employee_keystore_name} --alias {employee_aid_alias}")
pr_message(f"Training Provider AID: {training_provider_aid_prefix}")
pr_message(f"Company AID: {company_aid_prefix}")
pr_message(f"Employee AID: {employee_aid_prefix}")
pr_title("Generating and resolving OOBIs")
# Training Provider and Employee OOBI Exchange
training_provider_oobi = exec(f"kli oobi generate --name {training_provider_keystore_name} --alias {training_provider_aid_alias} --role witness")
employee_oobi = exec(f"kli oobi generate --name {employee_keystore_name} --alias {employee_aid_alias} --role witness")
company_oobi = exec(f"kli oobi generate --name {company_keystore_name} --alias {company_aid_alias} --role witness")
!kli oobi resolve --name {training_provider_keystore_name} \
--oobi-alias {employee_aid_alias} \
--oobi {employee_oobi}
!kli oobi resolve --name {employee_keystore_name} \
--oobi-alias {training_provider_aid_alias} \
--oobi {training_provider_oobi}
# Company and Employee OOBI Exchange
!kli oobi resolve --name {company_keystore_name} \
--oobi-alias {employee_aid_alias} \
--oobi {employee_oobi}
!kli oobi resolve --name {employee_keystore_name} \
--oobi-alias {company_aid_alias} \
--oobi {company_oobi}
# Company and Training Provider -----------------------------------
!kli oobi resolve --name {company_keystore_name} \
--oobi-alias {training_provider_aid_alias} \
--oobi {training_provider_oobi}
!kli oobi resolve --name {training_provider_keystore_name} \
--oobi-alias {company_aid_alias} \
--oobi {company_oobi}
pr_message("OOBI connections established.")
pr_continue()
```
## Schema Definitions
For this scenario, we require two distinct ACDC schemas:
- **Course Completion Schema** (' course_completion_schema.json' ): Defines the credential issued by the Training Provider.
- **Employee Skill Schema** (' employee_skill_schema.json'): Defines the credential issued by the Company, which will include the `NI2I` edge and a rule.
βΉοΈ NOTE
For this notebook, the schemas have been pre-SAIDified and are available on our mock schema server. The process of SAIDifying schemas was detailed in a previous notebook.
### Course Completion Schema
This schema defines a basic credential for certifying the completion of a training course. It's a standard, non-chained credential.
Filename: `course_completion_schema.json`
```python
course_schema_path = "config/schemas/course_completion_schema.json"
pr_title(f"Schema: {course_schema_path}")
course_schema_said = get_schema_said(course_schema_path)
pr_message(f"Schema SAID: {course_schema_said}")
pr_message(f"Retrieving Schema from Server:")
!curl -s http://vlei-server:7723/oobi/{course_schema_said} | jq
pr_continue()
```
### Employee Skill Schema
This schema defines the credential issued by the Company to the Employee. It includes an `e` (edges) section with an `NI2I` operator to reference the "Course Completion" credential and an `r` (rules) section for a verification policy.
Filename: `employee_skill_schema.json`
```python
skill_schema_path = "config/schemas/skill_certified_schema.json"
pr_title(f"Schema: {skill_schema_path}")
skill_schema_said = get_schema_said(skill_schema_path)
pr_message(f"Schema SAID: {skill_schema_said}")
pr_message(f"Retrieving Schema from Server:")
!curl -s http://vlei-server:7723/oobi/{skill_schema_said} | jq
pr_continue()
```
## Resolving Schema OOBIs
All three participants must resolve the OOBIs for both schemas to ensure they can understand and validate the credentials.
```python
pr_title("Resolving schema OOBIs")
course_schema_oobi = f"http://vlei-server:7723/oobi/{course_schema_said}"
skill_schema_oobi = f"http://vlei-server:7723/oobi/{skill_schema_said}"
# Participants resolving Course Completion Schema
!kli oobi resolve --name {training_provider_keystore_name} \
--oobi-alias "course_schema" \
--oobi {course_schema_oobi}
!kli oobi resolve --name {company_keystore_name} \
--oobi-alias "course_schema" \
--oobi {course_schema_oobi}
!kli oobi resolve --name {employee_keystore_name} \
--oobi-alias "course_schema" \
--oobi {course_schema_oobi}
# Participants resolving Employee Skill Schema
!kli oobi resolve --name {training_provider_keystore_name} \
--oobi-alias "skill_schema" \
--oobi {skill_schema_oobi}
!kli oobi resolve --name {company_keystore_name} \
--oobi-alias "skill_schema" \
--oobi {skill_schema_oobi}
!kli oobi resolve --name {employee_keystore_name} \
--oobi-alias "skill_schema" \
--oobi {skill_schema_oobi}
pr_message("Schema OOBIs resolved.")
pr_continue()
```
## Issuing credentials
Now that the setup is complete and the schemas are available, it's necessary to create the credential chain starting with the Course Completion credential and later the Employee Skill credential.
### Step 1: Course Completion Credential Issuance (Training Provider to Employee)
First, the Training Provider issues the "Course Completion" credential to the Employee. This establishes the base credential that will be referenced later.
**Create Course Completion Credential Data**
Create a JSON file with the specific attributes for the course completion.
```python
pr_title("Creating Course Completion credential data")
!echo '{ \
"courseName": "Advanced Cryptographic Systems", \
"courseLevel": "Expert", \
"completionDate": "2024-09-15" \
}' > config/credential_data/course_cred_data.json
!cat config/credential_data/course_cred_data.json | jq
pr_continue()
```
### Training Provider Issues Credential
The Training Provider uses `kli vc create` to issue the credential.
```python
pr_title("Creating Course Completion credential")
issue_time_training = exec("kli time")
!kli vc create --name {training_provider_keystore_name} \
--alias {training_provider_aid_alias} \
--registry-name {training_provider_registry_name} \
--schema {course_schema_said} \
--recipient {employee_aid_prefix} \
--data "@./config/credential_data/course_cred_data.json" \
--time {issue_time_training}
course_credential_said = exec(f"kli vc list --name {training_provider_keystore_name} --alias {training_provider_aid_alias} --issued --said --schema {course_schema_said}")
pr_message(f"Course Credential SAID: {course_credential_said}")
pr_continue()
```
**IPEX Transfer: Training Provider Grants, Employee Admits**
The credential is then transferred to the Employee using the standard IPEX grant/admit flow.
```python
pr_title("Transferring Course Completion credential (IPEX)")
time = exec("kli time")
!kli ipex grant --name {training_provider_keystore_name} \
--alias {training_provider_aid_alias} \
--said {course_credential_said} \
--recipient {employee_aid_prefix} \
--time {time}
# Employee polls for the grant and admits it
print("Polling mailboxes for IPEX Grant messages to admit...")
employee_grant_msg_said = exec(f"kli ipex list --name {employee_keystore_name} --alias {employee_aid_alias} --poll --said")
time = exec("kli time")
!kli ipex admit --name {employee_keystore_name} \
--alias {employee_aid_alias} \
--said {employee_grant_msg_said} \
--time {time}
pr_message("\nEmployee's received Course Completion Credential:")
!kli vc list --name {employee_keystore_name} \
--alias {employee_aid_alias} \
--verbose
pr_continue()
```
### Step 2: Employee Skill Credential Issuance (Company to Employee)
Now, the Company issues the "Employee Skill Certified" credential, which will link to the one the Employee just received.
Create Data Properties for Skill Credential
We need to create three separate JSON files for the attributes, the NI2I edge, and the rule.
**Attributes Data**
Generic mock data to represent an access claim.
```python
pr_message("Employee Skill Credential Attributes")
skill_cred_data_file_path = "config/credential_data/skill_cred_data.json"
skill_data = {
"skillName": "Secure System Design",
"skillLevel": "Proficient",
"assessmentDate": "2025-01-20"
}
with open(skill_cred_data_file_path, 'w') as f:
json.dump(skill_data, f, indent=4)
!cat {skill_cred_data_file_path} | jq
```
**Edge Data (NI2I) and SAIDification**
The edge data is created, linking to the `course_credential_said` obtained in the previous step. The operator `o` is explicitly set to `NI2I`. This file is then SAIDified using `kli saidify` to populate its `d` field.
```python
pr_message("Employee Skill Credential Edges (NI2I)")
skill_cred_edge_file_path = "config/credential_data/skill_cred_edge.json"
skill_edge = {
"d": "",
"supporting_evidence": {
"n": course_credential_said,
"s": course_schema_said,
"o": "NI2I"
}
}
with open(skill_cred_edge_file_path, 'w') as f:
json.dump(skill_edge, f, indent=4)
!kli saidify --file {skill_cred_edge_file_path}
!cat {skill_cred_edge_file_path} | jq
```
**Rule Data and SAIDification**
The rule data is created and SAIDified.
```python
pr_message("Employee Skill Credential Rules")
skill_cred_rule_file_path = "config/credential_data/skill_cred_rule.json"
skill_rule = {
"d": "",
"verification_policy": {
"l": "Verification of this skill certification requires checking the validity of supporting evidence."
}
}
with open(skill_cred_rule_file_path, 'w') as f:
json.dump(skill_rule, f, indent=4)
!kli saidify --file {skill_cred_rule_file_path}
!cat {skill_cred_rule_file_path} | jq
```
#### Company Issues Skill Credential
The Company now creates the chained credential using kli vc create, supplying the attributes, edges, and rules files.
```python
time = exec("kli time")
!kli vc create --name {company_keystore_name} \
--alias {company_aid_alias} \
--registry-name {company_registry_name} \
--schema {skill_schema_said} \
--recipient {employee_aid_prefix} \
--data "@./config/credential_data/skill_cred_data.json" \
--edges "@./config/credential_data/skill_cred_edge.json" \
--rules "@./config/credential_data/skill_cred_rule.json" \
--time {time}
skill_credential_said = exec(f"kli vc list --name {company_keystore_name} --alias {company_aid_alias} --issued --said --schema {skill_schema_said}")
pr_message(f"Employee Skill Credential SAID: {skill_credential_said}")
pr_continue()
```
### Step 3: Company Grants, Employee Admits Skill Credential
The final step is to transfer the newly created chained credential to the Employee.
```python
pr_title("Transferring Employee Skill Credential from Company to Employee")
time = exec("kli time")
!kli ipex grant --name {company_keystore_name} \
--alias {company_aid_alias} \
--said {skill_credential_said} \
--recipient {employee_aid_prefix} \
--time {time}
pr_title("Employee admitting Skill Credential")
# Employee polls for the grant and admits it
employee_skill_grant_msg_said = exec(f"kli ipex list --name {employee_keystore_name} --alias {employee_aid_alias} --poll --said")
time = exec("kli time")
!kli ipex admit --name {employee_keystore_name} \
--alias {employee_aid_alias} \
--said {employee_skill_grant_msg_said} \
--time {time}
pr_message("\nEmployee's received Employee Skill Credential:")
!kli vc list --name {employee_keystore_name} --alias {employee_aid_alias} --said {skill_credential_said} --verbose
pr_continue()
```
When you view the final "Employee Skill Credential" held by the Employee, you will see:
- The attributes (`a` section) for the certified skill.
- The edge (`e` section) with `supporting_evidence` linking to the Course Completion ACDC's SAID (n) and using the `NI2I` operator (`o`).
- The rule (`r` section) with the `verification_policy`.
This confirms the successful creation and issuance of a chained credential using an NI2I edge to provide external, verifiable context.
π SUMMARY
This notebook demonstrated the creation of a chained ACDC relationship using a Not-Issuer-To-Issuee (NI2I) edge and the inclusion of a rule.
- Setup: Three participants (Training Provider, Company, Employee) were initialized with keystores, AIDs, and credential registries for the issuers. OOBI connections were established between them.
- Schema Preparation: Two schemas, one for "Course Completion" and another for "Skill Certified" (which included definitions for
e and r sections), were resolved by all parties from a schema server.
- Base Credential Issuance (Training Provider to Employee):
- The Training Provider issued a "Course Completion" ACDC to the Employee.
- This credential was transferred via IPEX and admitted by the Employee. The SAID of this credential was saved for the next step.
- Chained Credential Issuance (Company to Employee):
- The Company prepared the data for the "Employee Skill" ACDC.
- An edge file was created, linking to the previously issued "Course Completion" ACDC's SAID and explicitly using the
"o": "NI2I" operator. This file was SAIDified.
- A rule file was created with a custom policy and was also SAIDified.
- The Company issued the "Employee Skill" ACDC using
kli vc create, supplying the attributes, edges, and rules files.
- This second credential was transferred to the Employee via IPEX.
- Verification: The final ACDC held by the Employee contained the skill attributes, the NI2I edge pointing to the course certificate as supporting evidence, and the embedded verification policy rule, successfully demonstrating the NI2I use case.
```python
# from scripts.saidify import process_schema_file
# # Run the saidify script
# process_schema_file("./config/schemas/course_completion_schema.bak.json", "./config/schemas/course_completion_schema.json", True)
# process_schema_file("./config/schemas/skill_certified_schema.bak.json", "./config/schemas/skill_certified_schema.json", True)
```
[<- Prev (ACDC Chained Credentials I2I)](101_80_ACDC_Chained_Credentials_I2I.ipynb) | [Next (KERIA Signify) ->](102_05_KERIA_Signify.ipynb)
# Introducing KERIA and Signify: Architecture and Concepts
π― OBJECTIVE
Introduces the foundational concepts of KERIA and Signify TS, focusing on:
- The KERIA/Signify client-agent architecture.
- Key KERIA endpoint interfaces.
- The concept of Endpoint Role records (End Roles).
- The relationship between Client AIDs and Agent AIDs.
- What SignifyTS and SignifyPy are.
## Edge Signing Client - Agent Architecture
KERI is a protocol for secure, self-certifying identifiers. **KERIA** is an implementation of the multi-tenant server portion of the Signify agent protocol for KERI agents and is designed to run as a service (e.g., in the cloud or self-hosted) that manages AIDs on behalf of a controller. **Signify TS** is a TypeScript library implementing the edge signing client protocol. It acts as an edge signing client, enabling applications to interact with a KERIA agent. **Signify Py** is a Python library implementing the edge client protocol of the Signify and KERIA protocol.
What does edge signing mean? It means that all of the cryptographic keypairs only ever exist at the edge, in memory, within the application that uses a Signify library. Keys existing at the edge provides security and confidence that only the person possessing the passcode, or cryptographic seed, is the one performing signing. The phrase "edge" is used to indicate that the Signify libraries are used in person-facing applications.
The idea behind this client-agent architecture is to enable "signing at the edge". Your sensitive private keys, used for signing key events and other data, remain on the client-side (managed by a Signify client library). The KERIA agent, running remotely, handles tasks like:
* Storing key indexes (retrieved by the Signify Client on boot up)
* Creating KERI AIDs delegated from the edge AID
* Managing Key Event Logs (KELs) for edge AIDs
* Creating, storing, issuing, revoking, presenting, and receiving ACDCs
* Acting as a cloud mailbox to send and receive KERI and ACDC messages from
* Interacting with witnesses
* Exchanging messages with other KERI agents
The KERIA agent itself never has access to your private keys. All critical signing operations happen on the client, and the signed events are then sent to the KERIA agent for processing and dissemination.
This architecture separates key management and signing authority (client-side) from the operational aspects of maintaining AIDs' KELs and ACDCs and their availability (agent-side).
### KERIA Deployment and Configuration
In a typical deployment, KERIA starts up and loads its configuration, including a list of default witnesses (or OOBI URLs) and ACDC schemas, from a JSON configuration file (e.g., **[keria configuration file](config/keria/keria-docker.json)**). This allows the agent to be pre-configured with a set of witnesses and connections to any other KERI AID that any AIDs created on that KERIA server are preconfigured to see.
## Agent Service Endpoints
A KERIA service instance exposes distinct HTTP endpoints to handle different types of interactions:
1. **Boot Interface** (`boot port`, e.g., 3903 by default):
* **Purpose**: Used for the initial setup and provisioning of a KERIA agent worker for a Signify client. This is where the client and agent establish their initial secure relationship.
* **Interaction**: The Signify client sends its client AID's inception event to this endpoint to request the creation of a delegated agent AID.
* **Accessibility**: Often restricted to internal infrastructure or disabled if agents are pre-configured (static worker mode).
2. **Admin Interface** (`admin port`, e.g., 3901 by default):
* **Purpose**: This is the primary REST API for the Signify client to command and control its KERIA agent.
* **Interaction**: Used for operations like creating new AIDs, rotating keys, issuing credentials, resolving OOBIs, etc. All requests to this interface must be authenticated (e.g., signed by the Client AID).
* **Accessibility**: Typically exposed externally to allow the client to manage its AIDs.
3. **KERI Protocol Interface** (`http port`, e.g., 3902 by default):
* **Purpose**: Handles standard KERI protocol messages (e.g., KELs, receipts, challenges) exchanged with other KERI agents and witnesses in the wider KERI network.
* **Interaction**: Facilitates multi-sig coordination, credential revocation, KEL exchange, etc., using CESR (Composable Event Streaming Representation) over HTTP.
* **Accessibility**: Exposed externally to enable interaction with the global KERI ecosystem.
This separation of interfaces enhances security and deployment flexibility.
## Understanding Endpoint Role Records
An **endpoint role record**, or "end role" for short, in KERI is an authorization that one AID grants to itself or another AID to act in a particular role representing a specific capacity to act on its behalf. Think of it as assigning a specific job to itself or another identifier.
For instance, when a Signify client connects to a KERIA agent, the **Client AID** (controlled by the user or application) delegates authority to an **Agent AID** (managed by the KERIA service). The Client AID essentially authorizes its Agent AID to perform certain KERI operations in its name, like anchoring its KEL with witnesses or responding to discovery requests.
Declaring an end role typically involves creating a KERI event, often an interaction event (`ixn`) or an establishment event (`icp` or `rot`) with specific configuration (`c` field) or an `end` role event, that specifies:
* The AID granting the authorization (the delegator or authorizer).
* The AID receiving the authorization (the delegate or authorized party).
* The specific role being granted (e.g., `agent`, `witness`, `watcher`, `controller`, `mailbox`).
This signed authorization will either be recorded in the KEL of the authorizing AID to make the role assignment verifiable by anyone who can access and validate that KEL or it will be stored locally for transmission when the key event log is requested through OOBI exchange or when it is sent directly from a given AID.
### Example of endpoint role in action
The end role records are necessary to enable specific permissions within the internal KERI communication system. For example, an agent AID having been assigned the role of `agent` for a given delegator, the Signify controller AID, allows messages for the controlling AID to be sent through the authorized agent AID.
## Client and Agent AIDs Explained
When you use Signify TS to connect to a KERIA agent, two primary AIDs are involved:
1. **Client AID**:
* This is an AID that *you* (or your application) control directly via the Signify TS client.
* It is a single signature AID.
* You hold its private keys.
* It's typically a transferable AID, allowing for key rotation.
* It acts as the **delegator** to the Agent AID.
* The rotation key index for this client AID is stored encrypted in the KERIA agent that the client pairs with.
2. **Agent AID**:
* This AID is created and managed by the KERIA service *on your behalf* when the request to the `/boot` endpoint is made with the Client AID's inception event.
* Its inception event specifies the Client AID as its delegator (`di` field in the inception event). This means the Agent AID's authority to act is derived from, and anchored to, your Client AID.
* It's also typically a transferable AID.
* The KERIA service uses this Agent AID to perform actions for your Client AID, such as acting as a communication proxy to interact with witnesses or other agents, without needing direct access to your Client AID's private keys.
The Signify client generates the Client AID and sends its inception event to the KERIA agent's Boot Interface (on `/boot`). The KERIA service then creates the delegated Agent AID and returns its inception event to the client. Finally, the Signify client approves this delegation by sending an interaction event back to the KERIA agent's Admin Interface.
This delegation model is fundamental to KERIA's security: your primary controlling keys (for the Client AID) remain "at the edge," while the KERIA agent operates with a delegated authority (via the Agent AID) that is always traceable back to your Client AID.
## SignifyTS and SignifyPy - Signify Client Libraries
There are currently two Signify protocol client library implementations, one in Typescript and one in Python. [SignifyTS](https://github.com/WebOfTrust/signify-ts) is the Typescript implementation whereas [SignifyPy](https://github.com/WebOfTrust/signifypy) is the Python implementation. The most up-to-date implementation is SignifyTS and is recommended to be used. SignifyPy would be usable with a modest amount of upgrade effort. A Rust client, [scir](https://github.com/WebOfTrust/scir) was started yet is currently mostly unfinished and should not be used.
### SignifyTS, or Signify TS
SignifyTS, also referred to as "signify-ts", is the Typescript implementation of the Signify client protocol and may be found at https://github.com/WebOfTrust/signify-ts.
### SignifyPy, or Signify Py
SignifyPy, also known as "sigpy", is the Python implementation of the Signify client protocol and was the original implementation of Signify and was used to drive initial creation of the KERIA server side of the Signify protocol. It may be found at https://github.com/WebOfTrust/signifypy.
π SUMMARY
The KERIA/Signify architecture enables "signing at the edge," where a Signify client (like Signify TS) manages private keys and signing operations locally, while a remote KERIA agent handles ACDC storage, witness interactions, and KERI protocol communications. KERIA exposes three main HTTP endpoints:
- Boot Interface: For initial client-agent provisioning and creation of a delegated Agent AID.
- Admin Interface: A REST API for the client to command and control its agent (e.g., create AIDs, rotate keys).
- KERI Protocol Interface: For standard KERI message exchange with other agents and witnesses.
Endpoint role records, end roles, in KERI define verifiable authorizations for one AID to act in a specific capacity for another (e.g., an Agent AID acting in the 'agent' role for a Client AID).
The connection process involves a Client AID (controlled by the user via Signify) delegating authority to an Agent AID (managed by KERIA).
SignifyTS and SignifyPy comprise the two implementations of the Signify client protocol that are usable with the recommendation being to use SignifyTS.
[<- Prev (ACDC Chained Credentials NI2I)](101_85_ACDC_Chained_Credentials_NI2I.ipynb) | [Next (KERIA Signify Basic Operations) ->](102_10_KERIA_Signify_Basic_Operations.ipynb)
# Signify TS Basics: Client Setup and AID Management
π― OBJECTIVE
Introduce basic operations using the typescript implementation of Signify, Signify TS: creating a client, initializing (booting) an agent, connecting to an agent, and creating an Autonomic Identifier (AID).
Familiarity with core KERI concepts (AIDs, KELs, digital signatures, witnesses, OOBIs) is assumed.
## Connecting to a KERIA Agent
Now that we understand the architecture, let's see how to use the [signify-ts](https://github.com/WebOfTrust/signify-ts) library to initialize a Signify controller and establish a connection with a KERIA agent. This process involves three main steps:
1. Initializing the `signify-ts` library, necessary since the dependency libsodium must be initialized in order to be used.
2. Creating a `SignifyClient` instance, creating your Client AID, which is where your cryptographic keypairs are stored in-memory, and contains your client's connection to a specific KERIA agent once bootstrapped.
3. Bootstrapping and connecting the client to a KERIA agent, which establishes the relationship Client AID and the delegated Agent AID in a specific KERIA instance.
βΉοΈ Note: KERIA should be available
This section assumes that a KERIA agent is running and its Boot and Admin interfaces are accessible at the specified URLs. In the context of these notebooks, KERIA is pre-configured and running as part of the Docker deployment.
### Initializing the Signify TS Library
The `signify-ts` library contains components for cryptographic operations using libsodium. Before any of its functionalities can be used, these components must be initialized. This is achieved by calling and the `ready()` function. This function should be called at the initialization of your application before any functions or SignifyClient methods from `signify-ts` are used.
```typescript
import { randomPasscode, ready, SignifyClient, Tier } from 'npm:signify-ts';
await ready();
console.log("Signify-ts library initialized and ready.");
```
Signify-ts library initialized and ready.
### Creating the Client Instance
Once the library is initialized, you can create an instance of `SignifyClient`. This object will be your primary interface for all interactions with the KERIA agent. It requires several parameters:
- **url**: The URL of the KERIA agent's Admin Interface. The client uses this for most command and control operations after the initial connection is established.
- **bran**: A 21-character, high-entropy string, often referred to as a "passcode." This bran serves as the root salt for deriving the Client AID's signing and rotation keys via a Hierarchical Deterministic (HD) key algorithm. It is critical to treat the bran as securely as a private key. Losing it means losing control of the Client AID and any identifiers or ACDCs created in the connected KERIA Agent, if any.
- **tier**: The security tier for the passcode hashing algorithm. Tier.low, Tier.med, and Tier.high represent different computational costs for deriving keys from the bran. Higher tiers are more resistant to brute-force attacks but require more processing power and time. The high tier is appropriate for any use. The low tier is primarily used for unit testing so that tests will complete quickly.
- **bootUrl**: The URL of the KERIA agent's Boot Interface. This is used for the initial setup and provisioning of the agent worker for this client.
```typescript
const adminUrl = 'http://keria:3901'; // KERIA agent's Admin Interface URL
const bootUrl = 'http://keria:3903'; // KERIA agent's Boot Interface URL
// Generate a new random 21-character bran (passcode/salt)
// In a real application, you would securely store and reuse this bran by having the user reenter it on opening the application.
const bran = randomPasscode();
// Create the SignifyClient instance
const client = new SignifyClient(
adminUrl,
bran,
Tier.low, // Using Tier.low for faster execution
bootUrl
);
console.log('SignifyClient instance created.');
console.log('Using Passcode (bran):', bran);
```
SignifyClient instance created.
Using Passcode (bran): DM4gMSUh1eELpAXSn7e7c
βΉοΈ NOTE
In a production environment, the bran must be securely generated and stored and should NOT be displayed on screen or in any log messages. It is displayed above for illustrative and training purposes only.
For a given Client AID, you must consistently use the same bran to reconnect and derive the correct private keys. Using randomPasscode() each time, as in this demo, will result in a new Client AID being created or an inability to connect to an existing one if the KERIA agent already has a state associated with a different bran for its controller.
### Bootstrapping and Connecting to the Agent
With the `SignifyClient` instance created, the next step is to establish the initial connection and state with the KERIA agent. This involves two methods:
- **`client.boot()`**: Initiates the bootstrapping process with the KERIA agent's Boot Interface:
- The client generates its Client AID using the provided bran.
- It sends the Client AID's inception event to the KERIA agent's Boot Interface, along with the KEL of the Client AID (also known as `caid`).
- The KERIA agent, upon successful verification of the client AID, creates a delegated Agent AID, that is delegated from the Client AID, and returns the delegated Agent AID inception event to the client.
- This step essentially provisions the necessary resources and partially the delegated relationship on the KERIA agent for this specific client.
- **`client.connect()`**: After `boot()` (or if the agent has been previously booted with the same bran), connect() completes the delegation to the KERIA Agent AID via its Admin Interface on the first invocation of `.connect()`. All subsequent invocations reuse the existing Agent state and just read the existing key state from the already existing agent.
```typescript
// Bootstrap the connection with the KERIA agent
// This creates the Client AID and requests the Agent AID creation.
await client.boot(); // Triggers a request to the /boot endpoint on the Boot URL from the initial SignifyClient configuration
console.log('Client boot process initiated with KERIA agent.');
// Completes the delegation, if needed, between the Client AID and the Agent AID, and initializes the SignifyClient dependencies.
await client.connect();
console.log('Client connected to KERIA agent.');
// Retrieve and display the current state
const state = await client.state();
console.log('\nConnection State Details:');
console.log('-------------------------');
console.log('Client AID Prefix: ', state.controller.state.i);
console.log('Client AID Keys: ', state.controller.state.k);
console.log('Client AID Next Keys Digest: ', state.controller.state.n);
console.log('')
console.log('Agent AID Prefix: ', state.agent.i);
console.log('Agent AID Type: ', state.agent.et); // Should be 'dip' for delegated inception
console.log('Agent AID Delegator:', state.agent.di); // Should be the Client AID's prefix
```
Client boot process initiated with KERIA agent.
Client connected to KERIA agent.
Connection State Details:
-------------------------
Client AID Prefix: EH8QJKt2VvEKXuG11MkjXcHpINk0NurX20CnU2s5KYFw
Client AID Keys: [ "DMh176ZFof98WEDQ3mTU4OUNls8jOIHxHssQB0QDtoYI" ]
Client AID Next Keys Digest: [ "EPWeorrNy2yJcl0Yf5gbmUwjlwdSzgHIUbMnuL4B7nPf" ]
Agent AID Prefix: ED-3z7JwTpmFSC87Z_LRlrvdT2hyQW9VsKVDIlFpF755
Agent AID Type: dip
Agent AID Delegator: EH8QJKt2VvEKXuG11MkjXcHpINk0NurX20CnU2s5KYFw
**Output Explanation:**
- **Client AID Prefix:** The unique, self-certifying identifier for the controller AID of the SignifyClient instance, tied to the bran.
- **Client AID Keys:** The current public signing key(s) for the Client AID.
- **Client AID Next Keys Digest:** The digest (hash) of the public key(s) pre-rotated for the next key rotation of the Client AID.
- **Agent AID Prefix:** The unique KERI AID of the KERIA agent worker associated with your client.
- **Agent AID Type:** dip indicates a "delegated inception" event, signifying that this Agent AID's authority is delegated by another AID, in this case the Client AID of the SignifyClient instance.
- **Agent AID Delegator:** This crucial field shows the prefix of the Client AID, confirming that the Agent AID is indeed delegated by your Client AID.
### Reconnecting to an Existing Agent
If the KERIA agent has already been booted for a specific `bran` (Client AID), you don't need to call `client.boot()` again when using the same bran. You directly use `client.connect()`. SignifyTS will detect and reuse the existing agent state.
```typescript
// Create a new client instance with the SAME bran
const client2 = new SignifyClient(
adminUrl,
bran, // Using the same bran as the first client
Tier.low,
bootUrl
);
console.log('Second SignifyClient instance created with the same bran.');
// Connect without booting, as the agent state for this bran should already exist
await client2.connect();
console.log('Second client connected to the existing KERIA agent.');
const state2 = await client2.state();
console.log('\nReconnection State Details:');
console.log('---------------------------');
console.log('Client AID Prefix: ', state2.controller.state.i); // Should be the same Client AID
console.log('Agent AID Prefix: ', state2.agent.i); // Should be the same Agent AID
console.log('Agent AID Delegator:', state2.agent.di); // Should be the same Client AID
```
Second SignifyClient instance created with the same bran.
Second client connected to the existing KERIA agent.
Reconnection State Details:
---------------------------
Client AID Prefix: EH8QJKt2VvEKXuG11MkjXcHpINk0NurX20CnU2s5KYFw
Agent AID Prefix: ED-3z7JwTpmFSC87Z_LRlrvdT2hyQW9VsKVDIlFpF755
Agent AID Delegator: EH8QJKt2VvEKXuG11MkjXcHpINk0NurX20CnU2s5KYFw
π SUMMARY
To connect to a KERIA agent using SignifyTS:
- Initialize the library with
await ready().
- Create a
SignifyClient instance, providing the agent's Admin and Boot URLs, a unique 21-character bran (passcode/salt for key derivation), and a security Tier.
- For the first-time connection with a new
bran, call await client.boot() to provision the Client AID and request the creation of a delegated Agent AID from KERIA.
- Call
await client.connect() to and retrieve the state of the Client and Agent AIDs and, on first invocation, complete any delegation approvals. The Client AID delegates authority to the Agent AID, whose inception event (type dip) will list the Client AID as its delegator.
- For subsequent connections using the same
bran, skip client.boot() and directly use client.connect().
The
bran is critical for deriving the Client AID's keys and must be kept secure and reused consistently in order to have the same identity across time.
## Adding an Autonomic Identifier (AID)
Once your Signify client is initialized and connected to the KERIA agent you can create new AIDs and instruct the agent to store key events and key indexes for the new AIDs, called managed AIDs. These AIDs will be controlled by your Client AID (established during the `connect()` phase) through the delegation mechanism.
### Initiating AID Inception
Creating a new AID occurs locally yet storing its KEL and current key index are asynchronous operations. When you request the KERIA agent to store the inception event and key index of the new AID the agent starts the process and also obtains witness receipts from any witnesses stated in the inception event. The `signify-ts` library handles this asynchronous operation by returning an "operation" object in response to creating an AID which you can then use to poll for completion of the inception process.
The `client.identifiers().create()` method is used to start the inception of a new AID.
**Parameters Explained:**
- **aidAlias (string):** This is a human-readable alias that you assign to the AID within your Signify client's local storage. It is used to refer to this AID in subsequent client operations. It is not part of the KERI protocol itself but a convenience label for client-side management.
- **inceptionArgs (object):** This object contains the configuration for the new AID:
- **toad (number):** The Threshold of Accountable Duplicity. This is the minimum number of witness receipts the controller (your Client AID via KERIA) requires for this new AID's events to be considered accountable.
- **wits (array of strings):** A list of AID prefixes of the witnesses that this new AID should use. These witnesses must be discoverable by your KERIA agent (e.g., pre-loaded during KERIA's startup or resolved via OOBIs by the client/agent).
- **Other parameters:** not shown for brevity but available, see **[CreateIdentifierArgs](https://weboftrust.github.io/signify-ts/interfaces/CreateIdentiferArgs.html)**
```typescript
// Define an alias for the new AID for easy reference within the client
const aidAlias = 'newAid';
// Inception request parameters
const identifierArgs = {
toad: 2, // Threshold of Accountable Duplicity: minimum number of witness receipts required
wits: [ // List of witness AID prefixes to use for this AID
'BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha',
'BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM',
'BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX'
]
// Other parameters can be specified. If not, defaults are used.
};
// Creates and sends the locally client-signed inception event to the KERIA agent,
// - initializing to zero (0) the agent-stored key index for this AID.
// - causing the agent to obtain witness receipts for the event as needed
const inceptionResult = await client.identifiers().create(aidAlias, identifierArgs);
console.log(`AID inception initiated for alias: ${aidAlias}`);
// The result contains information about the long-running operation
const inceptionOperation = await inceptionResult.op();
console.log('Inception Operation Details:');
console.log(inceptionOperation);
```
AID inception initiated for alias: newAid
Inception Operation Details:
{
name: "witness.ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF",
metadata: { pre: "ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF", sn: 0 },
done: false,
error: null,
response: null
}
**Outout explained**
Calling `inceptionResult.op()` returns a promise that resolves to an operation object containing:
- **name:** A unique name for this long-running operation (e.g., `witness.AID_PREFIX`). KERIA uses this to track the task. The prefix in the name corresponds to the AID being created.
- **metadata:** Contains details like the prefix (pre) of the AID being incepted and the sequence number (`sn`, which is 0 for inception).
- **done:** A boolean indicating if the operation has completed. Initially, it's `false`.
- **error:** Will contain error details if the operation fails.
- **response:** Will contain the result of the operation (the signed inception event) once `done` is `true`.
### Waiting for Operation Completion
Since AID inception involves network communication (e.g., with witnesses to gather receipts), it doesn't complete instantly. You need to poll or wait for the operation to finish. The `client.operations().wait()` method handles this, periodically checking with the KERIA agent until the operation's `done` flag becomes `true` or a timeout occurs.
```typescript
// Poll the KERIA agent for the completion of the inception operation.
// AbortSignal.timeout(30000) sets a 30-second timeout for waiting.
console.log('Waiting for inception operation to complete...');
const operationResponse = await client
.operations()
.wait(inceptionOperation, AbortSignal.timeout(30000)); // Pass the operation name
console.log('\nInception Operation Completed:');
console.log(operationResponse);
// The actual inception event is in the 'response' field of the completed operation
const newAidInceptionEvent = operationResponse.response;
console.log(`\nSuccessfully created AID with prefix: ${newAidInceptionEvent.i}`);
console.log(`Witnesses specified: ${JSON.stringify(newAidInceptionEvent.b)}`);
console.log(`Icp op name: ${inceptionOperation.name}`);
const icpOp = await client.operations().get(inceptionOperation.name);
console.log("Inception operation");
console.dir(icpOp);
```
Waiting for inception operation to complete...
Inception Operation Completed:
{
name: "witness.ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF",
metadata: { pre: "ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF", sn: 0 },
done: true,
error: null,
response: {
v: "KERI10JSON0001b7_",
t: "icp",
d: "ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF",
i: "ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF",
s: "0",
kt: "1",
k: [ "DDIdUqYZKNw1tqQsqidDt_IOMQrxsCkjodATHT2-GRcT" ],
nt: "1",
n: [ "EFfw_k3SV0jNDfJaBx40OMw3mPzWqzhisVy9II3L1gU_" ],
bt: "2",
b: [
"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha",
"BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM",
"BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX"
],
c: [],
a: []
}
}
Successfully created AID with prefix: ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF
Witnesses specified: ["BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM","BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX"]
Icp op name: witness.ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF
Inception operation
{
name: "witness.ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF",
metadata: { pre: "ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF", sn: 0 },
done: true,
error: null,
response: {
v: "KERI10JSON0001b7_",
t: "icp",
d: "ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF",
i: "ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF",
s: "0",
kt: "1",
k: [ "DDIdUqYZKNw1tqQsqidDt_IOMQrxsCkjodATHT2-GRcT" ],
nt: "1",
n: [ "EFfw_k3SV0jNDfJaBx40OMw3mPzWqzhisVy9II3L1gU_" ],
bt: "2",
b: [
"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha",
"BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM",
"BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX"
],
c: [],
a: []
}
}
**Completed Operation Output Explained:**
- `done`: Now true, indicating the inception is received on the KERIA agent's side and has been witnessed (receipted and the agent received the receipts).
- `response`: This field now contains the actual signed inception event (`icp`) for the newly created AID (`newAid`) originally submitted by the Client AID.
- `i`: The prefix of the AID now receipted and stored locally in the KERIA agent's database.
- `k`: The list of current public signing keys.
- `n`: The list of digests of the next (pre-rotated) public keys.
- `b`: The list of witness AIDs that this AID is configured to use.
- `bt`: The Threshold of Accountable Duplicity (TOAD) specified during creation (matches toad: 2 from our request).
The KERIA agent has successfully received the AID from the Controller AID, has communicated with witnesses to have the event receipted, and has stored its KEL, starting with the s inception event, in the local agent database.
## Managing Agent Operations
SignifyTS also provides methods to list and delete operations tracked by the KERIA agent for your client. This is useful to show in user interfaces so that the user knows when there are any in-progress operations for one or more managed AIDs.
### Listing Operations
Listing operations is agent-wide meaning all operations for all AIDs on this agent will be returned.
```typescript
// List all current long-running operations for this client
const operationsList = await client.operations().list();
console.log('\nCurrent Operations List:');
console.log(JSON.stringify(operationsList, null, 2));
```
Current Operations List:
[
{
"name": "witness.ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF",
"metadata": {
"pre": "ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF",
"sn": 0
},
"done": true,
"error": null,
"response": {
"v": "KERI10JSON0001b7_",
"t": "icp",
"d": "ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF",
"i": "ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF",
"s": "0",
"kt": "1",
"k": [
"DDIdUqYZKNw1tqQsqidDt_IOMQrxsCkjodATHT2-GRcT"
],
"nt": "1",
"n": [
"EFfw_k3SV0jNDfJaBx40OMw3mPzWqzhisVy9II3L1gU_"
],
"bt": "2",
"b": [
"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha",
"BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM",
"BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX"
],
"c": [],
"a": []
}
}
]
### Get Single Operation
A single operation may be retrieved by name in order to view its state. The name of an operation is formatted as `.` and the example `witness.EF03TKpT68zTvOeFJM4pU64XEonLsZ29rxYFKN8u8AFO` shows that this operation is waiting on a witnessfor the `EF03TKpT68zTvOeFJM4pU64XEonLsZ29rxYFKN8u8AFO` identifier.
```typescript
console.log(`Icp op name: ${inceptionOperation.name}`);
const icpOp = await client.operations().get(inceptionOperation.name);
console.log("Inception operation");
console.dir(icpOp);
```
Icp op name: witness.ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF
Inception operation
{
name: "witness.ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF",
metadata: { pre: "ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF", sn: 0 },
done: true,
error: null,
response: {
v: "KERI10JSON0001b7_",
t: "icp",
d: "ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF",
i: "ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF",
s: "0",
kt: "1",
k: [ "DDIdUqYZKNw1tqQsqidDt_IOMQrxsCkjodATHT2-GRcT" ],
nt: "1",
n: [ "EFfw_k3SV0jNDfJaBx40OMw3mPzWqzhisVy9II3L1gU_" ],
bt: "2",
b: [
"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha",
"BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM",
"BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX"
],
c: [],
a: []
}
}
### Waiting on an Operation
An operation may be waited on to know when an operation completes. Internally the SignifyTS library uses the `setTimeout` built-in along with an `AbortSignal` to control the polling loop that checks with the Signify controller's KERIA agent to determine operation status.
```typescript
// this code sample focuses on operating waiting and is a simple version of what is shown above
const aidAlias = 'waitAidExample';
const icpArgs = {
toad: 1,
wits: ['BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha']
};
const icpRes = await client.identifiers().create(aidAlias, icpArgs);
const icpOp = await icpRes.op();
console.log('Inception Operation Details:');
console.log(inceptionOperation);
// the wait command below
console.log('Waiting for inception operation to complete...');
const operationResponse = await client
.operations()
.wait(icpOp, AbortSignal.timeout(5000)); // Pass the operation name
console.log("Inception operation complete");
```
Inception Operation Details:
{
name: "witness.ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF",
metadata: { pre: "ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF", sn: 0 },
done: false,
error: null,
response: null
}
Waiting for inception operation to complete...
Inception operation complete
### Deleting Operations
As you have seen above old operations stay in the operation list which may or may not be desirable. You may delete operations if you want to clean up the operations list using the Operation delete API as shown below. Run the code as many times as you need in order to clear out the list, running the `.list()` command to verify your operations are being removed from the long-running operations response list.
```typescript
// Delete the completed inception operation (optional cleanup)
const opNameToDelete = operationsList[0].name;
await client.operations().delete(opNameToDelete);
console.log(`\nDeleted operation: ${opNameToDelete}`);
```
Deleted operation: witness.ECmTNMrQYlKin_gqj3kOtN0XwHO5TIYBz1-Gz2tnbixF
Now run the `client.operations().list()` function to see that the operations have been cleared out.
```typescript
// List all current long-running operations for this client
const operationsList = await client.operations().list();
console.log('\nCurrent Operations List:');
console.log(JSON.stringify(operationsList, null, 2));
```
Current Operations List:
[
{
"name": "witness.EGH7tjO0WgOge87vFoghxigky8N4_RLZ_ZC_k5FWAW-F",
"metadata": {
"pre": "EGH7tjO0WgOge87vFoghxigky8N4_RLZ_ZC_k5FWAW-F",
"sn": 0
},
"done": true,
"error": null,
"response": {
"v": "KERI10JSON000159_",
"t": "icp",
"d": "EGH7tjO0WgOge87vFoghxigky8N4_RLZ_ZC_k5FWAW-F",
"i": "EGH7tjO0WgOge87vFoghxigky8N4_RLZ_ZC_k5FWAW-F",
"s": "0",
"kt": "1",
"k": [
"DEdcIv9efB03ts7Hah6tym0XbXaD9P2GwtNluo2DQZdr"
],
"nt": "1",
"n": [
"ECSf4N55OPixAdmKBwEIoZyZwKebtcEPZjxafH7iKX2L"
],
"bt": "1",
"b": [
"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha"
],
"c": [],
"a": []
}
}
]
π SUMMARY
To create a new AID using Signify-ts and a KERIA agent:
- Use
client.identifiers().create(alias, config) to create an inception event locally for a new AID and then send it to the KERIA agent for getting witness receipts and for storing the event and receipts in the agent database. Provide a client-side alias as a human-readable label for the AID and a config object specifying parameters like toad (Threshold of Accountable Duplicity) and wits (list of witness AIDs).
- The
create() method returns an object from which you can get a long-running operation object using .op(). This operation is initially marked as not done.
- Use
client.operations().wait(operationName) to poll the KERIA agent until the operation completes. The resolved object will have
done: true and its response field will contain the signed inception event (icp) of the newly created AID.
- Operations can be listed with
client.operations().list() and deleted with client.operations().delete(operationName).
- Individual operations may be retrieved with
client.operations().get(name).
This process highlights the asynchronous nature of KERIA operations that involve agent-side processing and network interactions.
[<- Prev (KERIA Signify)](102_05_KERIA_Signify.ipynb) | [Next (KERIA Signify Connecting Clients) ->](102_15_KERIA_Signify_Connecting_Clients.ipynb)
# SignifyTS: Securely Connecting Controllers
π― OBJECTIVE
Explain how to establish a secure, mutually authenticated connection between two KERIA/SignifyTS controllers using Out-of-Band Introductions (OOBIs) and the challenge/response protocol to enhance trust.
## Controller and AID Setup
This notebook focuses on connecting two independent controllers using the KERIA/Signify architecture. This involves two `SignifyClient` instances, each managing its own AID, establishing contact (node discovery), and then mutually authenticating each to the other using the challenge signing and verification process. Conceptually, these steps mirror the `kli` process for connecting and verifying controllers yet are executed through the `signify-ts` library interacting with KERIA agents.
You will begin by setting up two distinct `SignifyClient` instances, which we'll call `clientA` (representing a controller Alfred) and `clientB` (representing a controller Betty). Each client will:
1. Generate a unique `bran` (passcode).
2. Instantiate `SignifyClient`.
3. Boot and connect to its KERIA agent, establishing its Client AID and the delegated Agent AID.
4. Create a primary AID (let's call them `aidA` for Alfred and `aidB` for Betty) with a set of predefined witnesses.
The specifics of client creation, booting, connecting, and basic AID inception using `signify-ts` were covered in the "KERIA-Signify Basic Operations" notebook. You will apply those principles below:
```typescript
import { randomPasscode, ready, SignifyClient, Tier } from 'npm:signify-ts';
const url = 'http://keria:3901';
const bootUrl = 'http://keria:3903';
// Inception request parameters
const inceptionArgs = {
toad: 3,
wits: [
'BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha',
'BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM',
'BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX'
]
};
await ready();
console.log("Signify library is ready.")
// ----- Client A (Alfred) -----
const aidAAlias = 'aidA'
const branA = randomPasscode();
const clientA = new SignifyClient(url, branA, Tier.low, bootUrl);
await clientA.boot();
await clientA.connect();
console.log("Agent delegated and ready")
const aInceptionResult = await clientA.identifiers().create(aidAAlias, inceptionArgs);
const aInceptionOperation = await aInceptionResult.op();
const { response: aidA } = await clientA
.operations()
.wait(aInceptionOperation, AbortSignal.timeout(30000));
await clientA.operations().delete(aInceptionOperation.name);
// ----- Client B (Betty) -----
const aidBAlias = 'aidB'
const branB = randomPasscode();
const clientB = new SignifyClient(url, branB, Tier.low, bootUrl);
await clientB.boot();
await clientB.connect();
const bInceptionResult = await clientB.identifiers().create(aidBAlias, inceptionArgs);
const bInceptionOperation = await bInceptionResult.op();
const { response: aidB } = await clientB
.operations()
.wait(bInceptionOperation, AbortSignal.timeout(30000));
await clientB.operations().delete(bInceptionOperation.name);
console.log(`Client A AID Pre: ${aidA.i}\nClient B AID Pre: ${aidB.i}`)
```
Signify library is ready.
Agent delegated and ready
Client A AID Pre: EEf12hYPxR7v5S9IMEMia1v_lzowPBpGUgMr8sRw5DJU
Client B AID Pre: EFr4NDK3M7B28dMWZ0s8drzuuJ_dXg-W5SM1c2fnMXnN
βΉοΈ Note
For this demonstration, both clients will connect to the same KERIA instance (defined by url and bootUrl). In a real-world scenario, Alfred and Betty would likely each have their own Signify clients running on their respective devices and interacting with their own (or chosen) KERIA agent instances. The KERIA agent URLs might be different for each. However, the KERI protocol and Signify patterns for connection and authentication remain the same.
##
## Assigning Agent End Roles
As discussed in "KERIA-Signify Basics", when a `SignifyClient` connects, it establishes a **Client AID** (which you directly control via the `bran`) and a delegated **Agent AID** (managed by the KERIA agent). For these Agent AIDs to act effectively on behalf of the AIDs we just created (`aidA` and `aidB`), we need to explicitly authorize the Agent AID to act in the `agent` role by assigning an `agent` end role to.
The `agent` role, in this context, signifies that the KERIA Agent AID associated with `clientA` is authorized to manage/interact on behalf of `aidA`, and similarly for `clientB` and `aidB`. This is a crucial step for enabling the KERIA agent to perform tasks like sending messages through the agent mailbox and responding to OOBI requests for these specific identifiers.
Use the `client.identifiers().addEndRole()` method to add the role. This method requires:
- The alias of the identifier granting the authorization (e.g., `aidAAlias`).
- The role to be assigned (e.g., `'agent'`).
- The prefix of the AID being authorized for that role. In this case, it's the prefix of the client's own KERIA Agent AID, accessible via `client.agent!.pre`.
```typescript
// ----- Client A: Assign 'agent' role for aidA to its KERIA Agent AID -----
const agentRole = 'agent';
// Authorize clientA's Agent AID to act as an agent for aidA
const aAddRoleResult = await clientA
.identifiers()
.addEndRole(aidAAlias,
agentRole,
clientA!.agent!.pre // clientA.agent.pre is the Agent AID prefix
);
const aAddRoleOperation = await aAddRoleResult.op();
const { response: aAddRoleResponse } = await clientA
.operations()
.wait(aAddRoleOperation, AbortSignal.timeout(30000));
await clientA.operations().delete(aAddRoleOperation.name);
console.log(`Client A: Assigned '${agentRole}' role to KERIA Agent ${clientA.agent!.pre} for AID ${aidA.i}`);
// ----- Client B: Assign 'agent' role for aidB to its KERIA Agent AID -----
// Authorize clientB's Agent AID to act as an agent for aidB
const bAddRoleResult = await clientB
.identifiers()
.addEndRole(aidBAlias,
agentRole,
clientB!.agent!.pre // clientB.agent.pre is the Agent AID prefix
);
const bAddRoleOperation = await bAddRoleResult.op();
const { response: bAddRoleResponse } = await clientB
.operations()
.wait(bAddRoleOperation, AbortSignal.timeout(30000));
await clientB.operations().delete(bAddRoleOperation.name);
console.log(`Client B: Assigned '${agentRole}' role to KERIA Agent ${clientB.agent!.pre} for AID ${aidB.i}`);
```
Client A: Assigned 'agent' role to KERIA Agent EMMKkxBz_28NGq5mmDCzBG7qPVbfNkiO3G25Mh53LI8t for AID EEf12hYPxR7v5S9IMEMia1v_lzowPBpGUgMr8sRw5DJU
Client B: Assigned 'agent' role to KERIA Agent EJMGsMkIMUzLZKLG2xMpfhu00H_vLEcKvqyc3uZJkBIa for AID EFr4NDK3M7B28dMWZ0s8drzuuJ_dXg-W5SM1c2fnMXnN
## Discovery via OOBIs
With the AIDs created and their respective KERIA agents authorized, Alfred (`clientA`, `aidA`) and Betty (`clientB`, `aidB`) need a way to discover each other. This is where Out-of-Band Introductions (OOBIs) are used.
### Generating OOBI URLs
Each client needs to generate an OOBI for its AID (`aidA` and `aidB`). This OOBI is associated with the `agent` role, meaning the OOBI URL (**IURL** for short) will point to an endpoint on their KERIA agent that is authorized to serve information about the AID.
Proceed by generating the IURLs:
- `clientA` generates an OOBI for `aidA` with the role `agent`.
- `clientB` generates an OOBI for `aidB` with the role `agent`.
```typescript
// ----- Generate OOBIs -----
// Client A generates OOBI for aidA (role 'agent')
const oobiA_Result = await clientA.oobis().get(aidAAlias, agentRole);
const oobiA_url = oobiA_Result.oobis[0]; // Assuming at least one OOBI is returned
console.log(`Client A (Alfred) generated OOBI for aidA: ${oobiA_url}`);
// Client B generates OOBI for aidB (role 'agent')
const oobiB_Result = await clientB.oobis().get(aidBAlias, agentRole);
const oobiB_url = oobiB_Result.oobis[0]; // Assuming at least one OOBI is returned
console.log(`Client B (Betty) generated OOBI for aidB: ${oobiB_url}`);
```
Client A (Alfred) generated OOBI for aidA: http://keria:3902/oobi/EEf12hYPxR7v5S9IMEMia1v_lzowPBpGUgMr8sRw5DJU/agent/EMMKkxBz_28NGq5mmDCzBG7qPVbfNkiO3G25Mh53LI8t
Client B (Betty) generated OOBI for aidB: http://keria:3902/oobi/EFr4NDK3M7B28dMWZ0s8drzuuJ_dXg-W5SM1c2fnMXnN/agent/EJMGsMkIMUzLZKLG2xMpfhu00H_vLEcKvqyc3uZJkBIa
### Resolving OOBI URLs
In a real scenario, Alfred would share `oobiA` with Betty, and Betty would share `oobiB` with Alfred through some non-KERI channel (e.g., email, QR code, messaging app). For this notebook, we'll just store them in variables.
Now perform the OOBI resolution. This means `clientA`'s KERIA agent uses the URL in `oobiB` to fetch `aidB`'s KEL from `clientB`'s KERIA agent. `clientA` then cryptographically verifies this KEL. `clientB` resolves `oobiA` similarly.
```typescript
// Client A resolves Client B's OOBI
const contactBAlias = 'Betty_Contact_for_Alfred'; // Alias for clientA to refer to aidB
console.log(`\nClient A (Alfred) attempting to resolve Betty's OOBI...`);
const AResolveOperation = await clientA.oobis().resolve(oobiB_url, contactBAlias);
const AResolveResponse = await clientA
.operations()
.wait(AResolveOperation, AbortSignal.timeout(30000));
await clientA.operations().delete(AResolveOperation.name);
console.log(`Client A resolved Betty's OOBI. Response:`, AResolveResponse.response ? "OK" : "Failed or no response data");
// Client B resolves Client A's OOBI
const contactAAlias = 'Alfred_Contact_for_Betty'; // Alias for clientB to refer to aidA
console.log(`\nClient B (Betty) attempting to resolve Alfred's OOBI...`);
const BResolveOperation = await clientB.oobis().resolve(oobiA_url, contactAAlias);
const BResolveResponse = await clientB
.operations()
.wait(BResolveOperation, AbortSignal.timeout(30000));
await clientB.operations().delete(BResolveOperation.name);
console.log(`Client B resolved Alfred's OOBI. Response:`, BResolveResponse.response ? "OK" : "Failed or no response data");
```
Client A (Alfred) attempting to resolve Betty's OOBI...
Client A resolved Betty's OOBI. Response: OK
Client B (Betty) attempting to resolve Alfred's OOBI...
Client B resolved Alfred's OOBI. Response: OK
### Verifying Resolved Contacts
Upon successful resolution, each client will have added the other's AID to their local contact list. Use `clientA.contacts().list()` to display the contacts:
```typescript
console.log(`\nVerifying contacts...`);
const AContacts = await clientA.contacts().list(undefined, 'alias', contactBAlias);
console.log(AContacts);
const BContacts = await clientB.contacts().list(undefined, 'alias', contactAAlias);
console.log(BContacts);
```
Verifying contacts...
[
{
alias: "Betty_Contact_for_Alfred",
oobi: "http://keria:3902/oobi/EFr4NDK3M7B28dMWZ0s8drzuuJ_dXg-W5SM1c2fnMXnN/agent/EJMGsMkIMUzLZKLG2xMpfhu00H_vLEcKvqyc3uZJkBIa",
id: "EFr4NDK3M7B28dMWZ0s8drzuuJ_dXg-W5SM1c2fnMXnN",
ends: {
agent: {
EJMGsMkIMUzLZKLG2xMpfhu00H_vLEcKvqyc3uZJkBIa: { http: "http://keria:3902/" }
},
witness: {
"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha": {
http: "http://witness-demo:5642/",
tcp: "tcp://witness-demo:5632/"
},
BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM: {
http: "http://witness-demo:5643/",
tcp: "tcp://witness-demo:5633/"
},
"BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX": {
http: "http://witness-demo:5644/",
tcp: "tcp://witness-demo:5634/"
}
}
},
challenges: [],
wellKnowns: []
}
]
[
{
alias: "Alfred_Contact_for_Betty",
oobi: "http://keria:3902/oobi/EEf12hYPxR7v5S9IMEMia1v_lzowPBpGUgMr8sRw5DJU/agent/EMMKkxBz_28NGq5mmDCzBG7qPVbfNkiO3G25Mh53LI8t",
id: "EEf12hYPxR7v5S9IMEMia1v_lzowPBpGUgMr8sRw5DJU",
ends: {
agent: {
EMMKkxBz_28NGq5mmDCzBG7qPVbfNkiO3G25Mh53LI8t: { http: "http://keria:3902/" }
},
witness: {
"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha": {
http: "http://witness-demo:5642/",
tcp: "tcp://witness-demo:5632/"
},
BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM: {
http: "http://witness-demo:5643/",
tcp: "tcp://witness-demo:5633/"
},
"BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX": {
http: "http://witness-demo:5644/",
tcp: "tcp://witness-demo:5634/"
}
}
},
challenges: [],
wellKnowns: []
}
]
## Mutual Authentication with Challenge-Response
Successfully resolving an OOBI means you've retrieved and cryptographically verified the KEL of the target AID. This establishes the authenticity and integrity of the AID's key history.
However, it does not, by itself, prove conclusively that the entity you are currently communicating with over the network (the one that provided the OOBI or is responding via the OOBI's endpoint) is the legitimate controller of that AID's private keys. A liveness test is needed to prove that the controllers of each AID are actually in control of each respective AID.
This is why the **Challenge-Response** protocol is critical for establishing authenticated control. It serves as that liveness test.
The process, as described in the "Connecting Controllers" notebook for `kli`, is as follows for each pair (e.g., Alfred challenging Betty):
1. **Generate Challenge**: Alfred (`clientA`) generates a set of unique challenge words.
2. **Send Challenge (Simulated OOB)**: Alfred communicates these words to Betty through an out-of-band channel (e.g., verbally, secure message). This step is crucial to prevent a Man-in-the-Middle (MITM) on the main KERI connection from intercepting or altering the challenge. For this notebook, we'll print the words.
3. **Respond to Challenge**: Betty (`clientB`), using `aidB`, signs the exact challenge words received from Alfred. The `respond()` method sends this signed response to Alfred's KERIA agent.
4. **Verify Response**: Alfred (`clientA`) receives the signed response. His KERIA agent verifies that the signature corresponds to `aidB`'s current authoritative keys (from the KEL he resolved earlier) and that the signed message matches the original challenge words. This is an asynchronous operation.
5. **Mark as Responded/Authenticated**: If verification is successful, Alfred (`clientA`) marks the challenge for `aidB` as successfully responded to and authenticated. This updates the contact information for Betty in Alfred's client.
This process is then repeated with Betty challenging Alfred.
### Generating Challenge Phrases
Generate a set of random words for each client. `signify-ts` uses `client.challenges().generate()` for this. The strength of the challenge can be specified by the bit length (e.g., 128 or 256 bits, which translates to a certain number of words).
```typescript
// ----- Generate Challenge Words -----
// Client A (Alfred) generates challenge words for Betty
const challengeWordsA = await clientA.challenges().generate(128); // 128-bit strength
console.log("Client A's challenge words for Betty:", challengeWordsA.words);
// Client B (Betty) generates challenge words for Alfred
const challengeWordsB = await clientB.challenges().generate(128); // 128-bit strength
console.log("Client B's challenge words for Alfred:", challengeWordsB.words);
```
Client A's challenge words for Betty: [
"ghost", "color",
"mandate", "nephew",
"cook", "small",
"myth", "door",
"lake", "turkey",
"spare", "what"
]
Client B's challenge words for Alfred: [
"stable", "salt",
"diesel", "police",
"cancel", "sell",
"portion", "twin",
"enjoy", "appear",
"field", "crystal"
]
### Performing the Challenge-Response Protocol
Perform the following sequence of steps to simulate the challenge/respond protocol.
Assume Alfred has securely (out-of-band) communicated `challengeWordsA.words` to Betty.
- Betty will now use `clientB.challenges().respond()` to sign these words with `aidB` and send the response to `aidA`.
- Alfred will then use `clientA.challenges().verify()` to verify Betty's response. This verification is an operation that needs to be polled.
- Finally, Alfred uses `clientA.challenges().responded()` to mark the contact as authenticated.
```typescript
// ----- Betty (Client B) responds to Alfred's (Client A) challenge -----
console.log(`\nBetty (aidB: ${aidB.i}) responding to Alfred's (aidA: ${aidA.i}) challenge...`);
// Betty uses aidBAlias to sign, targeting aidA.i with challengeWordsA.words
await clientB.challenges().respond(aidBAlias, aidA.i, challengeWordsA.words);
console.log("Betty's response sent.");
// ----- Alfred (Client A) verifies Betty's (Client B) response -----
console.log(`\nAlfred (aidA) verifying Betty's (aidB) response...`);
// Alfred verifies the response allegedly from aidB.i using challengeWordsA.words
const AVerifyBOperation = await clientA.challenges().verify(aidB.i, challengeWordsA.words);
const { response: AVerifyBResponseDetails } = await clientA
.operations()
.wait(AVerifyBOperation, AbortSignal.timeout(30000));
await clientA.operations().delete(AVerifyBOperation.name);
const exnSaidB = AVerifyBResponseDetails.exn.d;
console.log("Alfred: Betty's response verified. SAID of exn:", exnSaidB);
// Alfred marks the challenge for Betty (aidB.i) as successfully responded
await clientA.challenges().responded(aidB.i, exnSaidB);
console.log("Alfred: Marked Betty's contact as authenticated.");
// Check Alfred's contact list for Betty's authenticated status
const AContactsAfterAuth = await clientA.contacts().list(undefined, 'alias', contactBAlias);
console.log(AContactsAfterAuth)
```
Betty (aidB: EFr4NDK3M7B28dMWZ0s8drzuuJ_dXg-W5SM1c2fnMXnN) responding to Alfred's (aidA: EEf12hYPxR7v5S9IMEMia1v_lzowPBpGUgMr8sRw5DJU) challenge...
Betty's response sent.
Alfred (aidA) verifying Betty's (aidB) response...
Alfred: Betty's response verified. SAID of exn: EGaovC5tH_MqxM8E8-lOnp6wD-bw1kO-zqJHxogUUzx7
Alfred: Marked Betty's contact as authenticated.
[
{
alias: "Betty_Contact_for_Alfred",
oobi: "http://keria:3902/oobi/EFr4NDK3M7B28dMWZ0s8drzuuJ_dXg-W5SM1c2fnMXnN/agent/EJMGsMkIMUzLZKLG2xMpfhu00H_vLEcKvqyc3uZJkBIa",
id: "EFr4NDK3M7B28dMWZ0s8drzuuJ_dXg-W5SM1c2fnMXnN",
ends: {
agent: {
EJMGsMkIMUzLZKLG2xMpfhu00H_vLEcKvqyc3uZJkBIa: { http: "http://keria:3902/" }
},
witness: {
"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha": {
http: "http://witness-demo:5642/",
tcp: "tcp://witness-demo:5632/"
},
BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM: {
http: "http://witness-demo:5643/",
tcp: "tcp://witness-demo:5633/"
},
"BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX": {
http: "http://witness-demo:5644/",
tcp: "tcp://witness-demo:5634/"
}
}
},
challenges: [
{
dt: "2025-09-12T04:11:28.618000+00:00",
words: [
"ghost", "color",
"mandate", "nephew",
"cook", "small",
"myth", "door",
"lake", "turkey",
"spare", "what"
],
said: "EGaovC5tH_MqxM8E8-lOnp6wD-bw1kO-zqJHxogUUzx7",
authenticated: true
}
],
wellKnowns: []
}
]
Now, the roles reverse. Assume Betty (Client B) has securely (out-of-band) communicated `challengeWordsB.words` to Alfred (Client A).
Alfred will use `clientA.challenges().respond()` to sign these words with `aidA` and send the response to `aidB`.
Betty will then use `clientB.challenges().verify()` to verify Alfred's response and `clientB.challenges().responded()` to mark the contact.
```typescript
// ----- Alfred (Client A) responds to Betty's (Client B) challenge -----
console.log(`\nAlfred (aidA: ${aidA.i}) responding to Betty's (aidB: ${aidB.i}) challenge...`);
// Alfred uses aidAAlias to sign, targeting aidB.i with challengeWordsB.words
await clientA.challenges().respond(aidAAlias, aidB.i, challengeWordsB.words);
console.log("Alfred's response sent.");
// ----- Betty (Client B) verifies Alfred's (Client A) response -----
console.log(`\nBetty (aidB) verifying Alfred's (aidA) response...`);
// Betty verifies the response allegedly from aidA.i using challengeWordsB.words
const BVerifyAOperation = await clientB.challenges().verify(aidA.i, challengeWordsB.words);
const { response: BVerifyAResponseDetails } = await clientB
.operations()
.wait(BVerifyAOperation, AbortSignal.timeout(30000));
await clientB.operations().delete(BVerifyAOperation.name);
const exnSaidA = BVerifyAResponseDetails.exn.d;
console.log("Betty: Alfred's response verified. SAID of exn:", exnSaidA);
// Betty marks the challenge for Alfred (aidA.i) as successfully responded
await clientB.challenges().responded(aidA.i, exnSaidA);
console.log("Betty: Marked Alfred's contact as authenticated.");
// Check Betty's contact list for Alfred's authenticated status
const BContactsAfterAuth = await clientB.contacts().list(undefined, 'alias', contactAAlias);
console.log(BContactsAfterAuth);
```
Alfred (aidA: EEf12hYPxR7v5S9IMEMia1v_lzowPBpGUgMr8sRw5DJU) responding to Betty's (aidB: EFr4NDK3M7B28dMWZ0s8drzuuJ_dXg-W5SM1c2fnMXnN) challenge...
Alfred's response sent.
Betty (aidB) verifying Alfred's (aidA) response...
Betty: Alfred's response verified. SAID of exn: EGuHjc2voHRKPHwKRFO4H8wygZNBCHEpv3lMT2QfsxKX
Betty: Marked Alfred's contact as authenticated.
[
{
alias: "Alfred_Contact_for_Betty",
oobi: "http://keria:3902/oobi/EEf12hYPxR7v5S9IMEMia1v_lzowPBpGUgMr8sRw5DJU/agent/EMMKkxBz_28NGq5mmDCzBG7qPVbfNkiO3G25Mh53LI8t",
id: "EEf12hYPxR7v5S9IMEMia1v_lzowPBpGUgMr8sRw5DJU",
ends: {
agent: {
EMMKkxBz_28NGq5mmDCzBG7qPVbfNkiO3G25Mh53LI8t: { http: "http://keria:3902/" }
},
witness: {
"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha": {
http: "http://witness-demo:5642/",
tcp: "tcp://witness-demo:5632/"
},
BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM: {
http: "http://witness-demo:5643/",
tcp: "tcp://witness-demo:5633/"
},
"BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX": {
http: "http://witness-demo:5644/",
tcp: "tcp://witness-demo:5634/"
}
}
},
challenges: [
{
dt: "2025-09-12T04:11:29.215000+00:00",
words: [
"stable", "salt",
"diesel", "police",
"cancel", "sell",
"portion", "twin",
"enjoy", "appear",
"field", "crystal"
],
said: "EGuHjc2voHRKPHwKRFO4H8wygZNBCHEpv3lMT2QfsxKX",
authenticated: true
}
],
wellKnowns: []
}
]
If both challenge-response cycles complete successfully, Alfred and Betty have now established a mutually authenticated connection. This provides a strong foundation of trust for subsequent interactions, such as exchanging verifiable credentials.
π SUMMARY
This notebook demonstrated the process of connecting two KERIA/Signify controllers, Alfred (
clientA) and Betty (
clientB):
- Initial Setup: Each client was initialized, booted its KERIA agent, connected, and created an Autonomic Identifier(
aidA for Alfred, aidB for Betty).
- End Role Assignment: The KERIA Agent AID for each client was authorized with an
agent end role for its respective AID (aidA and aidB). This allows the KERIA agent to manage these AIDs, such as serving their KELs via OOBIs. This was done using client.identifiers().addEndRole().
- OOBI Generation & Resolution:
- Each client generated an OOBI URL for its AID, specifically for the
'agent' role, using client.oobis().get(alias, 'agent'). This OOBI points to their KERIA agent's endpoint for that AID.
- The OOBIs were (simulated) exchanged out-of-band.
- Each client then resolved the other's OOBI using
client.oobis().resolve(). This retrieved and cryptographically verified the other's KEL, adding them to their local contact list.
- Challenge-Response Protocol for Mutual Authentication:
- Each client generated unique challenge words using
client.challenges().generate().
- These words were (conceptually) exchanged out-of-band.
- Cycle 1 (Betty responds to Alfred):
- Betty signed Alfred's challenge words with
aidB using clientB.challenges().respond().
- Alfred verified Betty's signed response against
aidB's known keys using clientA.challenges().verify().
- Upon successful verification, Alfred marked Betty's contact as authenticated using
clientA.challenges().responded().
- Cycle 2 (Alfred responds to Betty): The same process was repeated with Alfred responding to Betty's challenge.
Successful completion of both OOBI resolution and the mutual challenge-response protocol establishes a high degree of trust. Both controllers have verified each other's identity (KEL) and cryptographically confirmed that the other party has active control of their private keys. The
challengesAuthenticated flag in their contact lists for each other should now be true.
[<- Prev (KERIA Signify Basic Operations)](102_10_KERIA_Signify_Basic_Operations.ipynb) | [Next (KERIA Signify Key Rotation) ->](102_17_KERIA_Signify_Key_Rotation.ipynb)
# Signify TS: Key Rotation
π― OBJECTIVE
This notebook demonstrates how to perform a single-signature key rotation for an Autonomic Identifier (AID) using the Signify TS library.
## Introduction to Key Rotation with Signify TS
Key rotation is a fundamental security practice in KERI. It involves changing the cryptographic keys associated with an AID while preserving the identifier itself. This allows an identity to remain stable and persistent over time, even as its underlying keys are updated for security reasons (e.g., to mitigate key compromise or to upgrade cryptographic algorithms).
In the KERIA/Signify architecture, the client (your application using Signify TS) initiates and signs the rotation event. The KERIA agent then handles the dissemination of this event to witnesses and makes it available to others. This notebook illustrates the end-to-end process, showing how a rotation is performed by one client and observed by another.
## Controller and AID Setup
First, we set up the environment for our demonstration. This involves:
- Two `SignifyClient` instances:
- `clientA` will act as the controller of the AID whose keys we will rotate.
- `clientB` will act as a remote agent who knows about the AID and will track its key state changes.
- AID Creation: `clientA` creates a new AID (`aidA`) that is transferable (i.e., its keys can be rotated).
- OOBI Resolution: `clientB` resolves an Out-of-Band Introduction (OOBI) for `aidA` to establish contact and retrieve its initial Key Event Log (KEL).
βΉοΈ NOTE
This section utilizes utility functions (from ./scripts_ts/utils.ts) to quickly establish the necessary preconditions for the key rotation demonstration. The detailed steps for client initialization, AID creation, and OOBI resolution are covered in previous notebooks.
```typescript
import { randomPasscode, RotateIdentifierArgs, SignifyClient} from 'npm:signify-ts';
import {
initializeAndConnectClient,
createNewAID,
addEndRoleForAID,
generateOOBI,
resolveOOBI,
DEFAULT_IDENTIFIER_ARGS,
DEFAULT_TIMEOUT_MS,
ROLE_AGENT,
} from './scripts_ts/utils.ts';
// clientA Client Setup
const clientABran = randomPasscode()
const clientAAidAlias = 'aidA'
const { client: clientA } = await initializeAndConnectClient(clientABran)
const { aid: aidA } = await createNewAID(clientA, clientAAidAlias, DEFAULT_IDENTIFIER_ARGS);
await addEndRoleForAID(clientA, clientAAidAlias, ROLE_AGENT);
const clientAOOBI = await generateOOBI(clientA, clientAAidAlias, ROLE_AGENT);
// clientB Client Setup and OOBI Resolution
const clientBBran = randomPasscode()
const { client: clientB } = await initializeAndConnectClient(clientBBran)
await resolveOOBI(clientB, clientAOOBI, clientAAidAlias);
console.log("Client and AID setup complete.");
console.log(`Client A created AID: ${aidA.i}`);
console.log(`Client B resolved OOBI for AID: ${aidA.i}`);
```
Using Passcode (bran): CmftjcfYKRyzKJ4aZ3MzY
Client boot process initiated with KERIA agent.
Client AID Prefix: ECd0LdwLacFRO80n_hISu-xsEWotRPjA39dc4JvULXPu
Agent AID Prefix: ENYBj8F8aZnK7qWjChBVfwAUzfEnFrlrpAF0PIxXG2z1
Initiating AID inception for alias: aidA
Successfully created AID with prefix: EEr8A5QFqPnxh3aKLaOf0V4DUBNGS0xa8Hhav1uNJYSm
Assigning 'agent' role to KERIA Agent ENYBj8F8aZnK7qWjChBVfwAUzfEnFrlrpAF0PIxXG2z1 for AID alias aidA
Successfully assigned 'agent' role for AID alias aidA.
Generating OOBI for AID alias aidA with role agent
Generated OOBI URL: http://keria:3902/oobi/EEr8A5QFqPnxh3aKLaOf0V4DUBNGS0xa8Hhav1uNJYSm/agent/ENYBj8F8aZnK7qWjChBVfwAUzfEnFrlrpAF0PIxXG2z1
Using Passcode (bran): D9DpJ4BZ8eKdzVh5l97sb
Client boot process initiated with KERIA agent.
Client AID Prefix: EOHevvKKwEbKz0tOQQur52mrHwZAkrifXo8dhbiylI7T
Agent AID Prefix: EFdoky6hnR7lJrbBCbSPRLccuvoQwPFwjcO7ZWZWgvkR
Resolving OOBI URL: http://keria:3902/oobi/EEr8A5QFqPnxh3aKLaOf0V4DUBNGS0xa8Hhav1uNJYSm/agent/ENYBj8F8aZnK7qWjChBVfwAUzfEnFrlrpAF0PIxXG2z1 with alias aidA
Successfully resolved OOBI URL. Response: OK
Contact "aidA" added/updated.
Client and AID setup complete.
Client A created AID: EEr8A5QFqPnxh3aKLaOf0V4DUBNGS0xa8Hhav1uNJYSm
Client B resolved OOBI for AID: EEr8A5QFqPnxh3aKLaOf0V4DUBNGS0xa8Hhav1uNJYSm
## Initial State Verification
Before performing the rotation, let's verify that both `clientA` (the controller) and `clientB` (the observer) have a consistent view of the AID's key state. We can do this by fetching the key state from each client and comparing their sequence numbers (`s`).
The `client.keyStates().get()` method retrieves the key state for a given AID prefix from the client's local KEL copy.
```typescript
// Get the key state from the local client (clientA)
let keystateA_before = (await clientA.keyStates().get(aidA.i))[0];
// Get the key state from the remote observer client (clientB)
let keystateB_before = (await clientB.keyStates().get(aidA.i))[0];
// Compare the sequence numbers to ensure they are synchronized
console.log("Initial sequence number for clientA:", keystateA_before.s);
console.log("Initial sequence number for clientB:", keystateB_before.s);
console.log("Are keystates initially in sync?", keystateA_before.s === keystateB_before.s);
```
Initial sequence number for clientA: 0
Initial sequence number for clientB: 0
Are keystates initially in sync? true
## The Key Rotation Process
Now, we'll proceed with the core steps of rotating the keys for `aidA`.
### Step 1: Perform the Rotation
The controller, `clientA`, initiates the key rotation using the `client.identifiers().rotate()` method. This method creates and signs a rotation (`rot`) event.
- `clientAAidAlias`: The alias of the identifier to rotate.
- `args`: A `RotateIdentifierArgs` object. For a simple rotation, this can be an empty object {}. It can also be used to specify changes to witnesses or other configuration during the rotation. **[see here for more details](https://weboftrust.github.io/signify-ts/interfaces/RotateIdentifierArgs.html)**
The default for rotating a single signature identifier with Signify TS is to create only one new key. More keys can be created by specifying additional configuration properties in a `RotateIdentifierArgs` object.
Like other establishment events in Signify TS, this is an asynchronous operation. The method returns a promise that resolves to an operation object, which we then wait on to confirm completion.
```typescript
// Define arguments for the rotation. For a standard rotation, this can be empty.
const args: RotateIdentifierArgs = {};
// Initiate the rotation operation
const rotateResult = await clientA
.identifiers()
.rotate(clientAAidAlias, args);
// Get the long-running operation details
const rotateOperation = await rotateResult.op();
// Wait for the rotation operation to complete on the KERIA agent
const rotateOperationResponse = await clientA
.operations()
.wait(rotateOperation, AbortSignal.timeout(DEFAULT_TIMEOUT_MS));
console.log("Key rotation operation completed successfully.");
```
Key rotation operation completed successfully.
### Step 2 Local verification
After the rotation operation completes, `clientA`'s local state for `aidA` should be immediately updated. We can verify this by fetching the key state again and observing the changes:
- The sequence number (`s`) should have incremented by 1.
- The list of current public keys (`k`) should be different.
- The digest of the next pre-rotated keys (`n`) should also be different, as a new set of future keys has been committed to.
```typescript
// Get the updated key state from the local client (clientA)
let keystateA_after = (await clientA.keyStates().get(aidA.i))[0];
console.log("--- Key State After Rotation (Local Verification) ---");
console.log("Previous sequence number:", keystateA_before.s);
console.log("New sequence number: ", keystateA_after.s);
console.log("\nPrevious keys:", keystateA_before.k);
console.log("New keys: ", keystateA_after.k);
console.log("\nPrevious next-key digest:", keystateA_before.n);
console.log("New next-key digest: ", keystateA_after.n);
```
--- Key State After Rotation (Local Verification) ---
Previous sequence number: 0
New sequence number: 1
Previous keys: [ "DBV5pRtXng5HUKL2q94Tw_wJieYIQN6esqfu2aq0l0yt" ]
New keys: [ "DAOuHbbGKFl5ClMDRlVeg_R8pBwEx72WL-v8iJBxJplH" ]
Previous next-key digest: [ "ED0BGBmwxUQGvJeG5vpGxwzGgag94s-FPgBhub8eD_G-" ]
New next-key digest: [ "EHo5FKa33tpMQ6wzYFF_yZ3UJVfq0YPP9YS1PWlimdse" ]
### Step 3: Remote Synchronization and Verification
At this point, the remote observer, `clientB`, is not yet aware of the rotation. Its local copy of the KEL for `aidA` is now outdated.
```typescript
// Get the key state from the remote observer again
let keystateB_stale = (await clientB.keyStates().get(aidA.i))[0];
console.log("--- Remote Observer State (Before Synchronization) ---");
console.log("Local controller's sequence number:", keystateA_after.s);
console.log("Remote observer's sequence number: ", keystateB_stale.s);
console.log("Are keystates in sync now?", keystateA_after.s === keystateB_stale.s);
```
--- Remote Observer State (Before Synchronization) ---
Local controller's sequence number: 1
Remote observer's sequence number: 0
Are keystates in sync now? false
To synchronize, `clientB` must query for the latest state of the AID's KEL. The `client.keyStates().query()` method is used for this purpose. It tells the client's KERIA agent to check the witnesses of the specified AID for any new events.
```typescript
// clientB queries for the latest key state of aidA from its witnesses
let queryOperation = await clientB
.keyStates()
.query(aidA.i, keystateA_after.s); // We can optionally specify the sequence number we expect to find
// Wait for the query operation to complete
const queryOperationResponse = await clientB
.operations()
.wait(queryOperation, AbortSignal.timeout(DEFAULT_TIMEOUT_MS));
console.log("\nRemote observer has queried for updates.");
// Now, get the key state from the remote observer again
let keystateB_synced = (await clientB.keyStates().get(aidA.i))[0];
console.log("\n--- Remote Observer State (After Synchronization) ---");
console.log("Local controller's sequence number:", keystateA_after.s);
console.log("Remote observer's sequence number: ", keystateB_synced.s);
console.log("Are keystates in sync now?", keystateA_after.s === keystateB_synced.s);
```
Remote observer has queried for updates.
--- Remote Observer State (After Synchronization) ---
Local controller's sequence number: 1
Remote observer's sequence number: 1
Are keystates in sync now? true
After the query, `clientB` has processed the `rot` event and its local key state for `aidA` is now consistent with `clientA`'s state. This demonstrates how KERI's distributed infrastructure maintains consistency across multiple parties.
π SUMMARY
This notebook demonstrated the key rotation process for a single-signature AID using Signify TS:
- Initiation: The controller of an AID (
clientA) uses client.identifiers().rotate() to create and sign a rotation (rot) event. This event is sent to the KERIA agent and getting receipts on this rotation event is an asynchronous operation that is managed by the KERIA agent.
- Local Verification: After the rotation operation completes, the controller's local key state is immediately updated. This is confirmed by observing an incremented sequence number (
s), a new set of current keys (k), and a new pre-rotation commitment for the next keys (n).
- Remote Synchronization: A remote agent (
clientB) does not automatically see the rotation. They must explicitly query for the latest key state using client.keyStates().query(). This action prompts their KERIA agent to check the AID's witnesses for new events.
- Consistency: After a successful query, the remote agent's local KEL is updated, and their view of the AID's key state becomes consistent with the controller's view.
This process validates KERI's core principles of forward security (old keys are retired) and distributed consistency, ensuring all parties can maintain a synchronized and verifiable view of an identity's evolution.
[<- Prev (KERIA Signify Connecting Clients)](102_15_KERIA_Signify_Connecting_Clients.ipynb) | [Next (KERIA Signify Credential Issuance) ->](102_20_KERIA_Signify_Credential_Issuance.ipynb)
# Signify TS: ACDC Credential Issuance with IPEX
π― OBJECTIVE
Demonstrate the process of issuing an ACDC (Authentic Chained Data Container) from an Issuer to a Holder using the Issuance and Presentation Exchange (IPEX) protocol with the Signify TS library.
βΉοΈ NOTE
This section utilizes utility functions (from ./scripts_ts/utils.ts) to quickly establish the necessary preconditions for credential issuance. The detailed steps for client initialization, AID creation, end role assignment, and OOBI resolution were covered in the "KERIA-Signify Connecting Controllers" notebook. Here, we provide a high-level recap of what these utility functions accomplish.
## Prerequisites: Client and AID Setup
The setup process, streamlined by the utility functions, performs the following key actions:
* **Signify Library Initialization**: Ensures the underlying cryptographic components (libsodium) of Signify TS are ready.
* **Client Initialization & Connection**: Three `SignifyClient` instances are createdβone each for an Issuer, a Holder, and a Verifier. Each client is bootstrapped and connected to its KERIA agent.
* **AID Creation**: Each client (Issuer, Holder, Verifier) creates a primary AID using default arguments.
* **End Role Assignment**: An `agent` end role is assigned to each client's KERIA Agent AID.
* **OOBI Generation and Resolution (Client-to-Client)**:
* OOBIs are generated for the Issuer, Holder, and Verifier AIDs, specifically for the `'agent'` role.
* Communication channels are established by resolving these OOBIs:
* Issuer's client resolves the Holder's OOBI.
* Holder's client resolves the Issuer's OOBI.
* Verifier's client resolves the Holder's OOBI.
* Holder's client resolves the Verifier's OOBI.
* **Schema OOBI Resolution**: The Issuer, Holder, and Verifier clients all resolve the OOBI for the "EventPass" schema (SAID: `EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK`). This schema is hosted on the schema server (vLEI-Server in this context). Resolving the schema OOBI ensures all parties have the correct and verifiable schema definition necessary to understand and validate the credential.
The code block below executes this setup.
```typescript
import { randomPasscode, Serder} from 'npm:signify-ts';
import { initializeSignify,
initializeAndConnectClient,
createNewAID,
addEndRoleForAID,
generateOOBI,
resolveOOBI,
createTimestamp,
DEFAULT_IDENTIFIER_ARGS,
DEFAULT_TIMEOUT_MS,
DEFAULT_DELAY_MS,
DEFAULT_RETRIES,
ROLE_AGENT,
IPEX_GRANT_ROUTE,
IPEX_ADMIT_ROUTE,
IPEX_APPLY_ROUTE,
IPEX_OFFER_ROUTE,
SCHEMA_SERVER_HOST
} from './scripts_ts/utils.ts';
// Clients setup
// Initialize Issuer, Holder and Verifier CLients, Create AIDs for each one, assign 'agent' role to the AIDs
// generate and resolve OOBIs
// Issuer Client
const issuerBran = randomPasscode()
const issuerAidAlias = 'issuerAid'
const { client: issuerClient } = await initializeAndConnectClient(issuerBran)
const { aid: issuerAid} = await createNewAID(issuerClient, issuerAidAlias, DEFAULT_IDENTIFIER_ARGS);
await addEndRoleForAID(issuerClient, issuerAidAlias, ROLE_AGENT);
const issuerOOBI = await generateOOBI(issuerClient, issuerAidAlias, ROLE_AGENT);
// Holder Client
const holderBran = randomPasscode()
const holderAidAlias = 'holderAid'
const { client: holderClient } = await initializeAndConnectClient(holderBran)
const { aid: holderAid} = await createNewAID(holderClient, holderAidAlias, DEFAULT_IDENTIFIER_ARGS);
await addEndRoleForAID(holderClient, holderAidAlias, ROLE_AGENT);
const holderOOBI = await generateOOBI(holderClient, holderAidAlias, ROLE_AGENT);
// Verifier Client
const verifierBran = randomPasscode()
const verifierAidAlias = 'verifierAid'
const { client: verifierClient } = await initializeAndConnectClient(verifierBran)
const { aid: verifierAid} = await createNewAID(verifierClient, verifierAidAlias, DEFAULT_IDENTIFIER_ARGS);
await addEndRoleForAID(verifierClient, verifierAidAlias, ROLE_AGENT);
const verifierOOBI = await generateOOBI(verifierClient, verifierAidAlias, ROLE_AGENT);
// Clients OOBI Resolution
// Resolve OOBIs to establish connections Issuer-Holder, Holder-Verifier
const issuerContactAlias = 'issuerContact';
const holderContactAlias = 'holderContact';
const verifierContactAlias = 'verifierContact';
await resolveOOBI(issuerClient, holderOOBI, holderContactAlias);
await resolveOOBI(holderClient, issuerOOBI, issuerContactAlias);
await resolveOOBI(verifierClient, holderOOBI, holderContactAlias);
await resolveOOBI(holderClient, verifierOOBI, verifierContactAlias);
// Schemas OOBI Resolution
// Resolve the Schemas from the Schema Server (VLEI-Server)
const schemaContactAlias = 'schemaContact';
const schemaSaid = 'EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK';
const schemaOOBI = `http://vlei-server:7723/oobi/${schemaSaid}`;
await resolveOOBI(issuerClient, schemaOOBI, schemaContactAlias);
await resolveOOBI(holderClient, schemaOOBI, schemaContactAlias);
await resolveOOBI(verifierClient, schemaOOBI, schemaContactAlias);
console.log("Client setup and OOBI resolutions complete.");
```
Using Passcode (bran): Ac0VLfHjkRBMlRQkIQWAV
Client boot process initiated with KERIA agent.
Client AID Prefix: EKclu_Wr9uNw3BqUvvR5OEE_GjfzQpJ-xxS-ToY15gZT
Agent AID Prefix: EEWh2NfpcqHjv2U68ykPSmybjMsndNfAoP_zXr3BZi3w
Initiating AID inception for alias: issuerAid
Successfully created AID with prefix: EFXyxqw8Oqh64Jqt59tG4b8uUjqdhxv6ItG07p8V-bFn
Assigning 'agent' role to KERIA Agent EEWh2NfpcqHjv2U68ykPSmybjMsndNfAoP_zXr3BZi3w for AID alias issuerAid
Successfully assigned 'agent' role for AID alias issuerAid.
Generating OOBI for AID alias issuerAid with role agent
Generated OOBI URL: http://keria:3902/oobi/EFXyxqw8Oqh64Jqt59tG4b8uUjqdhxv6ItG07p8V-bFn/agent/EEWh2NfpcqHjv2U68ykPSmybjMsndNfAoP_zXr3BZi3w
Using Passcode (bran): BhNxnLmtDhcga_1-SjgtZ
Client boot process initiated with KERIA agent.
Client AID Prefix: EMFteo1Eh4miqjX_EcXmZJtGOfVd3Trgw9r_OePEp69S
Agent AID Prefix: EIMjeKJ1fkrzmm2kkOzIOm_XUGxRXJux_fD8VdZCMkuF
Initiating AID inception for alias: holderAid
Successfully created AID with prefix: EAjFdCtYoYlHt_1I895vGQI65A2-0u98S4LmY-f7GK6b
Assigning 'agent' role to KERIA Agent EIMjeKJ1fkrzmm2kkOzIOm_XUGxRXJux_fD8VdZCMkuF for AID alias holderAid
Successfully assigned 'agent' role for AID alias holderAid.
Generating OOBI for AID alias holderAid with role agent
Generated OOBI URL: http://keria:3902/oobi/EAjFdCtYoYlHt_1I895vGQI65A2-0u98S4LmY-f7GK6b/agent/EIMjeKJ1fkrzmm2kkOzIOm_XUGxRXJux_fD8VdZCMkuF
Using Passcode (bran): AGr7S8ga8YiZvaOlAKXJW
Client boot process initiated with KERIA agent.
Client AID Prefix: ED0CF-hKXVrEnZKu3DlqlwlUITmL-xOfSR50jAk14J-m
Agent AID Prefix: EOcEindhzgAZ6S04H0idh5eyaquDYLWTQ3TwHVbO5jHA
Initiating AID inception for alias: verifierAid
Successfully created AID with prefix: EG4L5LQix-B0-yA7u1FyKHIHKPeLGGmiAcxgblxJjeUT
Assigning 'agent' role to KERIA Agent EOcEindhzgAZ6S04H0idh5eyaquDYLWTQ3TwHVbO5jHA for AID alias verifierAid
Successfully assigned 'agent' role for AID alias verifierAid.
Generating OOBI for AID alias verifierAid with role agent
Generated OOBI URL: http://keria:3902/oobi/EG4L5LQix-B0-yA7u1FyKHIHKPeLGGmiAcxgblxJjeUT/agent/EOcEindhzgAZ6S04H0idh5eyaquDYLWTQ3TwHVbO5jHA
Resolving OOBI URL: http://keria:3902/oobi/EAjFdCtYoYlHt_1I895vGQI65A2-0u98S4LmY-f7GK6b/agent/EIMjeKJ1fkrzmm2kkOzIOm_XUGxRXJux_fD8VdZCMkuF with alias holderContact
Successfully resolved OOBI URL. Response: OK
Contact "holderContact" added/updated.
Resolving OOBI URL: http://keria:3902/oobi/EFXyxqw8Oqh64Jqt59tG4b8uUjqdhxv6ItG07p8V-bFn/agent/EEWh2NfpcqHjv2U68ykPSmybjMsndNfAoP_zXr3BZi3w with alias issuerContact
Successfully resolved OOBI URL. Response: OK
Contact "issuerContact" added/updated.
Resolving OOBI URL: http://keria:3902/oobi/EAjFdCtYoYlHt_1I895vGQI65A2-0u98S4LmY-f7GK6b/agent/EIMjeKJ1fkrzmm2kkOzIOm_XUGxRXJux_fD8VdZCMkuF with alias holderContact
Successfully resolved OOBI URL. Response: OK
Contact "holderContact" added/updated.
Resolving OOBI URL: http://keria:3902/oobi/EG4L5LQix-B0-yA7u1FyKHIHKPeLGGmiAcxgblxJjeUT/agent/EOcEindhzgAZ6S04H0idh5eyaquDYLWTQ3TwHVbO5jHA with alias verifierContact
Successfully resolved OOBI URL. Response: OK
Contact "verifierContact" added/updated.
Resolving OOBI URL: http://vlei-server:7723/oobi/EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK with alias schemaContact
Successfully resolved OOBI URL. Response: OK
Contact "schemaContact" added/updated.
Resolving OOBI URL: http://vlei-server:7723/oobi/EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK with alias schemaContact
Successfully resolved OOBI URL. Response: OK
Contact "schemaContact" added/updated.
Resolving OOBI URL: http://vlei-server:7723/oobi/EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK with alias schemaContact
Successfully resolved OOBI URL. Response: OK
Contact "schemaContact" added/updated.
Client setup and OOBI resolutions complete.
## Credential Issuance Workflow Steps
With the clients set up and connected, you can proceed with the credential issuance workflow. This involves the Issuer creating a credential and transferring it to the Holder using the IPEX protocol. Below are the code snippets you need to follow to do the issuance.
### Step 1: Create Issuer's Credential Registry
Before an Issuer can issue credentials, it needs a Credential Registry. In KERI, a Credential Registry is implemented using a **Transaction Event Log (TEL)**. This TEL is a secure, hash-linked log, managed by the Issuer's AID, specifically for being the registry referenced by each ACDC event, both issuance and revocation. The registry itself is identified by a SAID derived from its inception event (`vcp` event type for registry inception). The TEL's history is anchored to the Issuer's Key Event Log, ensuring that all changes to the registry's state are cryptographically secured by the Issuer's controlling keys. This anchoring is achieved by including a digest of the TEL event within an anchor that is included in the data property of a KEL event.
Use the code below to let the Issuer client create this registry. A human-readable name (`issuerRegistryName`) is used to reference it within the client.
βΉοΈ NOTE: Production Registry Naming Suggestion
In production code you can name the registry whatever you want. It is not something that needs to be shown to the user and can be wholly managed behind the scenes. The important architectural consideration is that an issuer has a registry. Whether you decide to name the registry a user facing name or just a system name is up to you.
The suggestion is to keep the name a system-level thing that is not user facing unless absolutely necessary and valuable to the end user. The less things the end user has to worry about, the better. Generally speaking, the name of an ACDC registry is a developer or architect level concern, not a user concern, unless you are exposing multiple registries to the end user and need human-friendly names to distinguish them.
```typescript
//Create Issuer credential Registry
const issuerRegistryName = 'issuerRegistry' // Human readable identifier for the Registry
// Initiate registry creation
const createRegistryResult = await issuerClient
.registries()
.create({ name: issuerAidAlias, registryName: issuerRegistryName });
// Get the operation details
const createRegistryOperation = await createRegistryResult.op();
// Wait for the operation to complete
const createRegistryResponse = await issuerClient
.operations()
.wait(createRegistryOperation, AbortSignal.timeout(DEFAULT_TIMEOUT_MS));
// Clean up the operation from the agent's list
await issuerClient.operations().delete(createRegistryOperation.name);
console.log(`Registry '${issuerRegistryName}' created for Issuer AID ${issuerAid.i}.`);
console.log("Registry creation response:", JSON.stringify(createRegistryResponse.response, null, 2));
// Listing Registries to confirm creation and retrieve its SAID (regk)
const issuerRegistries = await issuerClient.registries().list(issuerAidAlias);
const issuerRegistry = issuerRegistries[0]
console.log(`Registry: Name='${issuerRegistry.name}', SAID (regk)='${issuerRegistry.regk}'`);
```
Registry 'issuerRegistry' created for Issuer AID EFXyxqw8Oqh64Jqt59tG4b8uUjqdhxv6ItG07p8V-bFn.
Registry creation response: {
"anchor": {
"i": "ELLNwuJU6TN5DbC82reqh77X0yRLLQPeDWPXyUjRarnK",
"s": "0",
"d": "ELLNwuJU6TN5DbC82reqh77X0yRLLQPeDWPXyUjRarnK"
}
}
Registry: Name='issuerRegistry', SAID (regk)='ELLNwuJU6TN5DbC82reqh77X0yRLLQPeDWPXyUjRarnK'
### Step 2: Retrieve Schema Definition
The Issuer needs the definition of the schema against which they intend to issue a credential. Since the schema OOBI was resolved during the setup phase, the schema definition can now be retrieved from the KERIA agent's cache using its SAID. You will reuse the `EventPass` schema (SAID: `EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK`) from previous KLI examples.
```typescript
// Retrieve Schemas
const issuerSchema = await issuerClient.schemas().get(schemaSaid);
console.log(issuerSchema)
```
{
"$id": "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
"$schema": "http://json-schema.org/draft-07/schema#",
title: "EventPass",
description: "Event Pass Schema",
type: "object",
credentialType: "EventPassCred",
version: "1.0.0",
properties: {
v: { description: "Credential Version String", type: "string" },
d: { description: "Credential SAID", type: "string" },
u: { description: "One time use nonce", type: "string" },
i: { description: "Issuer AID", type: "string" },
ri: { description: "Registry SAID", type: "string" },
s: { description: "Schema SAID", type: "string" },
a: {
oneOf: [
{ description: "Attributes block SAID", type: "string" },
{
"$id": "ELppbffpWEM-uufl6qpVTcN6LoZS2A69UN4Ddrtr_JqE",
description: "Attributes block",
type: "object",
properties: [Object],
additionalProperties: false,
required: [Array]
}
]
}
},
additionalProperties: false,
required: [ "v", "d", "i", "ri", "s", "a" ]
}
### Step 3: Issue the ACDC
Now the Issuer creates the actual ACDC. This involves:
1. Defining the `credentialClaims` β the specific attribute values for this instance of the `EventPass` credential.
2. Calling `issuerClient.credentials().issue()`. This method takes the Issuer's AID alias and an object specifying:
- `ri`: The SAID of the Credential Registry (`issuerRegistry.regk`) where this credential's issuance will be recorded.
- This field [will change](https://trustoverip.github.io/tswg-acdc-specification/#top-level-fields) from `ri` to `rd` in the upcoming 2.0 version of KERI and the 1.0 version of the ACDC Spec.
- `ri`: meant "registry identifier"
- `rd`: means "registry digest"
- `s`: The SAID of the schema (`schemaSaid`) this credential adheres to.
- `a`: An attributes block containing:
- `i`: The AID of the Issuee (the Holder, holderAid.i).
- The actual `credentialClaims`.
This `issue` command creates the ACDC locally within the Issuer's client and records an issuance event (e.g., `iss`) in the specified registry's TEL by sending the issued ACDC to the connected KERIA agent. The SAID of the newly created credential is then extracted from the response from KERIA.
Use the code below to perform these actions.
```typescript
// Issue Credential
const credentialClaims = {
"eventName":"GLEIF Summit",
"accessLevel":"staff",
"validDate":"2026-10-01"
}
const issueResult = await issuerClient
.credentials()
.issue(
issuerAidAlias,
{
ri: issuerRegistry.regk, //Registry Identifier (not the alias)
s: schemaSaid, // Schema identifier
a: { // Attributes block
i: holderAid.i, // Isuue or credential subject
...credentialClaims // The actual claims data
}
});
console.log("Issuing credential...")
// Issuance is an asynchronous operation.
const issueOperation = await issueResult.op; //In this case is .op instead of .op() (Inconsistency in the sdk)
// Wait for the issuance operation to complete.
const issueResponse = await issuerClient
.operations()
.wait(issueOperation, AbortSignal.timeout(DEFAULT_TIMEOUT_MS));
console.log("Finished issuing credential.");
// Clean up the operation.
await issuerClient.operations().delete(issueOperation.name);
// Extract the SAID of the newly created credential from the response.
// This SAID uniquely identifies this specific ACDC instance.
const credentialSaid = issueResponse.response.ced.d
// Display the issued credential from the Issuer's perspective.
const issuerCredential = await issuerClient.credentials().get(credentialSaid);
console.log(issuerCredential)
```
Issuing credential...
Finished issuing credential.
{
sad: {
v: "ACDC10JSON0001c4_",
d: "ELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V",
i: "EFXyxqw8Oqh64Jqt59tG4b8uUjqdhxv6ItG07p8V-bFn",
ri: "ELLNwuJU6TN5DbC82reqh77X0yRLLQPeDWPXyUjRarnK",
s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
a: {
d: "EE6oKr5ezGEVAln5UcDYNtQVFPSwC19NryGIDoxBSmGr",
i: "EAjFdCtYoYlHt_1I895vGQI65A2-0u98S4LmY-f7GK6b",
eventName: "GLEIF Summit",
accessLevel: "staff",
validDate: "2026-10-01",
dt: "2025-09-12T04:11:50.590000+00:00"
}
},
atc: "-IABELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V0AAAAAAAAAAAAAAAAAAAAAAAELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V",
iss: {
v: "KERI10JSON0000ed_",
t: "iss",
d: "EBD_jOy7MEW6Skk4_UejLCSyGGRXszH4xqDIjlyc6QO1",
i: "ELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V",
s: "0",
ri: "ELLNwuJU6TN5DbC82reqh77X0yRLLQPeDWPXyUjRarnK",
dt: "2025-09-12T04:11:50.590000+00:00"
},
issatc: "-VAS-GAB0AAAAAAAAAAAAAAAAAAAAAACECCggV8FcbSXZQwVGZkeJYxUPbtJi9m-YDBE3qlqrxg2",
pre: "EFXyxqw8Oqh64Jqt59tG4b8uUjqdhxv6ItG07p8V-bFn",
schema: {
"$id": "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
"$schema": "http://json-schema.org/draft-07/schema#",
title: "EventPass",
description: "Event Pass Schema",
type: "object",
credentialType: "EventPassCred",
version: "1.0.0",
properties: {
v: { description: "Credential Version String", type: "string" },
d: { description: "Credential SAID", type: "string" },
u: { description: "One time use nonce", type: "string" },
i: { description: "Issuer AID", type: "string" },
ri: { description: "Registry SAID", type: "string" },
s: { description: "Schema SAID", type: "string" },
a: { oneOf: [ [Object], [Object] ] }
},
additionalProperties: false,
required: [ "v", "d", "i", "ri", "s", "a" ]
},
chains: [],
status: {
vn: [ 1, 0 ],
i: "ELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V",
s: "0",
d: "EBD_jOy7MEW6Skk4_UejLCSyGGRXszH4xqDIjlyc6QO1",
ri: "ELLNwuJU6TN5DbC82reqh77X0yRLLQPeDWPXyUjRarnK",
ra: {},
a: { s: 2, d: "ECCggV8FcbSXZQwVGZkeJYxUPbtJi9m-YDBE3qlqrxg2" },
dt: "2025-09-12T04:11:50.590000+00:00",
et: "iss"
},
anchor: {
pre: "ELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V",
sn: 0,
d: "EBD_jOy7MEW6Skk4_UejLCSyGGRXszH4xqDIjlyc6QO1"
},
anc: {
v: "KERI10JSON00013a_",
t: "ixn",
d: "ECCggV8FcbSXZQwVGZkeJYxUPbtJi9m-YDBE3qlqrxg2",
i: "EFXyxqw8Oqh64Jqt59tG4b8uUjqdhxv6ItG07p8V-bFn",
s: "2",
p: "EOsAKVyXZIH0AH3dCzlNKIQPDnYDWGdSG6w2-cTqH1o-",
a: [
{
i: "ELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V",
s: "0",
d: "EBD_jOy7MEW6Skk4_UejLCSyGGRXszH4xqDIjlyc6QO1"
}
]
},
ancatc: [
"-VBq-AABAADUOMLr0sJR89AoE9pjJNK-DXA58P3imC_G7ZO_ImpLbg7YQ3KmdM-VZLiQftN9JHPYUBKRnMV5SKOONtbMSdsO-BADAAAhX4wdE-TWCTPUwtCTnPHMtQ9URHPodLOa2LA4rKh6h_GCauNZbkn8kT75YFQU25xzWMYQXoJ6qPoYxop_ZUcDABD2jbhWc2PoBX8yBY44qEGrB3LH9Rjk45utiNyVvn5YquazGPz8iOdY11pU22jZJ_4ef1JYk4yABYSE_jz6acwIACC_HeonFhFMScR5l-wM2Sz4JnGTp1hK2ArqKdez44Oe3Qw2ZoguHsx7ENjuZNuZA03FDSiq7mDv4TeEeMxjpUgD-EAB0AAAAAAAAAAAAAAAAAAAAAAA1AAG2025-09-12T04c11c50d684495p00c00"
]
}
### Step 4: Issuer Grants Credential via IPEX
The credential has been created but currently resides with the Issuer. To transfer it to the Holder, the Issuer initiates an IPEX (Issuance and Presentation Exchange) grant. This process uses KERI `exn` (exchange) messages. The grant message effectively offers the credential to the Holder.
The `issuerClient.ipex().grant()` method prepares the grant message, including the ACDC itself (`acdc`), the issuance event from the registry (`iss`), and the anchoring event from the Issuer's KEL (`anc`) along with its signatures (`ancAttachment`).
You'll notice in the code below calls to `Serder()`, which serves the purpose of packaging and parsing KERI events into their serialized forms (e.g. CESR-coded messages) for signing, verification, or transmission.
Then, `issuerClient.ipex().submitGrant()` sends this packaged grant message to the Holder's KERIA agent.
Use the code below to perform the IPEX grant.
```typescript
// Ipex Grant
const [grant, gsigs, gend] = await issuerClient.ipex().grant({
senderName: issuerAidAlias,
acdc: new Serder(issuerCredential.sad), // The ACDC (Verifiable Credential) itself
iss: new Serder(issuerCredential.iss), // The issuance event from the credential registry (TEL event)
anc: new Serder(issuerCredential.anc), // The KEL event anchoring the TEL issuance event
ancAttachment: issuerCredential.ancatc, // Signatures for the KEL anchoring event
recipient: holderAid.i, // AID of the Holder
datetime: createTimestamp(), // Timestamp for the grant message
});
// Issuer submits the prepared grant message to the Holder.
// This sends an 'exn' message to the Holder's KERIA agent.
const submitGrantOperation = await issuerClient
.ipex()
.submitGrant(
issuerAidAlias, // Issuer's AID alias
grant, // The grant message payload
gsigs, // Signatures for the grant message
gend, // Endorsements for the grant message
[holderAid.i] // List of recipient AIDs
);
console.log("Sending IPEX Grant as issuer")
// Wait for the submission operation to complete.
const submitGrantResponse = await issuerClient
.operations()
.wait(submitGrantOperation, AbortSignal.timeout(DEFAULT_TIMEOUT_MS));
console.log("IPEX Grant sent")
// Clean up the operation.
await issuerClient.operations().delete(submitGrantOperation.name);
```
Sending IPEX Grant as issuer
IPEX Grant sent
#### Credential Status Checking
**Holder Checks Credential Status (Optional)**
The Holder can proactively check the status of a credential in the Issuer's registry if they know the registry's SAID (`issuerRegistry.regk`) and the credential's SAID (`issuerCredential.sad.d`). This query demonstrates how a party can verify the status of an ACDC directly from its TEL.
The retry loop below shows one way to cause the holder to wait for the issued credential to arrive.
```typescript
// The flow transitions from the Issuer to the Holder.
// A delay and retry mechanism is added to allow time for KERIA agents and witnesses
// to propagate the credential issuance information.
let credentialState;
// Retry loop to fetch credential state from the Holder's perspective.
for (let attempt = 1; attempt <= DEFAULT_RETRIES ; attempt++) {
try{
// Holder's client queries the state of the credential in the Issuer's registry.
credentialState = await holderClient.credentials().state(issuerRegistry.regk, issuerCredential.sad.d)
console.log("Received the credential.")
break;
}
catch (error){
console.log(`[Retry] failed to get credential state on attempt #${attempt} of ${DEFAULT_RETRIES}`);
if (attempt === DEFAULT_RETRIES) {
console.error(`[Retry] Max retries (${DEFAULT_RETRIES}) reached for getting credential state.`);
throw error;
}
console.log(`[Retry] Waiting ${DEFAULT_DELAY_MS}ms before next attempt...`);
await new Promise(resolve => setTimeout(resolve, DEFAULT_DELAY_MS));
}
}
console.log(credentialState) // Displays the status (e.g., issued, revoked)
```
[Retry] failed to get credential state on attempt #1 of 5
[Retry] Waiting 5000ms before next attempt...
Received the credential.
{
vn: [ 1, 0 ],
i: "ELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V",
s: "0",
d: "EBD_jOy7MEW6Skk4_UejLCSyGGRXszH4xqDIjlyc6QO1",
ri: "ELLNwuJU6TN5DbC82reqh77X0yRLLQPeDWPXyUjRarnK",
ra: {},
a: { s: 2, d: "ECCggV8FcbSXZQwVGZkeJYxUPbtJi9m-YDBE3qlqrxg2" },
dt: "2025-09-12T04:11:50.590000+00:00",
et: "iss"
}
#### Notifications API for IPEX messages
Sending an IPEX Grant also causes a notification object to be sent from the issuer (discloser) to the holder (disclosee). The Notifications API is an internal module used to signal transmission of an ACDC. Polling the list of received notifications is the way a holder knows when they have received a credential presentation through an IPEX grant.
### Step 5: Holder Receives IPEX Grant Notification
The Holder's KERIA agent will receive the grant `exn` message sent by the Issuer and a notification object referencing the grant message. The Holder's client can list its notifications to find this incoming grant. The notification will contain the SAID of the `exn` message (`grantNotification.a.d`), which can then be used to retrieve the full details of the grant exchange from the Holder's client.
```typescript
// Holder waits for Grant notification
let notifications;
// Retry loop to fetch notifications.
for (let attempt = 1; attempt <= DEFAULT_RETRIES ; attempt++) {
try{
// List notifications, filtering for unread IPEX_GRANT_ROUTE messages.
let allNotifications = await holderClient.notifications().list( );
notifications = allNotifications.notes.filter(
(n) => n.a.r === IPEX_GRANT_ROUTE && n.r === false // n.r is 'read' status
)
if(notifications.length === 0){
throw new Error("Grant notification not found"); // Throw error to trigger retry
}
console.log("Found an unread notification for an IPEX Grant");
break;
}
catch (error){
console.log(`[Retry] Grant notification not found on attempt #${attempt} of ${DEFAULT_RETRIES}`);
if (attempt === DEFAULT_RETRIES) {
console.error(`[Retry] Max retries (${DEFAULT_RETRIES}) reached for grant notification.`);
throw error;
}
console.log(`[Retry] Waiting ${DEFAULT_DELAY_MS}ms before next attempt...`);
await new Promise(resolve => setTimeout(resolve, DEFAULT_DELAY_MS));
}
}
const grantNotification = notifications[0] // Assuming only one grant notification for simplicity
console.log("The notification", grantNotification) // Displays the notification details
// Retrieve the full IPEX grant exchange details using the SAID from the notification.
// The 'exn' field in the exchange will contain the actual credential data.
const grantExchange = await holderClient.exchanges().get(grantNotification.a.d);
console.log("The grant referenced by the notification")
console.log(grantExchange) // Displays the content of the grant message
```
Found an unread notification for an IPEX Grant
The notification {
i: "0ADlBQD-7OfutO-chnbpj0tm",
dt: "2025-09-12T04:11:53.537720+00:00",
r: false,
a: {
r: "/exn/ipex/grant",
d: "EMWcC2FyWRKUVl2s2rAlIuhVPrbPFWIxB6gHAIaB7bkf",
m: ""
}
}
The grant referenced by the notification
{
exn: {
v: "KERI10JSON00057f_",
t: "exn",
d: "EMWcC2FyWRKUVl2s2rAlIuhVPrbPFWIxB6gHAIaB7bkf",
i: "EFXyxqw8Oqh64Jqt59tG4b8uUjqdhxv6ItG07p8V-bFn",
rp: "EAjFdCtYoYlHt_1I895vGQI65A2-0u98S4LmY-f7GK6b",
p: "",
dt: "2025-09-12T04:11:53.109000+00:00",
r: "/ipex/grant",
q: {},
a: { i: "EAjFdCtYoYlHt_1I895vGQI65A2-0u98S4LmY-f7GK6b", m: "" },
e: {
acdc: {
v: "ACDC10JSON0001c4_",
d: "ELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V",
i: "EFXyxqw8Oqh64Jqt59tG4b8uUjqdhxv6ItG07p8V-bFn",
ri: "ELLNwuJU6TN5DbC82reqh77X0yRLLQPeDWPXyUjRarnK",
s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
a: {
d: "EE6oKr5ezGEVAln5UcDYNtQVFPSwC19NryGIDoxBSmGr",
i: "EAjFdCtYoYlHt_1I895vGQI65A2-0u98S4LmY-f7GK6b",
eventName: "GLEIF Summit",
accessLevel: "staff",
validDate: "2026-10-01",
dt: "2025-09-12T04:11:50.590000+00:00"
}
},
iss: {
v: "KERI10JSON0000ed_",
t: "iss",
d: "EBD_jOy7MEW6Skk4_UejLCSyGGRXszH4xqDIjlyc6QO1",
i: "ELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V",
s: "0",
ri: "ELLNwuJU6TN5DbC82reqh77X0yRLLQPeDWPXyUjRarnK",
dt: "2025-09-12T04:11:50.590000+00:00"
},
anc: {
v: "KERI10JSON00013a_",
t: "ixn",
d: "ECCggV8FcbSXZQwVGZkeJYxUPbtJi9m-YDBE3qlqrxg2",
i: "EFXyxqw8Oqh64Jqt59tG4b8uUjqdhxv6ItG07p8V-bFn",
s: "2",
p: "EOsAKVyXZIH0AH3dCzlNKIQPDnYDWGdSG6w2-cTqH1o-",
a: [ [Object] ]
},
d: "EGxDCE-A4trDikKZjixYaqyjBf1gbpAgT773O94vTJR_"
}
},
pathed: {
acdc: "-IABELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V0AAAAAAAAAAAAAAAAAAAAAAAEBD_jOy7MEW6Skk4_UejLCSyGGRXszH4xqDIjlyc6QO1",
iss: "-VAS-GAB0AAAAAAAAAAAAAAAAAAAAAAAECCggV8FcbSXZQwVGZkeJYxUPbtJi9m-YDBE3qlqrxg2",
anc: "-VBq-AABAADUOMLr0sJR89AoE9pjJNK-DXA58P3imC_G7ZO_ImpLbg7YQ3KmdM-VZLiQftN9JHPYUBKRnMV5SKOONtbMSdsO-BADAAAhX4wdE-TWCTPUwtCTnPHMtQ9URHPodLOa2LA4rKh6h_GCauNZbkn8kT75YFQU25xzWMYQXoJ6qPoYxop_ZUcDABD2jbhWc2PoBX8yBY44qEGrB3LH9Rjk45utiNyVvn5YquazGPz8iOdY11pU22jZJ_4ef1JYk4yABYSE_jz6acwIACC_HeonFhFMScR5l-wM2Sz4JnGTp1hK2ArqKdez44Oe3Qw2ZoguHsx7ENjuZNuZA03FDSiq7mDv4TeEeMxjpUgD-EAB0AAAAAAAAAAAAAAAAAAAAAAA1AAG2025-09-12T04c11c50d684495p00c00"
}
}
### Step 6: Holder Admits Credential
Upon receiving and reviewing the grant, the Holder decides to accept (`admit`) the credential. This involves:
- Preparing an `admit` `exn` message using `holderClient.ipex().admit()`.
- Submitting this `admit` message back to the Issuer using `holderClient.ipex().submitAdmit()`.
- Marking the original grant notification as read.
- The Holder's client then processes the admitted credential, verifying its signatures, schema, and status against the Issuer's KEL and TEL, and stores it locally.
```typescript
// Holder admits (accepts) the IPEX grant.
// Prepare the IPEX admit message.
const [admit, sigs, aend] = await holderClient.ipex().admit({
senderName: holderAidAlias, // Alias of the Holder's AID
message: '', // Optional message to include in the admit
grantSaid: grantNotification.a.d!,// SAID of the grant 'exn' message being admitted
recipient: issuerAid.i, // AID of the Issuer
datetime: createTimestamp(), // Timestamp for the admit message
});
// Holder submits the prepared admit message to the Issuer.
const admitOperation = await holderClient
.ipex()
.submitAdmit(holderAidAlias, admit, sigs, aend, [issuerAid.i]);
// Wait for the submission operation to complete.
const admitResponse = await holderClient
.operations()
.wait(admitOperation, AbortSignal.timeout(DEFAULT_TIMEOUT_MS));
// Clean up the operation.
await holderClient.operations().delete(admitOperation.name);
// Holder marks the grant notification as read.
await holderClient.notifications().mark(grantNotification.i);
console.log("Holder's notifications after marking grant as read:");
console.log(await holderClient.notifications().list());
// Holder can now get the credential from their local store.
// This implies the client has processed, verified, and stored it upon admission.
const holderReceivedCredential = await holderClient.credentials().get(issuerCredential.sad.d);
console.log("Credential as stored by Holder:");
console.log(holderReceivedCredential);
```
Holder's notifications after marking grant as read:
{
start: 0,
end: 0,
total: 1,
notes: [
{
i: "0ADlBQD-7OfutO-chnbpj0tm",
dt: "2025-09-12T04:11:53.537720+00:00",
r: true,
a: {
r: "/exn/ipex/grant",
d: "EMWcC2FyWRKUVl2s2rAlIuhVPrbPFWIxB6gHAIaB7bkf",
m: ""
}
}
]
}
Credential as stored by Holder:
{
sad: {
v: "ACDC10JSON0001c4_",
d: "ELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V",
i: "EFXyxqw8Oqh64Jqt59tG4b8uUjqdhxv6ItG07p8V-bFn",
ri: "ELLNwuJU6TN5DbC82reqh77X0yRLLQPeDWPXyUjRarnK",
s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
a: {
d: "EE6oKr5ezGEVAln5UcDYNtQVFPSwC19NryGIDoxBSmGr",
i: "EAjFdCtYoYlHt_1I895vGQI65A2-0u98S4LmY-f7GK6b",
eventName: "GLEIF Summit",
accessLevel: "staff",
validDate: "2026-10-01",
dt: "2025-09-12T04:11:50.590000+00:00"
}
},
atc: "-IABELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V0AAAAAAAAAAAAAAAAAAAAAAAELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V",
iss: {
v: "KERI10JSON0000ed_",
t: "iss",
d: "EBD_jOy7MEW6Skk4_UejLCSyGGRXszH4xqDIjlyc6QO1",
i: "ELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V",
s: "0",
ri: "ELLNwuJU6TN5DbC82reqh77X0yRLLQPeDWPXyUjRarnK",
dt: "2025-09-12T04:11:50.590000+00:00"
},
issatc: "-VAS-GAB0AAAAAAAAAAAAAAAAAAAAAACECCggV8FcbSXZQwVGZkeJYxUPbtJi9m-YDBE3qlqrxg2",
pre: "EFXyxqw8Oqh64Jqt59tG4b8uUjqdhxv6ItG07p8V-bFn",
schema: {
"$id": "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
"$schema": "http://json-schema.org/draft-07/schema#",
title: "EventPass",
description: "Event Pass Schema",
type: "object",
credentialType: "EventPassCred",
version: "1.0.0",
properties: {
v: { description: "Credential Version String", type: "string" },
d: { description: "Credential SAID", type: "string" },
u: { description: "One time use nonce", type: "string" },
i: { description: "Issuer AID", type: "string" },
ri: { description: "Registry SAID", type: "string" },
s: { description: "Schema SAID", type: "string" },
a: { oneOf: [ [Object], [Object] ] }
},
additionalProperties: false,
required: [ "v", "d", "i", "ri", "s", "a" ]
},
chains: [],
status: {
vn: [ 1, 0 ],
i: "ELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V",
s: "0",
d: "EBD_jOy7MEW6Skk4_UejLCSyGGRXszH4xqDIjlyc6QO1",
ri: "ELLNwuJU6TN5DbC82reqh77X0yRLLQPeDWPXyUjRarnK",
ra: {},
a: { s: 2, d: "ECCggV8FcbSXZQwVGZkeJYxUPbtJi9m-YDBE3qlqrxg2" },
dt: "2025-09-12T04:11:50.590000+00:00",
et: "iss"
},
anchor: {
pre: "ELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V",
sn: 0,
d: "EBD_jOy7MEW6Skk4_UejLCSyGGRXszH4xqDIjlyc6QO1"
},
anc: {
v: "KERI10JSON00013a_",
t: "ixn",
d: "ECCggV8FcbSXZQwVGZkeJYxUPbtJi9m-YDBE3qlqrxg2",
i: "EFXyxqw8Oqh64Jqt59tG4b8uUjqdhxv6ItG07p8V-bFn",
s: "2",
p: "EOsAKVyXZIH0AH3dCzlNKIQPDnYDWGdSG6w2-cTqH1o-",
a: [
{
i: "ELgxptI5uen2gvAxePZ3l2lSEfMR5qlHWxvmkk6pi38V",
s: "0",
d: "EBD_jOy7MEW6Skk4_UejLCSyGGRXszH4xqDIjlyc6QO1"
}
]
},
ancatc: [
"-VBq-AABAADUOMLr0sJR89AoE9pjJNK-DXA58P3imC_G7ZO_ImpLbg7YQ3KmdM-VZLiQftN9JHPYUBKRnMV5SKOONtbMSdsO-BADAAAhX4wdE-TWCTPUwtCTnPHMtQ9URHPodLOa2LA4rKh6h_GCauNZbkn8kT75YFQU25xzWMYQXoJ6qPoYxop_ZUcDABD2jbhWc2PoBX8yBY44qEGrB3LH9Rjk45utiNyVvn5YquazGPz8iOdY11pU22jZJ_4ef1JYk4yABYSE_jz6acwIACC_HeonFhFMScR5l-wM2Sz4JnGTp1hK2ArqKdez44Oe3Qw2ZoguHsx7ENjuZNuZA03FDSiq7mDv4TeEeMxjpUgD-EAB0AAAAAAAAAAAAAAAAAAAAAAA1AAG2025-09-12T04c11c53d683597p00c00"
]
}
### Step 7: Issuer Receives Admit Notification
The Issuer, in turn, will receive a notification that the Holder has admitted the credential. The Issuer's client lists its notifications, finds the `admit` message, and marks it as read. This completes the issuance loop.
```typescript
// Issuer retrieves the Admit notification from the Holder.
let issuerAdmitNotifications;
// Retry loop for the Issuer to receive the admit notification.
for (let attempt = 1; attempt <= DEFAULT_RETRIES ; attempt++) {
try{
// List notifications, filtering for unread IPEX_ADMIT_ROUTE messages.
let allNotifications = await issuerClient.notifications().list();
issuerAdmitNotifications = allNotifications.notes.filter(
(n) => n.a.r === IPEX_ADMIT_ROUTE && n.r === false
)
if(issuerAdmitNotifications.length === 0){
throw new Error("Admit notification not found"); // Throw error to trigger retry
}
break; // Exit loop if notification found
}
catch (error){
console.log(`[Retry] Admit notification not found for Issuer on attempt #${attempt} of ${DEFAULT_RETRIES}`);
if (attempt === DEFAULT_RETRIES) {
console.error(`[Retry] Max retries (${DEFAULT_RETRIES}) reached for Issuer's admit notification.`);
throw error;
}
console.log(`[Retry] Waiting ${DEFAULT_DELAY_MS}ms before next attempt...`);
await new Promise(resolve => setTimeout(resolve, DEFAULT_DELAY_MS));
}
}
const admitNotificationForIssuer = issuerAdmitNotifications[0] // Assuming one notification
// Issuer marks the admit notification as read.
await issuerClient.notifications().mark(admitNotificationForIssuer.i);
console.log("Issuer's notifications after marking admit as read:");
console.log(await issuerClient.notifications().list());
```
Issuer's notifications after marking admit as read:
{
start: 0,
end: 0,
total: 1,
notes: [
{
i: "0AD3_nvR8hwa9gwdC-jD0WVb",
dt: "2025-09-12T04:11:59.195147+00:00",
r: true,
a: {
r: "/exn/ipex/admit",
d: "EBds7K8xmjYAZTp6CE5sVKzJtlEqgfQlmE0pdL9jXfgA",
m: ""
}
}
]
}
**Cleanup (Optional)**
Once the IPEX flow for issuance is complete and notifications have been processed, both parties can optionally delete these notifications from their agent notification stores.
```typescript
// Issuer Remove Admit Notification from their list
await issuerClient.notifications().delete(admitNotificationForIssuer.i);
console.log("Issuer's notifications after deleting admit notification:");
console.log(await issuerClient.notifications().list());
// Holder Remove Grant Notification from their list
await holderClient.notifications().delete(grantNotification.i);
console.log("Holder's notifications after deleting grant notification:");
console.log(await holderClient.notifications().list());
```
Issuer's notifications after deleting admit notification:
{ start: 0, end: 0, total: 0, notes: [] }
Holder's notifications after deleting grant notification:
{ start: 0, end: 0, total: 0, notes: [] }
π SUMMARY
This notebook demonstrated how to perform credential issuance using the Issuance and Presentation Exchange (IPEX) protocol.
- Credential Registry: before any credentials may be created for an issuer it must create a credential registry that will be referenced by a set of issued credentials. An issuer may make more than one registry and name them according to purpose.
- Schema Definition: every credential must have a schema definition and this schema definition must be loaded into the issuer's local database prior to issuing the credential.
- Credential Issuance: an issuer may create a credential targeted towards a particular subject, or an identifier that will be the holder of an ACDC credential. Credentials may also be untargeted.
- Sharing with Holder (subject): following creation an issuer may share the ACDC credential with the subject of the newly created credential, the holder, using an IPEX Grant.
- IPEX action notifications: as a convenience for the users of the IPEX process there are notifications sent of each IPEX action that begin as unread and may be marked as read once a notification receiver has processed the referenced action or event.
The ACDC credential issuance process with IPEX is a critical part of the overall value that the KERI suite of protocols stands to provide as it is the first step in allowing verifiable, provenanced data sharing between multiple parties. This training shows how to use IPEX to issue and share credentials.
[<- Prev (KERIA Signify Key Rotation)](102_17_KERIA_Signify_Key_Rotation.ipynb) | [Next (KERIA Signify Credential Presentation and Revocation) ->](102_25_KERIA_Signify_Credential_Presentation_and_Revocation.ipynb)
# SignifyTS: ACDC Presentation and Revocation with IPEX
π― OBJECTIVE
Demonstrate the process of presenting an ACDC (Authentic Chained Data Container) from a Holder to a Verifier using the IPEX protocol with the SignifyTS library and the process of credential revocation.
βΉοΈ NOTE
This section utilizes utility functions (from
./scripts_ts/utils.ts) to quickly establish the necessary preconditions for credential presentation. Refer to the
ACDC Issuance notebook for a detailed explanation of the setup steps.
## Prerequisites: Client and Credential Setup
The client setup process from the previous notebook is reused here.
```typescript
import { randomPasscode, Serder} from 'npm:signify-ts';
import { initializeSignify,
initializeAndConnectClient,
createNewAID,
addEndRoleForAID,
generateOOBI,
resolveOOBI,
createTimestamp,
createCredentialRegistry,
getSchema,
issueCredential,
ipexGrantCredential,
getCredentialState,
waitForAndGetNotification,
ipexAdmitGrant,
markNotificationRead,
DEFAULT_IDENTIFIER_ARGS,
DEFAULT_TIMEOUT_MS,
DEFAULT_DELAY_MS,
DEFAULT_RETRIES,
ROLE_AGENT,
IPEX_GRANT_ROUTE,
IPEX_ADMIT_ROUTE,
IPEX_APPLY_ROUTE,
IPEX_OFFER_ROUTE,
SCHEMA_SERVER_HOST
} from './scripts_ts/utils.ts';
// Clients setup
// Initialize Issuer, Holder and Verifier Clients, Create AIDs for each one, assign 'agent' role to the AIDs
// generate and resolve OOBIs
// Issuer Client
console.log("Creating Issuer...")
const issuerBran = randomPasscode()
const issuerAidAlias = 'issuerAid'
const { client: issuerClient } = await initializeAndConnectClient(issuerBran)
const { aid: issuerAid} = await createNewAID(issuerClient, issuerAidAlias, DEFAULT_IDENTIFIER_ARGS);
await addEndRoleForAID(issuerClient, issuerAidAlias, ROLE_AGENT);
const issuerOOBI = await generateOOBI(issuerClient, issuerAidAlias, ROLE_AGENT);
// Holder Client
console.log("Creating Holder...");
const holderBran = randomPasscode()
const holderAidAlias = 'holderAid'
const { client: holderClient } = await initializeAndConnectClient(holderBran)
const { aid: holderAid} = await createNewAID(holderClient, holderAidAlias, DEFAULT_IDENTIFIER_ARGS);
await addEndRoleForAID(holderClient, holderAidAlias, ROLE_AGENT);
const holderOOBI = await generateOOBI(holderClient, holderAidAlias, ROLE_AGENT);
// Verifier Client
console.log("Creating Verifier...")
const verifierBran = randomPasscode()
const verifierAidAlias = 'verifierAid'
const { client: verifierClient } = await initializeAndConnectClient(verifierBran)
const { aid: verifierAid} = await createNewAID(verifierClient, verifierAidAlias, DEFAULT_IDENTIFIER_ARGS);
await addEndRoleForAID(verifierClient, verifierAidAlias, ROLE_AGENT);
const verifierOOBI = await generateOOBI(verifierClient, verifierAidAlias, ROLE_AGENT);
console.log("Created issuer, holder, and verifier AIDs");
// Clients OOBI Resolution
// Resolve OOBIs to establish connections Issuer-Holder, Holder-Verifier
const issuerContactAlias = 'issuerContact';
const holderContactAlias = 'holderContact';
const verifierContactAlias = 'verifierContact';
await resolveOOBI(issuerClient, holderOOBI, holderContactAlias);
await resolveOOBI(holderClient, issuerOOBI, issuerContactAlias);
await resolveOOBI(verifierClient, holderOOBI, holderContactAlias);
await resolveOOBI(holderClient, verifierOOBI, verifierContactAlias);
await resolveOOBI(issuerClient, verifierOOBI, holderContactAlias); // for sending revocation status
console.log("Resolved agent OOBIs to connect issuer, holder, and verifier");
// Schemas OOBI Resolution
// Resolve the Schemas from the Schema Server (VLEI-Server)
const schemaContactAlias = 'schemaContact';
const schemaSaid = 'EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK';
const schemaOOBI = `http://vlei-server:7723/oobi/${schemaSaid}`;
await resolveOOBI(issuerClient, schemaOOBI, schemaContactAlias);
await resolveOOBI(holderClient, schemaOOBI, schemaContactAlias);
await resolveOOBI(verifierClient, schemaOOBI, schemaContactAlias);
console.log("Resolved schema OOBIs to discover the ACDC schema as issuer, holder, and verifier");
console.log("\n\nβ
Client setup and OOBI resolutions complete.");
```
Creating Issuer...
Using Passcode (bran): DXqFqbhriflAbpfHtGhCb
Client boot process initiated with KERIA agent.
Client AID Prefix: EJaE5qzycFObah5TRtXwjr_wJmNT1WX3iQ0XfNHRpcRa
Agent AID Prefix: EMzug2qMIcy5dOTmWVaWJFVxLfvFMNCY_sJf_RAFvW7v
Initiating AID inception for alias: issuerAid
Successfully created AID with prefix: EGOrzLLCX0fN4XRHE_SQFBGu6sht8iO_l1VGCO7ag7AY
Assigning 'agent' role to KERIA Agent EMzug2qMIcy5dOTmWVaWJFVxLfvFMNCY_sJf_RAFvW7v for AID alias issuerAid
Successfully assigned 'agent' role for AID alias issuerAid.
Generating OOBI for AID alias issuerAid with role agent
Generated OOBI URL: http://keria:3902/oobi/EGOrzLLCX0fN4XRHE_SQFBGu6sht8iO_l1VGCO7ag7AY/agent/EMzug2qMIcy5dOTmWVaWJFVxLfvFMNCY_sJf_RAFvW7v
Creating Holder...
Using Passcode (bran): BqEf1olUY7fLOJ5HD59lC
Client boot process initiated with KERIA agent.
Client AID Prefix: EEtiYLdoIHKkbur161cbTRdzmKhaOB9ewDPunrZjU3o3
Agent AID Prefix: ENB5REtgeq_hge71JzF8jJvxwti169qPgWXTBWOCCtVm
Initiating AID inception for alias: holderAid
Successfully created AID with prefix: EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG
Assigning 'agent' role to KERIA Agent ENB5REtgeq_hge71JzF8jJvxwti169qPgWXTBWOCCtVm for AID alias holderAid
Successfully assigned 'agent' role for AID alias holderAid.
Generating OOBI for AID alias holderAid with role agent
Generated OOBI URL: http://keria:3902/oobi/EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG/agent/ENB5REtgeq_hge71JzF8jJvxwti169qPgWXTBWOCCtVm
Creating Verifier...
Using Passcode (bran): DrvtxpOKfaZAIKdMQs9Lr
Client boot process initiated with KERIA agent.
Client AID Prefix: EGdjSZ5qK7aM7rNt90jg8ZlFrDdS6YG-MreHQh4oAKUj
Agent AID Prefix: EHWra_FnhyO5V00wTEnW0fhbGsU1mNAPVF2-WVsdI9mb
Initiating AID inception for alias: verifierAid
Successfully created AID with prefix: ED-xfM6ATAg7mA3O94OQ_tN4Q7rbwl4v7N-WpEmzrfjy
Assigning 'agent' role to KERIA Agent EHWra_FnhyO5V00wTEnW0fhbGsU1mNAPVF2-WVsdI9mb for AID alias verifierAid
Successfully assigned 'agent' role for AID alias verifierAid.
Generating OOBI for AID alias verifierAid with role agent
Generated OOBI URL: http://keria:3902/oobi/ED-xfM6ATAg7mA3O94OQ_tN4Q7rbwl4v7N-WpEmzrfjy/agent/EHWra_FnhyO5V00wTEnW0fhbGsU1mNAPVF2-WVsdI9mb
Created issuer, holder, and verifier AIDs
Resolving OOBI URL: http://keria:3902/oobi/EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG/agent/ENB5REtgeq_hge71JzF8jJvxwti169qPgWXTBWOCCtVm with alias holderContact
Successfully resolved OOBI URL. Response: OK
Contact "holderContact" added/updated.
Resolving OOBI URL: http://keria:3902/oobi/EGOrzLLCX0fN4XRHE_SQFBGu6sht8iO_l1VGCO7ag7AY/agent/EMzug2qMIcy5dOTmWVaWJFVxLfvFMNCY_sJf_RAFvW7v with alias issuerContact
Successfully resolved OOBI URL. Response: OK
Contact "issuerContact" added/updated.
Resolving OOBI URL: http://keria:3902/oobi/EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG/agent/ENB5REtgeq_hge71JzF8jJvxwti169qPgWXTBWOCCtVm with alias holderContact
Successfully resolved OOBI URL. Response: OK
Contact "holderContact" added/updated.
Resolving OOBI URL: http://keria:3902/oobi/ED-xfM6ATAg7mA3O94OQ_tN4Q7rbwl4v7N-WpEmzrfjy/agent/EHWra_FnhyO5V00wTEnW0fhbGsU1mNAPVF2-WVsdI9mb with alias verifierContact
Successfully resolved OOBI URL. Response: OK
Contact "verifierContact" added/updated.
Resolving OOBI URL: http://keria:3902/oobi/ED-xfM6ATAg7mA3O94OQ_tN4Q7rbwl4v7N-WpEmzrfjy/agent/EHWra_FnhyO5V00wTEnW0fhbGsU1mNAPVF2-WVsdI9mb with alias holderContact
Successfully resolved OOBI URL. Response: OK
Contact "holderContact" added/updated.
Resolved agent OOBIs to connect issuer, holder, and verifier
Resolving OOBI URL: http://vlei-server:7723/oobi/EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK with alias schemaContact
Successfully resolved OOBI URL. Response: OK
Contact "schemaContact" added/updated.
Resolving OOBI URL: http://vlei-server:7723/oobi/EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK with alias schemaContact
Successfully resolved OOBI URL. Response: OK
Contact "schemaContact" added/updated.
Resolving OOBI URL: http://vlei-server:7723/oobi/EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK with alias schemaContact
Successfully resolved OOBI URL. Response: OK
Contact "schemaContact" added/updated.
Resolved schema OOBIs to discover the ACDC schema as issuer, holder, and verifier
β
Client setup and OOBI resolutions complete.
As you will be conducting a credential presentation in this notebook, let's generate one for use in the presentation workflow. Again, this involves creating a credential registry for the issuer, creating the ACDC credential, and then using IPEX grant and admit actions to send the credential to the holder and finally to the verifier.
```typescript
// Create Issuer Credential Registry
const issuerRegistryName = 'issuerRegistry'
console.log("Creating issuer registry")
const { registrySaid: registrySaid } = await createCredentialRegistry(issuerClient, issuerAidAlias, issuerRegistryName)
// Define credential Claims
const credentialClaims = {
"eventName":"GLEIF Summit",
"accessLevel":"staff",
"validDate":"2026-10-01"
}
// Issuer - Issue Credential
console.log("issuing credential to holder")
const { credentialSaid: credentialSaid} = await issueCredential(
issuerClient,
issuerAidAlias,
registrySaid,
schemaSaid,
holderAid.i,
credentialClaims
)
// Issuer - get credential (with all its data)
const credential = await issuerClient.credentials().get(credentialSaid);
// Issuer - Ipex grant
console.log("granting credential to holder")
const grantResponse = await ipexGrantCredential(
issuerClient,
issuerAidAlias,
holderAid.i,
credential
)
console.log("Issuer created and granted credential.")
// Holder - Wait for grant notification
console.log("Holder waiting for credential")
const grantNotifications = await waitForAndGetNotification(holderClient, IPEX_GRANT_ROUTE)
const grantNotification = grantNotifications[0]
// Holder - Admit Grant
const admitResponse = await ipexAdmitGrant(
holderClient,
holderAidAlias,
issuerAid.i,
grantNotification.a.d
)
console.log("Holder admitting credential")
// Holder - Mark notification
await markNotificationRead(holderClient, grantNotification.i)
// Issuer - Wait for admit notification
console.log("Issuer receiving admit...")
const admitNotifications = await waitForAndGetNotification(issuerClient, IPEX_ADMIT_ROUTE)
const admitNotification = admitNotifications[0]
// Issuer - Mark notification
await markNotificationRead(issuerClient, admitNotification.i)
console.log("\n\nβ
Issuer received admit. Issuance and reception complete.")
```
Creating issuer registry
Creating credential registry "issuerRegistry" for AID alias "issuerAid"...
Successfully created credential registry: EI1knJMnZANO17CvQPEQ5Hza0mWo4xE_JZ7guKtzyqfI
issuing credential to holder
Issuing credential from AID "issuerAid" to AID "EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG"...
{
name: "credential.EEr2KjMDimM-NzHvqpBjz-ZBGctjGZDK3WH2lndNlwCG",
metadata: {
ced: {
v: "ACDC10JSON0001c4_",
d: "EEr2KjMDimM-NzHvqpBjz-ZBGctjGZDK3WH2lndNlwCG",
i: "EGOrzLLCX0fN4XRHE_SQFBGu6sht8iO_l1VGCO7ag7AY",
ri: "EI1knJMnZANO17CvQPEQ5Hza0mWo4xE_JZ7guKtzyqfI",
s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
a: {
d: "EHX-zIC6mi2bqDC7sq9dCu8OgfuFoToMX_iL5q4rtqV_",
i: "EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG",
eventName: "GLEIF Summit",
accessLevel: "staff",
validDate: "2026-10-01",
dt: "2025-09-12T04:12:11.639000+00:00"
}
},
depends: {
name: "witness.EP6gx17zzX_JfzCPzEnp4ZuRJffPCauz6CyaZHur60KJ",
metadata: { pre: "EGOrzLLCX0fN4XRHE_SQFBGu6sht8iO_l1VGCO7ag7AY", sn: 2 },
done: false,
error: null,
response: null
}
},
done: true,
error: null,
response: {
ced: {
v: "ACDC10JSON0001c4_",
d: "EEr2KjMDimM-NzHvqpBjz-ZBGctjGZDK3WH2lndNlwCG",
i: "EGOrzLLCX0fN4XRHE_SQFBGu6sht8iO_l1VGCO7ag7AY",
ri: "EI1knJMnZANO17CvQPEQ5Hza0mWo4xE_JZ7guKtzyqfI",
s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
a: {
d: "EHX-zIC6mi2bqDC7sq9dCu8OgfuFoToMX_iL5q4rtqV_",
i: "EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG",
eventName: "GLEIF Summit",
accessLevel: "staff",
validDate: "2026-10-01",
dt: "2025-09-12T04:12:11.639000+00:00"
}
}
}
}
Successfully issued credential with SAID: EEr2KjMDimM-NzHvqpBjz-ZBGctjGZDK3WH2lndNlwCG
granting credential to holder
AID "issuerAid" granting credential to AID "EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG" via IPEX...
Successfully submitted IPEX grant from "issuerAid" to "EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG".
Issuer created and granted credential.
Holder waiting for credential
Waiting for notification with route "/exn/ipex/grant"...
[Retry] Grant notification not found on attempt #1 of 5
[Retry] Waiting 5000ms before next attempt...
AID "holderAid" admitting IPEX grant "ELOWFOpxTIMEle3DLOTlZWGQh3OXgePFXaKmwdHZla1R" from AID "EGOrzLLCX0fN4XRHE_SQFBGu6sht8iO_l1VGCO7ag7AY"...
Successfully submitted IPEX admit for grant "ELOWFOpxTIMEle3DLOTlZWGQh3OXgePFXaKmwdHZla1R".
Holder admitting credential
Marking notification "0ABrTf1ml6YO_Ch7wnf92WfD" as read...
Notification "0ABrTf1ml6YO_Ch7wnf92WfD" marked as read.
Issuer receiving admit...
Waiting for notification with route "/exn/ipex/admit"...
Marking notification "0ADeBsGFOHL6YOz-fKg14Twz" as read...
Notification "0ADeBsGFOHL6YOz-fKg14Twz" marked as read.
β
Issuer received admit. Issuance and reception complete.
Full Formal IPEX Credential Presentation Workflow: Apply through Admit
Now that the Holder possesses the credential, they can present it to a KERI AID acting as sample verifier.
NOTE: A more interesting verifier - vLEI Reporting API
In this instance the verifier does not do anything special with the credential above and beyond receiving the credential presentation. To go beyond simple reception of the credential you may review the GLEIF
vLEI Reporting API verifier sally. This verifier receives IPEX Grant messages, performs cryptographic verification on the chain of credentials, and also performs business logic checks on the types and issuer root of the parent QVI credential.
Getting back to this simplistic verifier, the workflow in this notebook also uses IPEX which may start with any of the following IPEX operations:
- IPEX Grant: The issuer or holder using an IPEX Grant to share the credential with a verifier.
- IPEX Apply: This includes the whole IPEX chain (apply -> offer -> agree -> grant -> admit): This begins with the Verifier requesting a presentation using IPEX Apply, followed by the Holder's IPEX Offer, followed by the Verifier's IPEX Agree, then the Holder's IPEX Grant, ended by the Verifier's IPEX Admit.
- IPEX Offer: This begins with the holder sending a metadata ACDC, or an ACDC showing the schema (shape) of data to share, to the verifier. The verifier can then respond with an IPEX Agree and the rest of the disclosure workflow with grant and admit can continue, as needed.
Below we dive into the second option, the longer, whole IPEX chain and show the code snipets you need to follow to do the presentation. If you wanted to start with IPEX Grant in your process then you could skip to step 7 and begin with the IPEX Grant.
### Step 1: Verifier Requests Presentation (Apply)
The Verifier initiates the presentation process by sending an IPEX apply message. This `apply` message is an `exn` message specifying the criteria for the credential they are requesting. This includes the `schemaSaid` and can include specific attributes the credential must have.
```typescript
// Verifier Ipex Apply (Presentation request)
// Prepare the IPEX apply message.
const [apply, sigsApply, _endApply] = await verifierClient.ipex().apply({ //_endApply is not used
senderName: verifierAidAlias, // Alias of the Verifier's AID
schemaSaid: schemaSaid, // SAID of the schema for the requested credential
attributes: { eventName:'GLEIF Summit' }, // Specific attributes the credential should have
recipient: holderAid.i, // AID of the Holder being asked for the presentation
datetime: createTimestamp(), // Timestamp for the apply message
});
// Verifier submits the prepared apply message to the Holder.
const applyOperation = await verifierClient
.ipex()
.submitApply(verifierAidAlias, apply, sigsApply, [holderAid.i]);
console.log("Verifier sending IPEX Apply to holder...")
// Wait for the submission operation to complete.
const applyResponse = await verifierClient
.operations()
.wait(applyOperation, AbortSignal.timeout(DEFAULT_TIMEOUT_MS));
console.log("IPEX Apply succeeded")
// Clean up the operation.
await verifierClient.operations().delete(applyOperation.name);
console.log("β
IPEX Apply complete")
```
Verifier sending IPEX Apply to holder...
IPEX Apply succeeded
β
IPEX Apply complete
### Step 2: Holder Receives Apply Request
#### Holder Apply Notification and Exchange
The Holder receives a notification for the Verifier's `apply` request. They retrieve the details of this request from the exchange message. After processing, the Holder marks the notification as read.
```typescript
// Holder receives the IPEX apply notification from the Verifier.
let holderApplyNotifications;
// Retry loop for the Holder to receive the apply notification.
for (let attempt = 1; attempt <= DEFAULT_RETRIES ; attempt++) {
try{
// List notifications, filtering for unread IPEX_APPLY_ROUTE messages.
let allNotifications = await holderClient.notifications().list()
holderApplyNotifications = allNotifications.notes.filter(
(n) => n.a.r === IPEX_APPLY_ROUTE && n.r === false // where "is read" (n.r) is false.
)
if(holderApplyNotifications.length === 0){
throw new Error("Apply notification not found"); // Throw error to trigger retry
}
break; // Exit loop if notification found
}
catch (error){
console.log(`[Retry] Apply notification not found for Holder on attempt #${attempt} of ${DEFAULT_RETRIES}`);
if (attempt === DEFAULT_RETRIES) {
console.error(`[Retry] Max retries (${DEFAULT_RETRIES}) reached for Holder's apply notification.`);
throw error;
}
console.log(`[Retry] Waiting ${DEFAULT_DELAY_MS}ms before next attempt...`);
await new Promise(resolve => setTimeout(resolve, DEFAULT_DELAY_MS));
}
}
const applyNotificationForHolder = holderApplyNotifications[0] // Assuming one notification
console.log("Holder received Apply Notification:");
console.log(applyNotificationForHolder);
// Retrieve the full IPEX apply exchange details.
const applyExchange = await holderClient.exchanges().get(applyNotificationForHolder.a.d);
console.log("\nDetails of Apply Exchange received by Holder:");
console.log(applyExchange);
// Extract the SAID of the apply 'exn' message for use in the offer.
const applyExchangeSaid = applyExchange.exn.d;
// Holder marks the apply notification as read.
await holderClient.notifications().mark(applyNotificationForHolder.i);
console.log("\nHolder's notifications after marking apply as read:");
console.log(await holderClient.notifications().list());
console.log("\n\nβ
Holder notification processing complete.")
```
[Retry] Apply notification not found for Holder on attempt #1 of 5
[Retry] Waiting 5000ms before next attempt...
Holder received Apply Notification:
{
i: "0AA8PpMsjBVmBpmvkV_i9iXp",
dt: "2025-09-12T04:12:19.041059+00:00",
r: false,
a: {
r: "/exn/ipex/apply",
d: "ECReKB0JTHwQ9eGlqYaK3lHlF5aBGKjsyek4iUkhkHE1",
m: ""
}
}
Details of Apply Exchange received by Holder:
{
exn: {
v: "KERI10JSON0001a0_",
t: "exn",
d: "ECReKB0JTHwQ9eGlqYaK3lHlF5aBGKjsyek4iUkhkHE1",
i: "ED-xfM6ATAg7mA3O94OQ_tN4Q7rbwl4v7N-WpEmzrfjy",
rp: "EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG",
p: "",
dt: "2025-09-12T04:12:18.659000+00:00",
r: "/ipex/apply",
q: {},
a: {
i: "EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG",
m: "",
s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
a: { eventName: "GLEIF Summit" }
},
e: {}
},
pathed: {}
}
Holder's notifications after marking apply as read:
{
start: 0,
end: 1,
total: 2,
notes: [
{
i: "0ABrTf1ml6YO_Ch7wnf92WfD",
dt: "2025-09-12T04:12:13.012539+00:00",
r: true,
a: {
r: "/exn/ipex/grant",
d: "ELOWFOpxTIMEle3DLOTlZWGQh3OXgePFXaKmwdHZla1R",
m: ""
}
},
{
i: "0AA8PpMsjBVmBpmvkV_i9iXp",
dt: "2025-09-12T04:12:19.041059+00:00",
r: true,
a: {
r: "/exn/ipex/apply",
d: "ECReKB0JTHwQ9eGlqYaK3lHlF5aBGKjsyek4iUkhkHE1",
m: ""
}
}
]
}
β
Holder notification processing complete.
### Step 3: Holder Finds Matching Credential
The Holder now needs to find a credential in their possession that satisfies the Verifier's `apply` request (matches the schema SAID and any specified attributes). The code below constructs a filter based on the `applyExchange` data and uses it to search the Holder's credentials.
The syntax of the `filter` attribute below sent to the `credentials().list(...)` call is intended to be similar to the MongoDB search syntax and is inspired by it.
```typescript
// The apply operation from the Verifier asks for a specific credential
// (matching schema and attribute values).
// This code snippet creates a credential filter based on the criteria
// from the applyExchange message received by the Holder.
let filter: { [x: string]: any } = { '-s': applyExchange.exn.a.s }; // Filter by schema SAID
// Add attribute filters from the apply request
for (const key in applyExchange.exn.a.a) { // 'a.a' contains the requested attributes
filter[`-a-${key}`] = applyExchange.exn.a.a[key];
}
console.log("Constructed filter for matching credentials:");
console.log(filter);
// Holder lists credentials matching the filter.
const matchingCredentials = await holderClient.credentials().list({ filter });
console.log("Matching credentials found by Holder:");
console.log(matchingCredentials); // Should list the EventPass credential issued earlier
console.log("\n\nβ
Matching credential complete.")
```
Constructed filter for matching credentials:
{
"-s": "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
"-a-eventName": "GLEIF Summit"
}
Matching credentials found by Holder:
[
{
sad: {
v: "ACDC10JSON0001c4_",
d: "EEr2KjMDimM-NzHvqpBjz-ZBGctjGZDK3WH2lndNlwCG",
i: "EGOrzLLCX0fN4XRHE_SQFBGu6sht8iO_l1VGCO7ag7AY",
ri: "EI1knJMnZANO17CvQPEQ5Hza0mWo4xE_JZ7guKtzyqfI",
s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
a: {
d: "EHX-zIC6mi2bqDC7sq9dCu8OgfuFoToMX_iL5q4rtqV_",
i: "EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG",
eventName: "GLEIF Summit",
accessLevel: "staff",
validDate: "2026-10-01",
dt: "2025-09-12T04:12:11.639000+00:00"
}
},
atc: "-IABEEr2KjMDimM-NzHvqpBjz-ZBGctjGZDK3WH2lndNlwCG0AAAAAAAAAAAAAAAAAAAAAAAEEr2KjMDimM-NzHvqpBjz-ZBGctjGZDK3WH2lndNlwCG",
iss: {
v: "KERI10JSON0000ed_",
t: "iss",
d: "EN-9Mt1MGzZmaTNwchPv-05uYd6LlR6oweGUNS5SGHHO",
i: "EEr2KjMDimM-NzHvqpBjz-ZBGctjGZDK3WH2lndNlwCG",
s: "0",
ri: "EI1knJMnZANO17CvQPEQ5Hza0mWo4xE_JZ7guKtzyqfI",
dt: "2025-09-12T04:12:11.639000+00:00"
},
issatc: "-VAS-GAB0AAAAAAAAAAAAAAAAAAAAAACEP6gx17zzX_JfzCPzEnp4ZuRJffPCauz6CyaZHur60KJ",
pre: "EGOrzLLCX0fN4XRHE_SQFBGu6sht8iO_l1VGCO7ag7AY",
schema: {
"$id": "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
"$schema": "http://json-schema.org/draft-07/schema#",
title: "EventPass",
description: "Event Pass Schema",
type: "object",
credentialType: "EventPassCred",
version: "1.0.0",
properties: {
v: { description: "Credential Version String", type: "string" },
d: { description: "Credential SAID", type: "string" },
u: { description: "One time use nonce", type: "string" },
i: { description: "Issuer AID", type: "string" },
ri: { description: "Registry SAID", type: "string" },
s: { description: "Schema SAID", type: "string" },
a: { oneOf: [Array] }
},
additionalProperties: false,
required: [ "v", "d", "i", "ri", "s", "a" ]
},
chains: [],
status: {
vn: [ 1, 0 ],
i: "EEr2KjMDimM-NzHvqpBjz-ZBGctjGZDK3WH2lndNlwCG",
s: "0",
d: "EN-9Mt1MGzZmaTNwchPv-05uYd6LlR6oweGUNS5SGHHO",
ri: "EI1knJMnZANO17CvQPEQ5Hza0mWo4xE_JZ7guKtzyqfI",
ra: {},
a: { s: 2, d: "EP6gx17zzX_JfzCPzEnp4ZuRJffPCauz6CyaZHur60KJ" },
dt: "2025-09-12T04:12:11.639000+00:00",
et: "iss"
},
anchor: {
pre: "EEr2KjMDimM-NzHvqpBjz-ZBGctjGZDK3WH2lndNlwCG",
sn: 0,
d: "EN-9Mt1MGzZmaTNwchPv-05uYd6LlR6oweGUNS5SGHHO"
},
anc: {
v: "KERI10JSON00013a_",
t: "ixn",
d: "EP6gx17zzX_JfzCPzEnp4ZuRJffPCauz6CyaZHur60KJ",
i: "EGOrzLLCX0fN4XRHE_SQFBGu6sht8iO_l1VGCO7ag7AY",
s: "2",
p: "EIbPRr2Z-97Wfw9OfEhrQkzFg7eAkxpAlsSMaZIj1-m-",
a: [
{
i: "EEr2KjMDimM-NzHvqpBjz-ZBGctjGZDK3WH2lndNlwCG",
s: "0",
d: "EN-9Mt1MGzZmaTNwchPv-05uYd6LlR6oweGUNS5SGHHO"
}
]
},
ancatc: [
"-VBq-AABAACTVQvz7zIGDw8XmxFd89gA31ONU34uNMNr2A9Dhltb1ILjeEb7e1c4_VDHC8zgrhX0KTxtCuNWiJnrnJGnNGcE-BADAADSXCDh_JwLGdkBFhn48oFLRygSzsyh7NFizHc2dDkDVmPR2UiaMrh8frthMj2uVu3ERmuK1NNMj88_lUKpf7QAABD6mI-fDyrdfDCmZjTqjybDpTcCqHhArd9B4Lj4GsVjeI3vZCyUIVyYoXSLGsZJUEY5K8CTg8SDR5CGIS9FceoBACDUx8j9C8Dv0q-scIpnK5sNFJq1vCaXPE6rlR_sVbpmz9XNxPwp0uphkWyC8Q-5sIAci3m-9w_c01j7Eo5LU0QJ-EAB0AAAAAAAAAAAAAAAAAAAAAAA1AAG2025-09-12T04c12c13d153540p00c00"
]
}
]
β
Matching credential complete.
### Step 4: Holder Offers Credential
Assuming a matching credential is found, the Holder prepares an IPEX offer message. This `offer` is intended to include only the metadata ACDC showing only the schema of the eventual full ACDC that will be presented later with an IPEX grant. This offer is sent back to the Verifier.
```typescript
// Holder prepares and submits an IPEX offer message with the matching credential.
// Prepare the IPEX offer message.
const [offer, sigsOffer, endOffer] = await holderClient.ipex().offer({
senderName: holderAidAlias, // Alias of the Holder's AID
recipient: verifierAid.i, // AID of the Verifier
acdc: new Serder(matchingCredentials[0].sad), // The ACDC being offered (first matching credential)
applySaid: applyExchangeSaid, // SAID of the Verifier's apply 'exn' message this offer is responding to
datetime: createTimestamp(), // Timestamp for the offer message
});
// Holder submits the prepared offer message to the Verifier.
const offerOperation = await holderClient
.ipex()
.submitOffer(holderAidAlias, offer, sigsOffer, endOffer, [
verifierAid.i, // Recipient AID
]);
console.log("Submitting Offer from holder to Verifier.")
// Wait for the submission operation to complete.
const offerResponse = await holderClient
.operations()
.wait(offerOperation, AbortSignal.timeout(DEFAULT_TIMEOUT_MS));
console.log("Holder submitted IPEX Offer to Verifier.");
// Clean up the operation.
await holderClient.operations().delete(offerOperation.name);
console.log("Holder deleted Offer operation.");
console.log("\n\nβ
Holder IPEX Offer complete.")
```
Submitting Offer from holder to Verifier.
Holder submitted IPEX Offer to Verifier.
Holder deleted Offer operation.
β
Holder IPEX Offer complete.
### Step 5: Verifier Receives Offer
The Verifier receives a notification for the Holder's `offer`. The Verifier retrieves the exchange details and marks the notification.
An offer is one of the three possible initiating IPEX actions along with Apply and Grant. An IPEX exchange may begin with an Offer to which a receiver, or disclosee, of the Offer would respond with an IPEX Agree, followed by a Grant and an Admit.
```typescript
// Verifier receives the IPEX offer notification from the Holder.
let verifierOfferNotifications;
// Retry loop for the Verifier to receive the offer notification.
for (let attempt = 1; attempt <= DEFAULT_RETRIES ; attempt++) {
try{
// List notifications, filtering for unread IPEX_OFFER_ROUTE messages.
verifierOfferNotifications = await verifierClient.notifications().list(
(n) => n.a.r === IPEX_OFFER_ROUTE && n.r === false
);
if(verifierOfferNotifications.notes.length === 0){
throw new Error("Offer notification not found"); // Throw error to trigger retry
}
break; // Exit loop if notification found
}
catch (error){
console.log(`[Retry] Offer notification not found for Verifier on attempt #${attempt} of ${DEFAULT_RETRIES}`);
if (attempt === DEFAULT_RETRIES) {
console.error(`[Retry] Max retries (${DEFAULT_RETRIES}) reached for Verifier's offer notification.`);
throw error;
}
console.log(`[Retry] Waiting ${DEFAULT_DELAY_MS}ms before next attempt...`);
await new Promise(resolve => setTimeout(resolve, DEFAULT_DELAY_MS));
}
}
const offerNotificationForVerifier = verifierOfferNotifications.notes[0]; // Assuming one notification
console.log("Verifier received Offer Notification:");
console.log(offerNotificationForVerifier);
// Retrieve the full IPEX offer exchange details.
const offerExchange = await verifierClient.exchanges().get(offerNotificationForVerifier.a.d);
console.log("\nDetails of Offer Exchange received by Verifier:");
console.log(offerExchange); // This will contain the ACDC presented by the Holder
// Extract the SAID of the offer 'exn' message for use in the agree.
let offerExchangeSaid = offerExchange.exn.d;
// Verifier marks the offer notification as read.
await verifierClient.notifications().mark(offerNotificationForVerifier.i);
console.log("\n\nVerifier's notifications after marking offer as read:");
console.log(await verifierClient.notifications().list());
console.log("\n\nβ
Verifier Offer notification handling complete.")
```
[Retry] Offer notification not found for Verifier on attempt #1 of 5
[Retry] Waiting 5000ms before next attempt...
Verifier received Offer Notification:
{
i: "0ABXiV1JPTPLLBfKgoa4MktC",
dt: "2025-09-12T04:12:24.663702+00:00",
r: false,
a: {
r: "/exn/ipex/offer",
d: "EBWG4vH1tKw7VZZU-sTF4ZU3TWdye-mQMUw2pn10dSwa",
m: ""
}
}
Details of Offer Exchange received by Verifier:
{
exn: {
v: "KERI10JSON000376_",
t: "exn",
d: "EBWG4vH1tKw7VZZU-sTF4ZU3TWdye-mQMUw2pn10dSwa",
i: "EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG",
rp: "ED-xfM6ATAg7mA3O94OQ_tN4Q7rbwl4v7N-WpEmzrfjy",
p: "ECReKB0JTHwQ9eGlqYaK3lHlF5aBGKjsyek4iUkhkHE1",
dt: "2025-09-12T04:12:24.231000+00:00",
r: "/ipex/offer",
q: {},
a: { i: "ED-xfM6ATAg7mA3O94OQ_tN4Q7rbwl4v7N-WpEmzrfjy", m: "" },
e: {
acdc: {
v: "ACDC10JSON0001c4_",
d: "EEr2KjMDimM-NzHvqpBjz-ZBGctjGZDK3WH2lndNlwCG",
i: "EGOrzLLCX0fN4XRHE_SQFBGu6sht8iO_l1VGCO7ag7AY",
ri: "EI1knJMnZANO17CvQPEQ5Hza0mWo4xE_JZ7guKtzyqfI",
s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
a: {
d: "EHX-zIC6mi2bqDC7sq9dCu8OgfuFoToMX_iL5q4rtqV_",
i: "EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG",
eventName: "GLEIF Summit",
accessLevel: "staff",
validDate: "2026-10-01",
dt: "2025-09-12T04:12:11.639000+00:00"
}
},
d: "ECDNeO6oSRg9MRgC5eMchaxsC6l2a84DpRNnK-aQagSX"
}
},
pathed: {}
}
Verifier's notifications after marking offer as read:
{
start: 0,
end: 0,
total: 1,
notes: [
{
i: "0ABXiV1JPTPLLBfKgoa4MktC",
dt: "2025-09-12T04:12:24.663702+00:00",
r: true,
a: {
r: "/exn/ipex/offer",
d: "EBWG4vH1tKw7VZZU-sTF4ZU3TWdye-mQMUw2pn10dSwa",
m: ""
}
}
]
}
β
Verifier Offer notification handling complete.
### Step 6: Verifier Agrees and Validates
Next, the Verifier, after validating the offered metadata ACDC credential (which signify-ts does implicitly upon processing the offer and preparing the agree), will send an IPEX agree message back to the Holder. This confirms successful receipt and validation of the metadata ACDC credential presented. This means that the verifier has agreed that the schema of the data being sent back is acceptable to the verifier. The actual data is shared later in the IPEX Grant step.
```typescript
// Verifier prepares and submits an IPEX agree message.
// Prepare the IPEX agree message.
const [agree, sigsAgree, _endAgree] = await verifierClient.ipex().agree({
senderName: verifierAidAlias, // Alias of the Verifier's AID
recipient: holderAid.i, // AID of the Holder
offerSaid: offerExchangeSaid, // SAID of the Holder's offer 'exn' message this agree is responding to
datetime: createTimestamp(), // Timestamp for the agree message
});
// Verifier submits the prepared agree message to the Holder.
const agreeOperation = await verifierClient
.ipex()
.submitAgree(verifierAidAlias, agree, sigsAgree, [holderAid.i]);
console.log("Verifier submitted IPEX Agree to Holder")
// Wait for the submission operation to complete.
const agreeResponse = await verifierClient
.operations()
.wait(agreeOperation, AbortSignal.timeout(DEFAULT_TIMEOUT_MS));
console.log("Verifier IPEX Agree sent");
// Clean up the operation.
await verifierClient.operations().delete(agreeOperation.name);
console.log("Verifier deleted Agree operation");
console.log("\n\nβ
Verifier IPEX Agree complete.")
// At this point, the Verifier has successfully received and validated the metadata ACDC credential.
```
Verifier submitted IPEX Agree to Holder
Verifier IPEX Agree sent
Verifier deleted Agree operation
β
Verifier IPEX Agree complete.
### Step 7: Holder shares credential with IPEX Grant
The act of sharing a credential and its data with the verifier happens with an IPEX Grant as shown below. This can be the first operation in a chain of IPEX operations or it can be performed after an IPEX Agree. In this case the grant occurs after an agree so we will chain to the prior agreement.
```typescript
// Holder - get credential (with all its data)
const credential = await holderClient.credentials().get(credentialSaid);
// Holder - Ipex grant
console.log("Granting credential from holder to issuer")
const grantResponse = await ipexGrantCredential(
holderClient,
holderAidAlias,
verifierAid.i,
credential
)
console.log("β
Holder granted credential.")
```
Granting credential from holder to issuer
AID "holderAid" granting credential to AID "ED-xfM6ATAg7mA3O94OQ_tN4Q7rbwl4v7N-WpEmzrfjy" via IPEX...
Successfully submitted IPEX grant from "holderAid" to "ED-xfM6ATAg7mA3O94OQ_tN4Q7rbwl4v7N-WpEmzrfjy".
β
Holder granted credential.
After the holder sends the IPEX Grant then the verifier will receive two things:
- The notification of the IPEX Grant
- An IPEX Grant, which is an exchange message, abbreviated as `exn`.
The notification message contains a digest of the `exn` IPEX Grant message, which contains the presented ACDC as an embedded data property. So, to retrieve the ACDC the grant `exn` digest should be retrieved from the `a.d` property of the notification and used to load the
```typescript
// Verifier - Wait for grant notification
console.log("Verifier waiting for credential")
const grantNotifications = await waitForAndGetNotification(verifierClient, IPEX_GRANT_ROUTE)
const grantNotification = grantNotifications[0]
console.log("Verifier received IPEX Grant notification", grantNotification)
// Retrieve the full IPEX offer exchange details.
const grantExn = await verifierClient.exchanges().get(grantNotification.a.d);
console.log("Details of ACDC embedded in the Grant Exchange received by Verifier:");
const embeddedACDC = grantExn.exn.e.acdc;
console.log(embeddedACDC); // This will contain the ACDC presented by the Holder
console.log("\n\nβ
Verifier IPEX Grant notification processing complete.")
```
Verifier waiting for credential
Waiting for notification with route "/exn/ipex/grant"...
[Retry] Grant notification not found on attempt #1 of 5
[Retry] Waiting 5000ms before next attempt...
Verifier received IPEX Grant notification {
i: "0ACTl0UOevpcP-X4ZLv8Poiu",
dt: "2025-09-12T04:12:30.597778+00:00",
r: false,
a: {
r: "/exn/ipex/grant",
d: "ECDH8_eQ0jPpBEnn4IjSJxtSCGmT0jUSQh2qN1zlFwBo",
m: ""
}
}
Details of ACDC embedded in the Grant Exchange received by Verifier:
{
v: "ACDC10JSON0001c4_",
d: "EEr2KjMDimM-NzHvqpBjz-ZBGctjGZDK3WH2lndNlwCG",
i: "EGOrzLLCX0fN4XRHE_SQFBGu6sht8iO_l1VGCO7ag7AY",
ri: "EI1knJMnZANO17CvQPEQ5Hza0mWo4xE_JZ7guKtzyqfI",
s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK",
a: {
d: "EHX-zIC6mi2bqDC7sq9dCu8OgfuFoToMX_iL5q4rtqV_",
i: "EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG",
eventName: "GLEIF Summit",
accessLevel: "staff",
validDate: "2026-10-01",
dt: "2025-09-12T04:12:11.639000+00:00"
}
}
β
Verifier IPEX Grant notification processing complete.
As you see the ACDC has already been received by the verifier.
This means that the verifier could act on the ACDC directly after receiving the Exchange message for the IPEX Grant containing the ACDC. Sending back an IPEX Admit message is entirely optional and is left up to the architectural design preferences of the individual application implementor. The [vLEI Reporting API verifier (sally)](https://github.com/GLEIF-IT/sally/) used by GLEIF in production follows the model of extracting the ACDC from the IPEX Grant `exn` and does not send back an IPEX Admit message because sending the admit, in this use case, does not yet provide business value. That might change in the future.
This demonstration shows completing the entire formal IPEX workflow by using an IPEX Admit to respond to the IPEX Grant.
### Step 8: Verifier Sends IPEX Admit to the Holder
While a verifier does not have to explicitly admit a credential doing so may provide valuable information to the holder or issuer depending on the use case so that workflow is shown below.
```typescript
// Verifier - Admit Grant
const admitResponse = await ipexAdmitGrant(
verifierClient,
verifierAidAlias,
holderAid.i,
grantNotification.a.d
)
console.log("Verifier admitting credential")
// Verifier - Mark notification
await markNotificationRead(verifierClient, grantNotification.i)
console.log("\nβ
Verifier marked notification read")
```
AID "verifierAid" admitting IPEX grant "ECDH8_eQ0jPpBEnn4IjSJxtSCGmT0jUSQh2qN1zlFwBo" from AID "EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG"...
Successfully submitted IPEX admit for grant "ECDH8_eQ0jPpBEnn4IjSJxtSCGmT0jUSQh2qN1zlFwBo".
Verifier admitting credential
Marking notification "0ACTl0UOevpcP-X4ZLv8Poiu" as read...
Notification "0ACTl0UOevpcP-X4ZLv8Poiu" marked as read.
β
Verifier marked notification read
You can now view the Verifier's list of notifications to see that the Grant notification has been marked as read.
```typescript
// Verifier shows Grant notification is now read
let notifications;
// Retry loop to fetch notifications.
for (let attempt = 1; attempt <= DEFAULT_RETRIES ; attempt++) {
try{
// List notifications, filtering for unread IPEX_GRANT_ROUTE messages.
let allNotifications = await verifierClient.notifications().list( );
notifications = allNotifications.notes.filter(
(n) => n.a.r === IPEX_GRANT_ROUTE // get all notifications even if read
)
if(notifications.length === 0){
throw new Error("Grant notification not found"); // Throw error to trigger retry
}
console.log("Found a notification for an IPEX Grant");
break;
}
catch (error){
console.log(`[Retry] Grant notification not found on attempt #${attempt} of ${DEFAULT_RETRIES}`);
if (attempt === DEFAULT_RETRIES) {
console.error(`[Retry] Max retries (${DEFAULT_RETRIES}) reached for grant notification.`);
throw error;
}
console.log(`[Retry] Waiting ${DEFAULT_DELAY_MS}ms before next attempt...`);
await new Promise(resolve => setTimeout(resolve, DEFAULT_DELAY_MS));
}
}
const grantNote = notifications[0] // Assuming only one grant notification for simplicity
console.log("β
Existing grant notification for verifier now shows as read with 'r: true'");
console.log(grantNote);
```
Found a notification for an IPEX Grant
β
Existing grant notification for verifier now shows as read with 'r: true'
{
i: "0ACTl0UOevpcP-X4ZLv8Poiu",
dt: "2025-09-12T04:12:30.597778+00:00",
r: true,
a: {
r: "/exn/ipex/grant",
d: "ECDH8_eQ0jPpBEnn4IjSJxtSCGmT0jUSQh2qN1zlFwBo",
m: ""
}
}
Lastly the holder receives the admit which also means receiving a notification of the IPEX Admit. This notification can be marked as read to signify that the entire formal process is complete as shown below.
```typescript
// Holder - Wait for admit notification
console.log("Holder receiving admit...")
const admitNotifications = await waitForAndGetNotification(holderClient, IPEX_ADMIT_ROUTE)
const admitNotification = admitNotifications[0]
// Issuer - Mark notification
await markNotificationRead(holderClient, admitNotification.i)
console.log("\n\nβ
Holder received admit. Presentation exchange complete.")
```
Holder receiving admit...
Waiting for notification with route "/exn/ipex/admit"...
Marking notification "0AAkUq24By8Kf_s2_TWQtWPh" as read...
Notification "0AAkUq24By8Kf_s2_TWQtWPh" marked as read.
β
Holder received admit. Presentation exchange complete.
## Credential Revocation by Issuer
Circumstances may require a credential to be invalidated before its intended expiry or if it has no expiry. This process is known as revocation. Only the original Issuer of a credential can revoke it. Revocation involves the Issuer recording a revocation event in the specific credential's Transaction Event Log (TEL), which is part of the Issuer's credential database. This event is, like all TEL events, anchored to the Issuer's KEL. The issuer may directly tell the holder of the revocation status, which the holder may, in turn, directly tell a verifier, using the IPEX Grant and Admit steps.
The Issuer uses the `issuerClient.credentials().revoke()` method, specifying the alias of their issuing AID and the SAID of the credential to be revoked. This action creates a new event in the TEL associated with the credential, marking its status as revoked.
First, check the credential status before revocation. The status object contains details about the latest event in the credential's TEL. The `et` field indicates the event type (e.g., `iss` for issuance).
```typescript
// Log the credential's status from the Issuer's perspective before revocation.
// The 'status' field shows the latest event in the credential's Transaction Event Log (TEL).
// 'et: "iss"' indicates it's currently in an issued state.
const statusBefore = (await issuerClient.credentials().get(credentialSaid)).status;
console.log("Credential status before revocation:", statusBefore);
// Issuer revokes the credential.
// This creates a revocation event in the credential's TEL within the Issuer's registry.
const revokeResult = await issuerClient.credentials().revoke(issuerAidAlias, credentialSaid); // Changed from revokeOperation to revokeResult to get .op
const revokeOperation = revokeResult.op; // Get the operation from the result
// Wait for the revocation operation to complete.
const revokeResponse = await issuerClient
.operations()
.wait(revokeOperation, AbortSignal.timeout(DEFAULT_TIMEOUT_MS)); // Used revokeOperation directly
// Log the credential status after revocation.
// Note the 'et: "rev"' indicating it's now revoked, and the sequence number 's' has incremented.
const statusAfter = (await issuerClient.credentials().get(credentialSaid)).status;
console.log("β
Credential status after revocation:", statusAfter);
```
Credential status before revocation: {
vn: [ 1, 0 ],
i: "EEr2KjMDimM-NzHvqpBjz-ZBGctjGZDK3WH2lndNlwCG",
s: "0",
d: "EN-9Mt1MGzZmaTNwchPv-05uYd6LlR6oweGUNS5SGHHO",
ri: "EI1knJMnZANO17CvQPEQ5Hza0mWo4xE_JZ7guKtzyqfI",
ra: {},
a: { s: 2, d: "EP6gx17zzX_JfzCPzEnp4ZuRJffPCauz6CyaZHur60KJ" },
dt: "2025-09-12T04:12:11.639000+00:00",
et: "iss"
}
β
Credential status after revocation: {
vn: [ 1, 0 ],
i: "EEr2KjMDimM-NzHvqpBjz-ZBGctjGZDK3WH2lndNlwCG",
s: "1",
d: "ENWjDrT1_mqdEtpR5cohaSLwjXpPMAp3t94FV5AublLJ",
ri: "EI1knJMnZANO17CvQPEQ5Hza0mWo4xE_JZ7guKtzyqfI",
ra: {},
a: { s: 3, d: "ECtamOP5KYCURrHyzNfeAdDIFTqhFCN5air7Y1y2vWuq" },
dt: "2025-09-12T04:12:36.315000+00:00",
et: "rev"
}
The output shows the change in the credential's status object:
- Before revocation, `et` (event type) was `iss`.
- After revocation, `et` is `rev`.
- The sequence number `s` of the TEL event also increments, reflecting the new event.
- The digest d of the event changes, as it's a new event.
This demonstrates that the Issuer has successfully updated the credential's status in their registry. Anyone (like a Verifier) who subsequently checks this registry for the credential's status will see that it has been revoked.
### Propagating revocation state to Holders and Verifiers
Knowing issuance and revocation state for ACDC credentials comprises an essential part of some use cases and so propagating revocation state to verifiers, and possibly holders becomes an important workflow. Direct transmission of revocation state is one way of propagating this state between either an issue and a holder, between an issuer and a verifier, or between a holder and a verifier. This direct transmission may be accomplished with an IPEX Grant. Again, for the verifier the IPEX Admit in response to this Grant is optional, yet for the Holder to show the credential as revoked in their KERIA Agent database they must send an IPEX admit.
#### Directly Sending Revocation State from Issuer to Holder
Now that the credential has been shown to be revoked in the issuer's database you can re-grant it to the Holder to propagate the revocation state to the holder. This process may be used with the verifier as well to inform the verifier of credential revocation.
```typescript
// Issuer - get credential (with all its data)
const credential = await issuerClient.credentials().get(credentialSaid);
console.log(`Issuer credential state (iss = issued, rev = revoked): ${credential.status.et}`);
```
Issuer credential state (iss = issued, rev = revoked): rev
The issuer then re-grants this credential to the holder.
```typescript
// Issuer - Ipex grant
console.log("granting credential to holder")
const grantResponse = await ipexGrantCredential(
issuerClient,
issuerAidAlias,
holderAid.i,
credential
)
console.log("β
Issuer created and granted credential.")
```
granting credential to holder
AID "issuerAid" granting credential to AID "EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG" via IPEX...
Successfully submitted IPEX grant from "issuerAid" to "EDTeQiSu_8hLVGyQ04C9-p5k3WPdfrmi1WT9XXyWXMJG".
β
Issuer created and granted credential.
After being granted the credential the holder may take the grant notification and admit the credential which will cause the credential to show as revoked in the Holder's database.
```typescript
// Holder - Wait for grant notification
console.log("Holder waiting for credential")
const grantNotifications = await waitForAndGetNotification(holderClient, IPEX_GRANT_ROUTE)
const grantNotification = grantNotifications[0]
// Holder - Admit Grant
const admitResponse = await ipexAdmitGrant(
holderClient,
holderAidAlias,
issuerAid.i,
grantNotification.a.d
)
console.log("Holder admitting credential")
// Holder - Mark notification
await markNotificationRead(holderClient, grantNotification.i)
const credential = await holderClient.credentials().get(credentialSaid);
console.log(`β
Holder credential state (iss = issued, rev = revoked): ${credential.status.et}`);
```
Holder waiting for credential
Waiting for notification with route "/exn/ipex/grant"...
[Retry] Grant notification not found on attempt #1 of 5
[Retry] Waiting 5000ms before next attempt...
AID "holderAid" admitting IPEX grant "EMTj0CR8ErK2R5HxSFDfbXniC4Uf8BlCQku7ekjamDjA" from AID "EGOrzLLCX0fN4XRHE_SQFBGu6sht8iO_l1VGCO7ag7AY"...
Successfully submitted IPEX admit for grant "EMTj0CR8ErK2R5HxSFDfbXniC4Uf8BlCQku7ekjamDjA".
Holder admitting credential
Marking notification "0ADj6EemnDUGvDoxzMnGrGrH" as read...
Notification "0ADj6EemnDUGvDoxzMnGrGrH" marked as read.
β
Holder credential state (iss = issued, rev = revoked): rev
Now that the Holder has received the Grant and sent back an Admit then the issuer may mark as read the notification of the Admit.
```typescript
// Issuer - Wait for admit notification
console.log("Issuer receiving admit...")
const admitNotifications = await waitForAndGetNotification(issuerClient, IPEX_ADMIT_ROUTE)
const admitNotification = admitNotifications[0]
// Issuer - Mark notification
await markNotificationRead(issuerClient, admitNotification.i)
console.log("\n\nβ
Issuer received admit. Propagation of revocation state to holder complete.")
```
Issuer receiving admit...
Waiting for notification with route "/exn/ipex/admit"...
Marking notification "0ADUsXMBGdNd_-hLL4cOaXos" as read...
Notification "0ADUsXMBGdNd_-hLL4cOaXos" marked as read.
β
Issuer received admit. Propagation of revocation state to holder complete.
#### Directly Sending Revocation State from Issuer to Verifier
The issuer may send revocation state directly to the verifier as shown below. There is an alternative flow using what are known as Observers that will be explained in an upcoming training.
```typescript
// Issuer - Ipex grant
console.log("granting credential to verifier")
const grantResponse = await ipexGrantCredential(
issuerClient,
issuerAidAlias,
verifierAid.i,
credential
)
console.log("β
Issuer created and granted credential.")
```
granting credential to verifier
AID "issuerAid" granting credential to AID "ED-xfM6ATAg7mA3O94OQ_tN4Q7rbwl4v7N-WpEmzrfjy" via IPEX...
Successfully submitted IPEX grant from "issuerAid" to "ED-xfM6ATAg7mA3O94OQ_tN4Q7rbwl4v7N-WpEmzrfjy".
β
Issuer created and granted credential.
Finally, the verifier may process the notification of the re-granted credential and send back an IPEX Admit to the Holder, yet, again, the necessity of sending the Admit depends on the use case. The verifier has received the revocation state by virtue of receiving and processing the IPEX Grant following the revocation of the credential and the holder learning about the revocation of the credential state.
In use cases needing end-verifiability and signing of the reception of credentials then IPEX Admits must ALWAYS be sent since they represent the credential receiver cryptographically signing their acceptance of a credential presentation. This may be required for legal certainty on the terms of data sharing expressed in the rules section of an ACDC or other terms such as those in a privacy policy or terms of service for an application or service. An IPEX Admit is useful when the legal requirements of a use case include the need for a receiver of a credential (holder) or a credential presentation (verifier) to sign that they received it.
```typescript
// Verifier - Wait for grant notification
console.log("Verifier waiting for credential")
const grantNotifications = await waitForAndGetNotification(verifierClient, IPEX_GRANT_ROUTE)
const grantNotification = grantNotifications[0]
// Verifier - Admit Grant
const admitResponse = await ipexAdmitGrant(
verifierClient,
verifierAidAlias,
issuerAid.i,
grantNotification.a.d
)
console.log("Verifier admitting credential")
// Verifier - Mark notification
await markNotificationRead(verifierClient, grantNotification.i)
```
Verifier waiting for credential
Waiting for notification with route "/exn/ipex/grant"...
[Retry] Grant notification not found on attempt #1 of 5
[Retry] Waiting 5000ms before next attempt...
AID "verifierAid" admitting IPEX grant "EPiaeSv6ix1gyVkACzKkC2MUZOp4CDFUf5wDcmCdmFzH" from AID "EGOrzLLCX0fN4XRHE_SQFBGu6sht8iO_l1VGCO7ag7AY"...
Successfully submitted IPEX admit for grant "EPiaeSv6ix1gyVkACzKkC2MUZOp4CDFUf5wDcmCdmFzH".
Verifier admitting credential
Marking notification "0ADNeJ1Swux_flAxknJRASip" as read...
Notification "0ADNeJ1Swux_flAxknJRASip" marked as read.
As noted above, the revocation state shows up prior to sending the IPEX Admit. You can run the below code snippet prior to sending the IPEX Admit and you will see that the credential state shows up as revoked even though the Admit has not yet been sent.
```typescript
// You can run this prior to sending the IPEX Admit to see that the credential state is already
// revoked before sending the Admit.
// You can also run this after sending the Admit. The result is the same before or after.
const credential = await verifierClient.credentials().get(credentialSaid);
console.log(`β
Verifier credential state (iss = issued, rev = revoked): ${credential.status.et}`);
```
β
Verifier credential state (iss = issued, rev = revoked): rev
#### Discovering Revocation State
Typically the responsibility for discovering revocation state would be stay with the verifier rather than with the issuer. The verifier is usually a subscriber to revocation state of credentials it depends on. The best way to accomplish this is with the equivalent of a Watcher network yet for credentials which, in the case of ACDCs, are called Observers. A verifier would have Observers set up that watch any issuers of credentials it accepts, or any propagation networks such as a global DHT of issuers, KEL state, and ACDC state, so that the verifier can be automatically informed of credential state within seconds, or sub-seconds, once an issuer publishes a revocation event.
So, what is shown in this demonstration of the issuer sending revocation state to the verifier is not scalable and is not appropriate for production. It is only suitable for toy or proof of concept projects. A production implementation would use observers.
### Current state of Observers in the KERI ecosystem
A barebones implementation of Observers as a feature of a witness for an issuer exist in the KERI ecosystem in the [keripy](https://github.com/WebOfTrust/keripy) implementation of witnesses. Those witnesses expose a `/query` endpoint for polling the state of an ACDC. An upcoming training shows how to use this functionality and how to incorporate it into a verifier so that using ACDCs and monitoring revocation state is seamless and with as few steps for the user as possible.
Next we cover the final topic for this particular training, cleaning up the holder database by deleting revoked credentials.
## Deleting Revoked Credentials from the Holder Database
Once a Holder becomes aware that a credential they possess has been revoked (e.g., by checking its status in the Issuer's registry or being informed through other means), it may want to delete that credential from its database. This is a design choice left up to the developers and architects of a given system. Deletion of credentials after revocation is not required and may not be desirable from a recordkeeping standpoint, yet if needed it can be a useful way to clean up credential stores and is a simple way to prevent credential holders from accidentally presenting revoked credentials to verifiers.
The `holderClient.credentials().delete()` method removes the credential from the Holder's local client storage.
```typescript
// Holder deletes the (now revoked) credential from their local store.
await holderClient.credentials().delete(credentialSaid);
// Verify the credential is no longer in the Holder's list.
console.log("Holder's credential list after deleting the revoked credential:");
console.log(await holderClient.credentials().list()); // Should be an empty array or not contain the revoked credential
```
Holder's credential list after deleting the revoked credential:
[]
π SUMMARY
This notebook demonstrated ACDC Presentation and Revocation steps using IPEX with KERIA and Signify TS:
- Full Formal IPEX flow: the entire IPEX set of verbs including apply, offer, agree, grant, and admit may be used to orchestrate the disclosure process
- Partial flow for presentation: an abbreviated IPEX flow with only grant and admit may be used to share credentials. This is the most common workflow.
- Simple sharing: only an IPEX Grant is needed to share credentials as a verifier does not need to reply with an IPEX Admit, which permits the simplest presentation flow from either an issuer or holder to a verifier.
- Revocation: After a successful credential creation the issuer may choose to revoke a credential. Propagating this state to a holder or a verifier may be directly performed with another IPEX Grant and Admit or may be discovered through observer infrastructure.
The IPEX credential presentation and revocation flows are a critical part of the value add the whole vLEI protocol stack, KERI, ACDC, and CESR, stand to provide as the basis for data sharing with verifiable credentials. This training walked you through both the presentation and revocation workflows.
[<- Prev (KERIA Signify Credential Issuance)](102_20_KERIA_Signify_Credential_Issuance.ipynb) | [Next (Third Party Tools) ->](102_30_Third_Party_Tools.ipynb)
# Third-Party Tools
π― OBJECTIVE
Introduce third-party open-source projects that leverage the Key Event Receipt Infrastructure (KERI) and Authentic Chained Data Containers (ACDC) protocols, providing tools for managing identifiers and verifiable credentials.
## Open Source
The KERI and ACDC protocols provide a foundation for decentralized identity. A growing ecosystem of open-source tools is being built on these protocols, offering functionalities for developers and end-users. This section highlights a few notable third-party projects that integrate with KERI-based ecosystems.
### Signify Browser Extension
The signify-browser-extension is a browser-based wallet that integrates with web applications to manage KERI Autonomic Identifiers (AIDs) and interact with KERI-based ecosystems. It allows users to approve operations (like signing data or consenting to credential exchanges) directly from their browser.
GitHub repository: https://github.com/WebOfTrust/signify-browser-extension
### KERI Foundation Wallet (SparΓ‘n)
SparΓ‘n offers a graphical user interface (GUI) for managing local KERI identifiers and cryptographic keys. It provides an alternative to the command-line interface (kli) for KERI operations. SparΓ‘n is built on top of the KERI library, a custom KERI agent runtime, and the Flet application framework.
GitHub Repository: https://github.com/keri-foundation/wallet
### Veridian Wallet
Veridian Wallet is an open-source application developed by the Cardano Foundation. It offers features for Identifier and credential management in a mobile app (Android & iOS).
GitHub Repository: https://github.com/cardano-foundation/veridian-wallet
[<- Prev (KERIA Signify Credential Presentation and Revocation)](102_25_KERIA_Signify_Credential_Presentation_and_Revocation.ipynb) | [Next (vLEI Ecosystem) ->](103_05_vLEI_Ecosystem.ipynb)
# The GLEIF verifiable LEI (vLEI) Ecosystem
π― OBJECTIVE
To provide a theoretical understanding of the verifiable Legal Entity Identifier (vLEI) ecosystem. Including its architecture, the role of GLEIF as the Root of Trust, the underlying KERI and ACDC principles that enable its security and verifiability, and governance aspects.
## Introduction to the vLEI
The verifiable Legal Entity Identifier (vLEI) system, led by the Global Legal Entity Identifier Foundation (GLEIF), is a new kind of authentication (AuthN) and authorization (AuthZ) technology stack, representing a significant advancement in digital organizational identity capability. It extends the traditional, widely adopted Legal Entity Identifier (LEI) ecosystem into the digital realm, enabling secure, certain, and verifiable organizational identity in digital interactions. The core purpose of the vLEI is to address critical challenges in the digital world, such as a strong, secure identity assurance, preventing identity impersonation and fraud, and the overall need for trustworthy authentication of organizations for business activity.
The vLEI leverages the robust security and verifiability features of the Key Event Receipt Infrastructure (KERI) and Authentic Chained Data Containers (ACDCs) protocols. KERI provides the foundation for self-certifying, decentralized identifiers (AIDs), while ACDCs serve as the data sharing format as verifiable credentials, representing the vLEI itself and associated identity and data attestations. This combination allows for automated cryptographic verification of an organization's identity, reducing risks and costs associated with traditional identity validation methods.
The vLEI ecosystem is designed for a wide range of digital business activities, including, but not limited to:
- Approving business transactions and contracts with individualized attestations (signatures and credentials)
- Securely onboarding customers with reusable identity
- Reducing counterparty risk through automatable due diligence
- Facilitating trusted interactions within import/export and supply chain networks, again with reusable identity
- Streamlining regulatory filings and reports by tying them to individual signatures from organizational actors
## The GLEIF vLEI Ecosystem Architecture
The vLEI ecosystem is structured as a hierarchical chain of trust, with GLEIF positioned at its apex as shown in the below diagram, followed by qualified vLEI issuers (QVIs), legal entities that receive LEIs and vLEI credentials, and people within legal entities acting in official or contextual roles. This architecture ensures that all vLEIs and the authorities of entities issuing them can be cryptographically verified back to a common, trusted root.
### GLEIF as the Root of Trust
GLEIF serves as the ultimate Root of Trust in the vLEI ecosystem. This role is anchored by the **GLEIF Root AID**, a KERI-based identifier meticulously established and managed by GLEIF. The generation and administration of the GLEIF Root AID, along with its delegated AIDs, are governed by stringent policies emphasizing the highest duty of care, the use of self-certifying autonomic identifiers, and a strong cryptographic foundation.
From the GLEIF Root AID, GLEIF establishes delegated AIDs for its operational purposes, such as:
- **GLEIF External Delegated AID (GEDA)**: Used by GLEIF to manage its relationship with and authorize **Qualified vLEI Issuers (QVIs)**.
The genesis of these core GLEIF AIDs involves a rigorous, multi-party ceremony with multiple **GLEIF Authorized Representatives (GARs)**, employing Out-of-Band Interaction (OOBI) sessions and challenge-response mechanisms to ensure authenticity and security.
### The Chain of Verifiable Authority
As shown above, and as abbreviated in the below image, the vLEI ecosystem operates on a clear cryptographic chain of verifiable trust, detailing how authority and credentials flow from GLEIF down to individual representatives of legal entities.

1. **GLEIF to Qualified vLEI Issuers (QVIs):**
The chain begins with GLEIF enabling **Qualified vLEI Issuers (QVIs)**. QVIs are organizations formally accredited by GLEIF to issue vLEI credentials to Legal Entities. They act as crucial intermediaries, extending GLEIF's trust into the broader ecosystem. GLEIF, through its GEDA, establishes a QVI's authority and operational capability by providing two key components:
- A **QVI Delegated AID**: This is a KERI AID for the QVI, cryptographically delegated from GLEIF's own authority. The QVI uses this delegated AID for its operations within the vLEI ecosystem.
- The **QVI vLEI Credential**: GLEIF issues this specific ACDC to the QVI. It serves as the QVI's formal, verifiable authorization from GLEIF, attesting to its status and its right to issue vLEI credentials to other legal entities.
1. **QVI to Legal Entity:**
Entitled by its delegated AID and its **Qualified vLEI Issuer vLEI Credential** from GLEIF, the QVI then issues a **Legal Entity vLEI Credential** to an organization (a Legal Entity). This ACDC represents the verified digital identity of the Legal Entity. To maintain the integrity of the trust chain, the Legal Entity vLEI Credential issued by the QVI includes a cryptographic link back to the "Qualified vLEI Issuer vLEI Credential" held by the issuing QVI. The Legal Entity itself, through its Legal Entity Authorized Representatives (LARs), creates and manages its own AID to which this credential is issued.
1. **Legal Entity and QVI in Issuance of Role Credentials:**
Once a Legal Entity holds its own valid Legal Entity vLEI Credential (and by extension, controls its own KERI AID), credentials can be issued to individuals representing the organization in various official or functional capacities. The issuance mechanism for these role credentials varies:
- **Legal Entity Official Organizational Role (OOR) vLEI Credentials**: These are for individuals in formally recognized official roles within the Legal Entity (e.g., CEO, Director). OOR vLEI Credentials are issued by a QVI, contracted by the Legal Entity for this purpose.
- **Legal Entity Engagement Context Role (ECR) vLEI Credentials**: These are for individuals representing the Legal Entity in other specific engagements or functional contexts. ECR vLEI Credentials can be issued either by a QVI (contracted by the Legal Entity) or directly by the Legal Entity itself. In all cases, these role credentials cryptographically link the individual, acting in their specified role, back to the Legal Entity's vLEI, thereby extending the verifiable chain of authority and context.
- In each of these cases the actual OOR or ECR is issued by a QVI on request by a specific legal entity. Taking the form of OOR Authorization and ECR Authorization credentials, the legal entity makes a request to a QVI that an OOR or ECR credential be issued to a given person.
This layered delegation and credential issuance process ensures that the authority for each credential can be cryptographically verified up the chain, ultimately anchored with GLEIF as the Root of Trust for the entire vLEI ecosystem.
## Core Technical Principles
The vLEI ecosystem is built upon several core KERI principles and governance requirements to ensure its security, interoperability, and trustworthiness.
### Self-Certifying Identifiers (AIDs)
All identifiers within the vLEI ecosystem are KERI Autonomic Identifiers (AIDs). AIDs are self-certifying, meaning their authenticity can be verified directly using cryptography alone, without reliance on a central registry for the identifier itself. An AID is cryptographically bound to key pairs controlled by an entity. Both transferable AIDs (whose control can be rotated to new keys) and non-transferable AIDs (e.g., for witnesses) are used in the vLEI ecosystem.
### Key Management and Security
KERI's advanced key management features are integral to the vLEI ecosystem's security:
- **Pre-rotation:** KERI's pre-rotation mechanism is employed, where the commitment to the next set of keys is made in the current key establishment event (inception or rotation). This enhances security by ensuring that new keys are not exposed until they are actively used for rotation.
- **Multi-signature (Multi-sig):** Multi-sig control is extensively used as a form of multi-party computation (MPC), especially for critical identities like the GLEIF Root AID and QVI AIDs. This requires signatures from multiple authorized parties to approve an event, significantly increasing resilience against compromise.
- **Cooperative Delegation:** KERI's cooperative delegation model is used for delegating AIDs (e.g., GLEIF delegating to QVIs). This requires cryptographic commitment from both the delegator and the delegate, enhancing security as an attacker would need to compromise keys from both entities.
### Use of KERI Infrastructure
The vLEI ecosystem relies on standard KERI infrastructure components:
- **Witnesses:** These are entities designated by an AID controller to receive, verify, sign (receipt), and store key events. They ensure the availability and consistency of Key Event Logs (KELs) for signers. GLEIF maintains its own Witness pool for its AIDs, and QVIs also utilize witnesses.
- Currently (June 2025) witnesses are also used as mailboxes, a store and forward communication relay similar to DIDComm Relays (formerly known as Mediators).
- **Watchers:** Entities that keep copies of KELs (or Key Event Receipt Logs - KERLs) to independently verify the state of AIDs. Verifiers and Validators may use Watcher networks to protect the integrity of their verification process. You can think of Watchers as primarily verification infrastructure. Similar to how witnesses provide a signing threshold, watchers may be used to provide a verification threshold as a part of a verification process or workflow.
- **Mailboxes:** The always-online store and forward mechanism for AID controllers to receive messages even when the controlling device is offline or unavailable.
- As stated above, mailboxes are currently deployed with witnesses. Work is underway to provide an open-source, production grade, multi-tenant, standalone mailbox service. There are infrastructure vendors who provide such standalonemailbox services.
### ACDC and Schema Requirements
vLEIs are implemented as Authentic Chained Data Containers (ACDCs).
- **Structure:** ACDCs have a defined structure including an envelope (metadata) and payload (attributes, and optionally, edges and rules).
- **SAIDs:** Both ACDCs and their schemas are identified by Self-Addressing Identifiers (SAIDs), which are cryptographic digests of their content, ensuring tamper-evidence and cryptographic integrity.
- **Schemas:** All vLEI credentials adhere to official JSON Schemas published by GLEIF. These schemas are also SAIDified and versioned. The schema registry provides the SAIDs and URLs for these official schemas.
- **Serialization:** JSON serialization is mandatory for vLEI credentials.
- **Proof Format:** Signatures use the Ed25519 CESR Proof Format.
### Importance of OOBI and Challenge-Response
- **Out-of-Band Introductions (OOBIs):** Used for discovery, allowing controllers to find each other's KELs and schema definitions. For instance, GARs use OOBI protocols during the GLEIF AID genesis, and also use OOBIs to resolve ACDC schema locations.
- **Challenge-Response:** This protocol is crucial for mutual authentication between controllers after initial discovery via OOBI. It ensures that the entity on the other side genuinely possesses the private keys for the AID they claim to control. This involves exchanging unique challenge messages and verifying signed responses.
## The Legal Entity vLEI Credential
The primary credential issued to an organization within the vLEI ecosystem is the Legal Entity vLEI Credential. Its purpose is to provide a simple, safe, and secure way to identify the Legal Entity who holds it to any verifier wanting to verify a vLEI credential.

### Issuance Process
The issuance of a Legal Entity vLEI Credential by a QVI to a Legal Entity is a process governed by the [vLEI Ecosystem Framework](https://www.gleif.org/en/organizational-identity/introducing-the-verifiable-lei-vlei/introducing-the-vlei-ecosystem-governance-framework):
1. **Identity Verification of Legal Entity Representatives:** The QVI must perform thorough Identity Assurance and Identity Authentication of the Legal Entity's representatives:
- **Designated Authorized Representatives (DARs):** These individuals are authorized by the Legal Entity to, among other things, designate LARs. Their identity and authority must be verified by the QVI.
- **Legal Entity Authorized Representatives (LARs):** These individuals are designated by DARs and are authorized to request and manage vLEI credentials on behalf of the Legal Entity. Their identities must also be assured and authenticated. This often involves real-time OOBI sessions with the QAR (QVI Authorized Representative), sharing of AIDs, and a challenge-response process.
1. **Multi-Signature by LARs:** For enhanced security, if a Legal Entity has multiple LARs, the Legal Entity vLEI Credential typically requires multi-signatures from a threshold of LARs to accept and manage it. The LARs form a multi-sig group to control the Legal Entity's AID.
1. **QVI Issuance Workflow:** The QVI itself follows a dual-control process, often involving two or more QARs, for issuing and signing the Legal Entity vLEI Credential.
1. **Reporting:** QVIs must report issuance events to GLEIF through the vLEI Reporting API. GLEIF then updates the Legal Entity's LEI page to reflect the issued vLEI credentials, as shown on GLEIF's [organization page](https://search.gleif.org/#/record/506700GE1G29325QX363/verifiable_credentials).
The Legal Entity vLEI Credential schema specifies required fields, including the "LEI" of the Legal Entity. It also uses the ACDC "sources" section to chain back to the QVI who authorized the credential, representing a verifiable chain of trust.
## Revocation in the vLEI Ecosystem
Revocation is a critical aspect of any credentialing system. In the vLEI ecosystem, credentials can be revoked if they are compromised, if the underlying LEI lapses, or if an individual no longer holds an authorized role.
- **Mechanism:** Revocation is performed by the original issuer of the credential. It involves recording a revocation event in the credential's Transaction Event Log (TEL) within the issuer's credential registry. This TEL event is anchored to the issuer's KEL.
- **Legal Entity vLEI Credential Revocation:** A QAR revokes a Legal Entity vLEI Credential upon a fully signed request from the Legal Entity's LAR(s) or due to involuntary reasons (e.g., lapsed LEI). The QAR reports this revocation to GLEIF.
- **Role Credential Revocation:** For OOR or ECR credentials, the Legal Entity typically notifies the QVI (if QVI-issued) or handles it internally (if LE-issued) to revoke the credential when a person's role changes or employment ends.
The vLEI framework ensures that revocation status is verifiable, allowing relying parties to confirm the ongoing validity of a presented credential.
π SUMMARY
The GLEIF vLEI ecosystem provides a robust framework for digital organizational identity, leveraging KERI for secure, decentralized identifiers (AIDs) and ACDCs for verifiable credentials. GLEIF acts as the Root of Trust, delegating authority to Qualified vLEI Issuers (QVIs) who, in turn, issue vLEI credentials to Legal Entities. These Legal Entities can then issue role-specific vLEI credentials to individuals.
Key principles include self-certifying AIDs, advanced key management (pre-rotation, multi-sig), cooperative delegation, and the use of KERI infrastructure like Witnesses and Watchers. vLEIs and their schemas are SAID-ified for tamper-evidence and integrity. The issuance and presentation of vLEIs utilize the IPEX protocol, with OOBI and challenge-response mechanisms ensuring secure discovery and authentication. Rigorous identity verification processes are mandated for issuing credentials, particularly the Legal Entity vLEI Credential, involving DARs and LARs. The system also includes defined processes for credential revocation.
[<- Prev (Third Party Tools)](102_30_Third_Party_Tools.ipynb) | [Next (vLEI Trust Chain) ->](103_10_vLEI_Trust_Chain.ipynb)
# vLEI Trust Chain
π― OBJECTIVE
To provide a practical, hands-on demonstration of the vLEI trust chain using SignifyTS.
## The simplified vLEI Trust Chain
To clearly explain the fundamentals of vLEI credentials and schemas, this notebook presents a simplified model of the credential issuance hierarchy. We will trace the flow of authority and the process of creating chained credentials using official vLEI schema definitions. For the sake of clarity, we have excluded the more advanced topics of multisignatures and delegated identifier structures, which are key components of the complete vLEI production trust chain. A practical, in-depth example of these advanced features can be found in the **[qvi-software repository](https://github.com/GLEIF-IT/qvi-software/tree/main/qvi-workflow)**.
The outcome of this training is to produce a verification chain similar to the one shown below except that all identifiers are single signature identifiers instead of multi-signature identifiers.

## Setup Phase
The first step is to create the four distinct identity clients that represent the actors in our scenario: GLEIF, a Qualified vLEI Issuer (QVI), a Legal Entity (LE), and a Role holder. We will establish secure connections between all relevant parties using OOBIs and create the necessary credential registries for the issuers.
```typescript
import { randomPasscode, Saider} from 'npm:signify-ts@0.3.0-rc1';
import {
initializeSignify, initializeAndConnectClient, createNewAID, addEndRoleForAID,
generateOOBI, resolveOOBI, createCredentialRegistry, issueCredential,
ipexGrantCredential, getCredentialState, waitForAndGetNotification,
ipexAdmitGrant, markNotificationRead,
DEFAULT_IDENTIFIER_ARGS, ROLE_AGENT, IPEX_GRANT_ROUTE, IPEX_ADMIT_ROUTE, SCHEMA_SERVER_HOST,
prTitle, prMessage, prContinue, prAlert, isServiceHealthy, sleep
} from './scripts_ts/utils.ts';
initializeSignify()
// Create clients, AIDs and OOBIs.
prTitle("Creating clients setup")
// Fixed Bran to keep a consistent root of trust (DO NOT MODIFY or else validation with the Sally verifier will break)
const gleifBran = "Dm8Tmz05CF6_JLX9sVlFe"
const gleifAlias = 'gleif'
const { client: gleifClient } = await initializeAndConnectClient(gleifBran)
let gleifPrefix
// GLEIF GEDA (GLEIF External Delegated AID) setup
// uses try/catch to permit reusing existing GEDA upon re-run of this test file.
try{
const gleifAid = await gleifClient.identifiers().get(gleifAlias);
gleifPrefix = gleifAid.prefix
} catch {
prMessage("Creating GLEIF AID")
const { aid: newAid} = await createNewAID(gleifClient, gleifAlias, DEFAULT_IDENTIFIER_ARGS);
await addEndRoleForAID(gleifClient, gleifAlias, ROLE_AGENT);
gleifPrefix = newAid.i
}
const gleifOOBI = await generateOOBI(gleifClient, gleifAlias, ROLE_AGENT);
prMessage(`GLEIF Prefix: ${gleifPrefix}`)
// QVI
const qviBran = randomPasscode()
const qviAlias = 'qvi'
const { client: qviClient } = await initializeAndConnectClient(qviBran)
const { aid: qviAid} = await createNewAID(qviClient, qviAlias, DEFAULT_IDENTIFIER_ARGS);
await addEndRoleForAID(qviClient, qviAlias, ROLE_AGENT);
const qviOOBI = await generateOOBI(qviClient, qviAlias, ROLE_AGENT);
const qviPrefix = qviAid.i
prMessage(`QVI Prefix: ${qviPrefix}`)
// LE
const leBran = randomPasscode()
const leAlias = 'le'
const { client: leClient } = await initializeAndConnectClient(leBran)
const { aid: leAid} = await createNewAID(leClient, leAlias, DEFAULT_IDENTIFIER_ARGS);
await addEndRoleForAID(leClient, leAlias, ROLE_AGENT);
const leOOBI = await generateOOBI(leClient, leAlias, ROLE_AGENT);
const lePrefix = leAid.i
prMessage(`LE Prefix: ${lePrefix}`)
// Role Holder
const roleBran = randomPasscode()
const roleAlias = 'role'
const { client: roleClient } = await initializeAndConnectClient(roleBran)
const { aid: roleAid} = await createNewAID(roleClient, roleAlias, DEFAULT_IDENTIFIER_ARGS);
await addEndRoleForAID(roleClient, roleAlias, ROLE_AGENT);
const roleOOBI = await generateOOBI(roleClient, roleAlias, ROLE_AGENT);
const rolePrefix = roleAid.i
prMessage(`ROLE Prefix: ${rolePrefix}`)
// Client OOBI resolution (Create contacts)
prTitle("Resolving OOBIs")
await Promise.all([
resolveOOBI(gleifClient, qviOOBI, qviAlias),
resolveOOBI(qviClient, gleifOOBI, gleifAlias),
resolveOOBI(qviClient, leOOBI, leAlias),
resolveOOBI(qviClient, roleOOBI, roleAlias),
resolveOOBI(leClient, gleifOOBI, gleifAlias),
resolveOOBI(leClient, qviOOBI, qviAlias),
resolveOOBI(leClient, roleOOBI, roleAlias),
resolveOOBI(roleClient, gleifOOBI, gleifAlias),
resolveOOBI(roleClient, leOOBI, leAlias),
resolveOOBI(roleClient, qviOOBI, qviAlias)
]);
// Create Credential Registries
prTitle("Creating Credential Registries")
// GLEIF GEDA Registry
// uses try/catch to permit reusing existing GEDA upon re-run of this test file.
let gleifRegistrySaid
try{
const registries = await gleifClient.registries().list(gleifAlias);
gleifRegistrySaid = registries[0].regk
} catch {
prMessage("Creating GLEIF Registry")
const { registrySaid: newRegistrySaid } = await createCredentialRegistry(gleifClient, gleifAlias, 'gleifRegistry')
gleifRegistrySaid = newRegistrySaid
}
// QVI and LE registry
const { registrySaid: qviRegistrySaid } = await createCredentialRegistry(qviClient, qviAlias, 'qviRegistry')
const { registrySaid: leRegistrySaid } = await createCredentialRegistry(leClient, leAlias, 'leRegistry')
prContinue()
```
Creating clients setup
Using Passcode (bran): Dm8Tmz05CF6_JLX9sVlFe
Signify-ts library initialized.
Client boot process initiated with KERIA agent.
Client AID Prefix: EAahBlwoMzpTutCwwyc8QitdbzrbLXhKLuydIbVOGjCM
Agent AID Prefix: EKtDG7pLmYMforv2XuNouqMWIslaw3n79QZq8q7RBI2d
Generating OOBI for AID alias gleif with role agent
Generated OOBI URL: http://keria:3902/oobi/EECGZ7vbYzOM1FWicPFIot-4AiteMX6Xr5htEVAA5Iq2/agent/EKtDG7pLmYMforv2XuNouqMWIslaw3n79QZq8q7RBI2d
GLEIF Prefix: EECGZ7vbYzOM1FWicPFIot-4AiteMX6Xr5htEVAA5Iq2
Using Passcode (bran): AaGt5rjZRDve7qEBKef6s
Client boot process initiated with KERIA agent.
Client AID Prefix: EH2GiFoVXWi4D0ktUE6NwpCNQZj6PpgQmbCGZ2OOd03f
Agent AID Prefix: EMlCbBalJtkWpjzbHdvy238bud0S03AEnwb6aUbunUYZ
Initiating AID inception for alias: qvi
Successfully created AID with prefix: ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB
Assigning 'agent' role to KERIA Agent EMlCbBalJtkWpjzbHdvy238bud0S03AEnwb6aUbunUYZ for AID alias qvi
Successfully assigned 'agent' role for AID alias qvi.
Generating OOBI for AID alias qvi with role agent
Generated OOBI URL: http://keria:3902/oobi/ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB/agent/EMlCbBalJtkWpjzbHdvy238bud0S03AEnwb6aUbunUYZ
QVI Prefix: ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB
Using Passcode (bran): D3JcdQhJMIUQJx1_iBSXr
Client boot process initiated with KERIA agent.
Client AID Prefix: EE2eDDu0qaAxZvHkmKcYyFzS-RK_fPtfKI07AxvzebEu
Agent AID Prefix: EMRKCrs8TUM_6fo143ofNksU7m8zdeBVuDVhOUMx9yx3
Initiating AID inception for alias: le
Successfully created AID with prefix: ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ
Assigning 'agent' role to KERIA Agent EMRKCrs8TUM_6fo143ofNksU7m8zdeBVuDVhOUMx9yx3 for AID alias le
Successfully assigned 'agent' role for AID alias le.
Generating OOBI for AID alias le with role agent
Generated OOBI URL: http://keria:3902/oobi/ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ/agent/EMRKCrs8TUM_6fo143ofNksU7m8zdeBVuDVhOUMx9yx3
LE Prefix: ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ
Using Passcode (bran): CimFJ8s6BsfubR513PBv0
Client boot process initiated with KERIA agent.
Client AID Prefix: ELIptEqv5mFrsJU-hqbhoUCgyzhChxt732EVF2GZanWC
Agent AID Prefix: EKS9HxyPWFbx9pvrhHq5xLBR8CfkCwD5KrkUoGwWWoRp
Initiating AID inception for alias: role
Successfully created AID with prefix: EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu
Assigning 'agent' role to KERIA Agent EKS9HxyPWFbx9pvrhHq5xLBR8CfkCwD5KrkUoGwWWoRp for AID alias role
Successfully assigned 'agent' role for AID alias role.
Generating OOBI for AID alias role with role agent
Generated OOBI URL: http://keria:3902/oobi/EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu/agent/EKS9HxyPWFbx9pvrhHq5xLBR8CfkCwD5KrkUoGwWWoRp
ROLE Prefix: EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu
Resolving OOBIs
Resolving OOBI URL: http://keria:3902/oobi/ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB/agent/EMlCbBalJtkWpjzbHdvy238bud0S03AEnwb6aUbunUYZ with alias qvi
Resolving OOBI URL: http://keria:3902/oobi/EECGZ7vbYzOM1FWicPFIot-4AiteMX6Xr5htEVAA5Iq2/agent/EKtDG7pLmYMforv2XuNouqMWIslaw3n79QZq8q7RBI2d with alias gleif
Resolving OOBI URL: http://keria:3902/oobi/ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ/agent/EMRKCrs8TUM_6fo143ofNksU7m8zdeBVuDVhOUMx9yx3 with alias le
Resolving OOBI URL: http://keria:3902/oobi/EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu/agent/EKS9HxyPWFbx9pvrhHq5xLBR8CfkCwD5KrkUoGwWWoRp with alias role
Resolving OOBI URL: http://keria:3902/oobi/EECGZ7vbYzOM1FWicPFIot-4AiteMX6Xr5htEVAA5Iq2/agent/EKtDG7pLmYMforv2XuNouqMWIslaw3n79QZq8q7RBI2d with alias gleif
Resolving OOBI URL: http://keria:3902/oobi/ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB/agent/EMlCbBalJtkWpjzbHdvy238bud0S03AEnwb6aUbunUYZ with alias qvi
Resolving OOBI URL: http://keria:3902/oobi/EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu/agent/EKS9HxyPWFbx9pvrhHq5xLBR8CfkCwD5KrkUoGwWWoRp with alias role
Resolving OOBI URL: http://keria:3902/oobi/EECGZ7vbYzOM1FWicPFIot-4AiteMX6Xr5htEVAA5Iq2/agent/EKtDG7pLmYMforv2XuNouqMWIslaw3n79QZq8q7RBI2d with alias gleif
Resolving OOBI URL: http://keria:3902/oobi/ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ/agent/EMRKCrs8TUM_6fo143ofNksU7m8zdeBVuDVhOUMx9yx3 with alias le
Resolving OOBI URL: http://keria:3902/oobi/ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB/agent/EMlCbBalJtkWpjzbHdvy238bud0S03AEnwb6aUbunUYZ with alias qvi
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Contact "qvi" added/updated.
Contact "le" added/updated.
Contact "qvi" added/updated.
Contact "gleif" added/updated.
Contact "role" added/updated.
Contact "le" added/updated.
Contact "gleif" added/updated.
Contact "role" added/updated.
Contact "qvi" added/updated.
Contact "gleif" added/updated.
Creating Credential Registries
Creating credential registry "qviRegistry" for AID alias "qvi"...
Successfully created credential registry: ED7V4aCJrFccq8vtvExmQW12pIe25ZT381275ZgWLxwl
Creating credential registry "leRegistry" for AID alias "le"...
Successfully created credential registry: EIWmQXgTeIg3XK_ixkyZST4_h7L6AdEjtXe9iMrFU31r
You can continue β
## Schema Resolution
For any party to issue or verify a credential, they must first have a copy of its corresponding schema. The schemas define the structure, attributes, and rules for each type of vLEI credential. In this ecosystem, schemas are identified by a SAID and are hosted on a schema server. All participants will resolve the OOBIs for the schemas they need to interact with.
The schemas used in this demonstration are:
- **QVI Credential**: Issued by GLEIF to a QVI, authorizing it to issue vLEI credentials.
- **vLEI Credential**: Issued by a QVI to a Legal Entity, representing its digital identity.
- **OOR Auth Credential**: An authorization issued by a Legal Entity to a QVI, permitting the QVI to issue a specific OOR credential on its behalf.
- **OOR Credential**: Issued to an individual in an official capacity (e.g., CEO), based on an OOR authorization.
- **ECR Auth Credential**: An authorization issued by a Legal Entity, permitting another party (like a QVI) to issue an ECR credential on its behalf.
- **ECR Credential**: Issued to an individual for a specific business role or context (e.g., Project Manager), based on an ECR authorization or issued directly by the LE.
βΉοΈ NOTE
For this demonstration, the vLEI schemas are pre-loaded into our local schema server, and their SAIDs are known beforehand.
```typescript
// Schemas
// vLEI Schema SAIDs. These are well known schemas. Already preloaded
const QVI_SCHEMA_SAID = 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao';
const LE_SCHEMA_SAID = 'ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY';
const ECR_AUTH_SCHEMA_SAID = 'EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g';
const ECR_SCHEMA_SAID = 'EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw';
const OOR_AUTH_SCHEMA_SAID = 'EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E';
const OOR_SCHEMA_SAID = 'EBNaNu-M9P5cgrnfl2Fvymy4E_jvxxyjb70PRtiANlJy';
const QVI_SCHEMA_URL = `${SCHEMA_SERVER_HOST}/oobi/${QVI_SCHEMA_SAID}`;
const LE_SCHEMA_URL = `${SCHEMA_SERVER_HOST}/oobi/${LE_SCHEMA_SAID}`;
const ECR_AUTH_SCHEMA_URL = `${SCHEMA_SERVER_HOST}/oobi/${ECR_AUTH_SCHEMA_SAID}`;
const ECR_SCHEMA_URL = `${SCHEMA_SERVER_HOST}/oobi/${ECR_SCHEMA_SAID}`;
const OOR_AUTH_SCHEMA_URL = `${SCHEMA_SERVER_HOST}/oobi/${OOR_AUTH_SCHEMA_SAID}`;
const OOR_SCHEMA_URL = `${SCHEMA_SERVER_HOST}/oobi/${OOR_SCHEMA_SAID}`;
prTitle("Schema OOBIs")
prMessage(`QVI_SCHEMA_URL:\n - ${QVI_SCHEMA_URL}`)
prMessage(`LE_SCHEMA_URL:\n - ${LE_SCHEMA_URL}`)
prMessage(`ECR_AUTH_SCHEMA_URL:\n - ${ECR_AUTH_SCHEMA_URL}`)
prMessage(`ECR_SCHEMA_URL:\n - ${ECR_SCHEMA_URL}`)
prMessage(`OOR_AUTH_SCHEMA_URL:\n - ${OOR_AUTH_SCHEMA_URL}`)
prMessage(`OOR_SCHEMA_URL:\n - ${OOR_SCHEMA_URL}`)
prContinue()
```
Schema OOBIs
QVI_SCHEMA_URL:
- http://vlei-server:7723/oobi/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao
LE_SCHEMA_URL:
- http://vlei-server:7723/oobi/ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY
ECR_AUTH_SCHEMA_URL:
- http://vlei-server:7723/oobi/EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g
ECR_SCHEMA_URL:
- http://vlei-server:7723/oobi/EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw
OOR_AUTH_SCHEMA_URL:
- http://vlei-server:7723/oobi/EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E
OOR_SCHEMA_URL:
- http://vlei-server:7723/oobi/EBNaNu-M9P5cgrnfl2Fvymy4E_jvxxyjb70PRtiANlJy
You can continue β
All clients now resolve all the necessary schemas in order to have knowledge of the schemas they use.
```typescript
prTitle("Resolving Schemas")
await Promise.all([
resolveOOBI(gleifClient, QVI_SCHEMA_URL),
resolveOOBI(qviClient, QVI_SCHEMA_URL),
resolveOOBI(qviClient, LE_SCHEMA_URL),
resolveOOBI(qviClient, ECR_AUTH_SCHEMA_URL),
resolveOOBI(qviClient, ECR_SCHEMA_URL),
resolveOOBI(qviClient, OOR_AUTH_SCHEMA_URL),
resolveOOBI(qviClient, OOR_SCHEMA_URL),
resolveOOBI(leClient, QVI_SCHEMA_URL),
resolveOOBI(leClient, LE_SCHEMA_URL),
resolveOOBI(leClient, ECR_AUTH_SCHEMA_URL),
resolveOOBI(leClient, ECR_SCHEMA_URL),
resolveOOBI(leClient, OOR_AUTH_SCHEMA_URL),
resolveOOBI(leClient, OOR_SCHEMA_URL),
resolveOOBI(roleClient, QVI_SCHEMA_URL),
resolveOOBI(roleClient, LE_SCHEMA_URL),
resolveOOBI(roleClient, ECR_AUTH_SCHEMA_URL),
resolveOOBI(roleClient, ECR_SCHEMA_URL),
resolveOOBI(roleClient, OOR_AUTH_SCHEMA_URL),
resolveOOBI(roleClient, OOR_SCHEMA_URL),
]);
prContinue()
```
Resolving Schemas
Resolving OOBI URL: http://vlei-server:7723/oobi/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/EBNaNu-M9P5cgrnfl2Fvymy4E_jvxxyjb70PRtiANlJy with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/EBNaNu-M9P5cgrnfl2Fvymy4E_jvxxyjb70PRtiANlJy with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E with alias undefined
Resolving OOBI URL: http://vlei-server:7723/oobi/EBNaNu-M9P5cgrnfl2Fvymy4E_jvxxyjb70PRtiANlJy with alias undefined
Successfully resolved OOBI URL. Response: OK
Contact "undefined" added/updated.
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Successfully resolved OOBI URL. Response: OK
Contact "undefined" added/updated.
Contact "undefined" added/updated.
Contact "undefined" added/updated.
Contact "undefined" added/updated.
Contact "undefined" added/updated.
Contact "undefined" added/updated.
Contact "undefined" added/updated.
Contact "undefined" added/updated.
Contact "undefined" added/updated.
Contact "undefined" added/updated.
Contact "undefined" added/updated.
Contact "undefined" added/updated.
Contact "undefined" added/updated.
Contact "undefined" added/updated.
Contact "undefined" added/updated.
Contact "undefined" added/updated.
Contact "undefined" added/updated.
Contact "undefined" added/updated.
You can continue β
## Credential Issuance Chain
The core of this demonstration is to build the vLEI trust chain credential by credential. The test follows the official vLEI ecosystem hierarchy, showing how authority is passed down from GLEIF to a QVI, then to a Legal Entity, and finally to an individual Role Holder.
The issuance flow is as follows:
- **QVI Credential**: GLEIF issues a "Qualified vLEI Issuer" credential to the QVI.
- **LE Credential**: The QVI issues a "Legal Entity" credential to the LE.
- **OOR Auth Credential**: The LE issues an "Official Organizational Role" authorization to the QVI.
- **OOR Credential**: The QVI, using the authorization from the previous step, issues the final OOR credential to the Role holder.
- **ECR Auth Credential**: The LE issues an "Engagement Context Role" authorization credential to the QVI.
- **ECR Credential (Path 1)**: The LE directly issues an ECR credential to the Role holder.
- **ECR Credential (Path 2)**: The QVI issues another ECR credential to the same Role holder, this time using the ECR authorization credential.
The key to this chain of trust lies within the `e` (edges) block of each ACDC. This block contains cryptographic pointers to the credential that authorizes the issuance of the current one. We will examine these edge blocks at each step to see how the chain is formed.
### Step 1: QVI Credential - GLEIF issues a Qualified vLEI Issuer credential to the QVI
The chain of trust begins with GLEIF, the root of the ecosystem, issuing a credential to a QVI. This credential attests that the QVI is qualified and authorized to issue vLEI credentials to other legal entities. As the first link in our chain, this credential does not have an edge block pointing to a prior authority.
```typescript
// QVI LEI (Arbitrary value)
const qviData = {
LEI: '254900OPPU84GM83MG36',
};
// GLEIF - Issue credential
prTitle("Issuing Credential")
const { credentialSaid: credentialSaid} = await issueCredential(
gleifClient, gleifAlias, gleifRegistrySaid,
QVI_SCHEMA_SAID,
qviPrefix,
qviData
)
// GLEIF - get credential
const qviCredential = await gleifClient.credentials().get(credentialSaid);
// GLEIF - Ipex grant
prTitle("Granting Credential")
const grantResponse = await ipexGrantCredential(
gleifClient, gleifAlias,
qviPrefix,
qviCredential
)
// QVI - Wait for grant notification
const grantNotifications = await waitForAndGetNotification(qviClient, IPEX_GRANT_ROUTE)
const grantNotification = grantNotifications[0]
// QVI - Admit Grant
prTitle("Admitting Grant")
const admitResponse = await ipexAdmitGrant(
qviClient, qviAlias,
gleifPrefix,
grantNotification.a.d
)
// QVI - Mark notification
await markNotificationRead(qviClient, grantNotification.i)
// GLEIF - Wait for admit notification
const admitNotifications = await waitForAndGetNotification(gleifClient, IPEX_ADMIT_ROUTE)
const admitNotification = admitNotifications[0]
// GLEIF - Mark notification
await markNotificationRead(gleifClient, admitNotification.i)
prContinue()
```
Issuing Credential
Issuing credential from AID "gleif" to AID "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB"...
{
name: "credential.EC6fjMGfTBcj3qlj4MkqgZrpz1W-7tXonEhK6WAIX7jJ",
metadata: {
ced: {
v: "ACDC10JSON000197_",
d: "EC6fjMGfTBcj3qlj4MkqgZrpz1W-7tXonEhK6WAIX7jJ",
i: "EECGZ7vbYzOM1FWicPFIot-4AiteMX6Xr5htEVAA5Iq2",
ri: "EO6JNHwIxVoTPrp8NRGjLaftgeCHMrMAnUU1viAuK-ao",
s: "EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao",
a: {
d: "EHCnsk0W_pXvmscUuqT2HBcL_Rrf6DXvysZCucdxHu7l",
i: "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB",
LEI: "254900OPPU84GM83MG36",
dt: "2025-09-12T04:32:10.380000+00:00"
}
},
depends: {
name: "witness.EHDzSCDcguepq3FImANHtYY7HqUbwjcP1oFRSASVUF1s",
metadata: { pre: "EECGZ7vbYzOM1FWicPFIot-4AiteMX6Xr5htEVAA5Iq2", sn: 4 },
done: false,
error: null,
response: null
}
},
done: true,
error: null,
response: {
ced: {
v: "ACDC10JSON000197_",
d: "EC6fjMGfTBcj3qlj4MkqgZrpz1W-7tXonEhK6WAIX7jJ",
i: "EECGZ7vbYzOM1FWicPFIot-4AiteMX6Xr5htEVAA5Iq2",
ri: "EO6JNHwIxVoTPrp8NRGjLaftgeCHMrMAnUU1viAuK-ao",
s: "EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao",
a: {
d: "EHCnsk0W_pXvmscUuqT2HBcL_Rrf6DXvysZCucdxHu7l",
i: "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB",
LEI: "254900OPPU84GM83MG36",
dt: "2025-09-12T04:32:10.380000+00:00"
}
}
}
}
Successfully issued credential with SAID: EC6fjMGfTBcj3qlj4MkqgZrpz1W-7tXonEhK6WAIX7jJ
Granting Credential
AID "gleif" granting credential to AID "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB" via IPEX...
Successfully submitted IPEX grant from "gleif" to "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB".
Waiting for notification with route "/exn/ipex/grant"...
[Retry] Grant notification not found on attempt #1 of 5
[Retry] Waiting 5000ms before next attempt...
Admitting Grant
AID "qvi" admitting IPEX grant "ECtHXWPGioUGvZ8MNtzVFat_ryOQx62tsG3ya5gZqZGq" from AID "EECGZ7vbYzOM1FWicPFIot-4AiteMX6Xr5htEVAA5Iq2"...
Successfully submitted IPEX admit for grant "ECtHXWPGioUGvZ8MNtzVFat_ryOQx62tsG3ya5gZqZGq".
Marking notification "0AAkw4s9oss0I0tyD_pHQ4XB" as read...
Notification "0AAkw4s9oss0I0tyD_pHQ4XB" marked as read.
Waiting for notification with route "/exn/ipex/admit"...
Marking notification "0AAwyvF4dim2WDtHeRavwVzi" as read...
Notification "0AAwyvF4dim2WDtHeRavwVzi" marked as read.
You can continue β
### Step 2: LE Credential - QVI issues a Legal Entity credential to the LE
Now that the QVI is authorized, it can issue a vLEI credential to a Legal Entity. To maintain the chain of trust, this new LE Credential must be cryptographically linked back to the QVI's authorizing credential.
This link is created in the `leEdge` object.
- `n: qviCredential.sad.d`: The `n` field (node) is populated with the SAID of the QVI's own credential, issued in Step 1. This is the direct cryptographic pointer.
- `s: qviCredential.sad.s`: The `s` field specifies the required schema SAID of the credential being pointed to, ensuring the link is to the correct type of credential.
The `Saider.saidify()` function is a utility that makes this edge block itself verifiable. It calculates a cryptographic digest (SAID) of the edge's content and embeds that digest back into the block under the `d` field.
```typescript
// Credential Data
const leData = {
LEI: '875500ELOZEL05BVXV37',
};
const leEdge = Saider.saidify({
d: '',
qvi: {
n: qviCredential.sad.d,
s: qviCredential.sad.s,
},
})[1];
const leRules = Saider.saidify({
d: '',
usageDisclaimer: {
l: 'Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled.',
},
issuanceDisclaimer: {
l: 'All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework.',
},
})[1];
// qvi - Issue credential
prTitle("Issuing Credential")
const { credentialSaid: credentialSaid} = await issueCredential(
qviClient, qviAlias, qviRegistrySaid,
LE_SCHEMA_SAID,
lePrefix,
leData, leEdge, leRules
)
// qvi - get credential (with all its data)
prTitle("Granting Credential")
const leCredential = await qviClient.credentials().get(credentialSaid);
// qvi - Ipex grant
const grantResponse = await ipexGrantCredential(
qviClient, qviAlias,
lePrefix,
leCredential
)
// LE - Wait for grant notification
const grantNotifications = await waitForAndGetNotification(leClient, IPEX_GRANT_ROUTE)
const grantNotification = grantNotifications[0]
// LE - Admit Grant
prTitle("Admitting Grant")
const admitResponse = await ipexAdmitGrant(
leClient, leAlias,
qviPrefix,
grantNotification.a.d
)
// LE - Mark notification
await markNotificationRead(leClient, grantNotification.i)
// QVI - Wait for admit notification
const admitNotifications = await waitForAndGetNotification(qviClient, IPEX_ADMIT_ROUTE)
const admitNotification = admitNotifications[0]
// QVI - Mark notification
await markNotificationRead(qviClient, admitNotification.i)
prContinue()
```
Issuing Credential
Issuing credential from AID "qvi" to AID "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ"...
{
name: "credential.EEbwZ1kvlmJmjS-w7wqMXUnHGH_jf7aGszTSyYrZsOYW",
metadata: {
ced: {
v: "ACDC10JSON0005c8_",
d: "EEbwZ1kvlmJmjS-w7wqMXUnHGH_jf7aGszTSyYrZsOYW",
i: "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB",
ri: "ED7V4aCJrFccq8vtvExmQW12pIe25ZT381275ZgWLxwl",
s: "ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY",
a: {
d: "EJbsRgbWE3bIalbOp7rp1pvAeC5VTgP0zUBfCcrCSsEB",
i: "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ",
LEI: "875500ELOZEL05BVXV37",
dt: "2025-09-12T04:32:18.699000+00:00"
},
e: {
d: "ENwYh-0Hwkz7rqeTb2M_pLUS4IKcycGYjsuIbzbMk-cq",
qvi: {
n: "EC6fjMGfTBcj3qlj4MkqgZrpz1W-7tXonEhK6WAIX7jJ",
s: "EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao"
}
},
r: {
d: "EGZ97EjPSINR-O-KHDN_uw4fdrTxeuRXrqT5ZHHQJujQ",
usageDisclaimer: {
l: "Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled."
},
issuanceDisclaimer: {
l: "All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework."
}
}
},
depends: {
name: "witness.EIgEL2I5DagKseoLud6SezCvNwB5aM0Ak96qvVj34ZlZ",
metadata: { pre: "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB", sn: 2 },
done: false,
error: null,
response: null
}
},
done: true,
error: null,
response: {
ced: {
v: "ACDC10JSON0005c8_",
d: "EEbwZ1kvlmJmjS-w7wqMXUnHGH_jf7aGszTSyYrZsOYW",
i: "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB",
ri: "ED7V4aCJrFccq8vtvExmQW12pIe25ZT381275ZgWLxwl",
s: "ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY",
a: {
d: "EJbsRgbWE3bIalbOp7rp1pvAeC5VTgP0zUBfCcrCSsEB",
i: "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ",
LEI: "875500ELOZEL05BVXV37",
dt: "2025-09-12T04:32:18.699000+00:00"
},
e: {
d: "ENwYh-0Hwkz7rqeTb2M_pLUS4IKcycGYjsuIbzbMk-cq",
qvi: {
n: "EC6fjMGfTBcj3qlj4MkqgZrpz1W-7tXonEhK6WAIX7jJ",
s: "EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao"
}
},
r: {
d: "EGZ97EjPSINR-O-KHDN_uw4fdrTxeuRXrqT5ZHHQJujQ",
usageDisclaimer: {
l: "Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled."
},
issuanceDisclaimer: {
l: "All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework."
}
}
}
}
}
Successfully issued credential with SAID: EEbwZ1kvlmJmjS-w7wqMXUnHGH_jf7aGszTSyYrZsOYW
Granting Credential
AID "qvi" granting credential to AID "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ" via IPEX...
Successfully submitted IPEX grant from "qvi" to "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ".
Waiting for notification with route "/exn/ipex/grant"...
[Retry] Grant notification not found on attempt #1 of 5
[Retry] Waiting 5000ms before next attempt...
Admitting Grant
AID "le" admitting IPEX grant "EP6Ss-ASNLSL10HjRStHPHiafzLSZ21UUOfmm-mOXa5a" from AID "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB"...
Successfully submitted IPEX admit for grant "EP6Ss-ASNLSL10HjRStHPHiafzLSZ21UUOfmm-mOXa5a".
Marking notification "0ADwUPzGisTInu8vqRY98E9f" as read...
Notification "0ADwUPzGisTInu8vqRY98E9f" marked as read.
Waiting for notification with route "/exn/ipex/admit"...
Marking notification "0ACV-rTeh8sD7KE_jOsQTw6c" as read...
Notification "0ACV-rTeh8sD7KE_jOsQTw6c" marked as read.
You can continue β
### Step 3: OOR AUTH Credential - LE issues an Official Organizational Role authorization to QVI
Before a QVI can issue a credential for an official role (like CEO or Director) on behalf of a Legal Entity, it must first receive explicit authorization. This step shows the LE issuing an "OOR Authorization" credential to the QVI.
The edge block here links back to the `leCredential` from **Step 2**, proving that the entity granting this authorization is a valid Legal Entity within the vLEI ecosystem.
```typescript
// Credential Data
const oorAuthData = {
AID: '',
LEI: leData.LEI,
personLegalName: 'Jane Doe',
officialRole: 'CEO',
};
const oorAuthEdge = Saider.saidify({
d: '',
le: {
n: leCredential.sad.d,
s: leCredential.sad.s,
},
})[1];
// LE - Issue credential
prTitle("Issuing Credential")
const { credentialSaid: credentialSaid} = await issueCredential(
leClient, leAlias, leRegistrySaid,
OOR_AUTH_SCHEMA_SAID,
qviPrefix,
oorAuthData, oorAuthEdge, leRules // Reuses LE rules
)
// LE - get credential
const oorAuthCredential = await leClient.credentials().get(credentialSaid);
// LE - Ipex grant
prTitle("Granting Credential")
const grantResponse = await ipexGrantCredential(
leClient, leAlias,
qviPrefix,
oorAuthCredential
)
// QVI - Wait for grant notification
const grantNotifications = await waitForAndGetNotification(qviClient, IPEX_GRANT_ROUTE)
const grantNotification = grantNotifications[0]
// QVI - Admit Grant
prTitle("Admitting Grant")
const admitResponse = await ipexAdmitGrant(
qviClient, qviAlias,
lePrefix,
grantNotification.a.d
)
// QVI - Mark notification
await markNotificationRead(qviClient, grantNotification.i)
// LE - Wait for admit notification
const admitNotifications = await waitForAndGetNotification(leClient, IPEX_ADMIT_ROUTE)
const admitNotification = admitNotifications[0]
// LE - Mark notification
await markNotificationRead(leClient, admitNotification.i)
prContinue()
```
Issuing Credential
Issuing credential from AID "le" to AID "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB"...
{
name: "credential.ENuAxM6nDPLgaFARHz-w2V34Nv0lpup1iLZ3bFw1WfNS",
metadata: {
ced: {
v: "ACDC10JSON000602_",
d: "ENuAxM6nDPLgaFARHz-w2V34Nv0lpup1iLZ3bFw1WfNS",
i: "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ",
ri: "EIWmQXgTeIg3XK_ixkyZST4_h7L6AdEjtXe9iMrFU31r",
s: "EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E",
a: {
d: "ENaSGdLArVp0eburis-MqghDW_Xe7OqzaB2Q6vFnoitS",
i: "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB",
AID: "",
LEI: "875500ELOZEL05BVXV37",
personLegalName: "Jane Doe",
officialRole: "CEO",
dt: "2025-09-12T04:32:27.259000+00:00"
},
e: {
d: "EHW84Tb8rCQN71yG5TDxcmdH5mUuMnXEklYUw_5m-Rnp",
le: {
n: "EEbwZ1kvlmJmjS-w7wqMXUnHGH_jf7aGszTSyYrZsOYW",
s: "ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY"
}
},
r: {
d: "EGZ97EjPSINR-O-KHDN_uw4fdrTxeuRXrqT5ZHHQJujQ",
usageDisclaimer: {
l: "Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled."
},
issuanceDisclaimer: {
l: "All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework."
}
}
},
depends: {
name: "witness.EA-FGXuvvlfmQnNTU42NPwIA07FTqkTNEBLsGNRY0tHu",
metadata: { pre: "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ", sn: 2 },
done: false,
error: null,
response: null
}
},
done: true,
error: null,
response: {
ced: {
v: "ACDC10JSON000602_",
d: "ENuAxM6nDPLgaFARHz-w2V34Nv0lpup1iLZ3bFw1WfNS",
i: "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ",
ri: "EIWmQXgTeIg3XK_ixkyZST4_h7L6AdEjtXe9iMrFU31r",
s: "EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E",
a: {
d: "ENaSGdLArVp0eburis-MqghDW_Xe7OqzaB2Q6vFnoitS",
i: "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB",
AID: "",
LEI: "875500ELOZEL05BVXV37",
personLegalName: "Jane Doe",
officialRole: "CEO",
dt: "2025-09-12T04:32:27.259000+00:00"
},
e: {
d: "EHW84Tb8rCQN71yG5TDxcmdH5mUuMnXEklYUw_5m-Rnp",
le: {
n: "EEbwZ1kvlmJmjS-w7wqMXUnHGH_jf7aGszTSyYrZsOYW",
s: "ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY"
}
},
r: {
d: "EGZ97EjPSINR-O-KHDN_uw4fdrTxeuRXrqT5ZHHQJujQ",
usageDisclaimer: {
l: "Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled."
},
issuanceDisclaimer: {
l: "All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework."
}
}
}
}
}
Successfully issued credential with SAID: ENuAxM6nDPLgaFARHz-w2V34Nv0lpup1iLZ3bFw1WfNS
Granting Credential
AID "le" granting credential to AID "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB" via IPEX...
Successfully submitted IPEX grant from "le" to "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB".
Waiting for notification with route "/exn/ipex/grant"...
[Retry] Grant notification not found on attempt #1 of 5
[Retry] Waiting 5000ms before next attempt...
Admitting Grant
AID "qvi" admitting IPEX grant "EEIUgj1kjZpvSNASpgCtA2vapw3mHQXTiyQVCGlQ2lid" from AID "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ"...
Successfully submitted IPEX admit for grant "EEIUgj1kjZpvSNASpgCtA2vapw3mHQXTiyQVCGlQ2lid".
Marking notification "0ADTy070IYJAgVnsOLYRfIwC" as read...
Notification "0ADTy070IYJAgVnsOLYRfIwC" marked as read.
Waiting for notification with route "/exn/ipex/admit"...
Marking notification "0ADbUrKLtAZbvC4IbzFwecbL" as read...
Notification "0ADbUrKLtAZbvC4IbzFwecbL" marked as read.
You can continue β
### Step 4: OOR Credential - QVI issues the final OOR credential to the Role holder
Now, with the specific OOR authorization from the LE, the QVI can issue the final OOR credential to the individual Role Holder.
This is a critical link in the chain. The edge block in this new credential points to the `oorAuthCredential` from **Step 3**.
- `o: 'I2I'`: It uses the `I2I` (Issuer-to-Issuee) operator. This enforces a strict rule during verification, the issuer of this OOR credential (the QVI) must be the same entity as the issuee of the authorization credential it's pointing to. This cryptographically proves that the QVI had the correct, specific authorization from the LE to issue this very role credential.
```typescript
// Credential Data
const oorData = {
LEI: oorAuthData.LEI,
personLegalName: oorAuthData.personLegalName,
officialRole: oorAuthData.officialRole,
};
const oorEdge = Saider.saidify({
d: '',
auth: {
n: oorAuthCredential.sad.d,
s: oorAuthCredential.sad.s,
o: 'I2I',
},
})[1];
// QVI - Issue credential
prTitle("Issuing Credential")
const { credentialSaid: credentialSaid} = await issueCredential(
qviClient, qviAlias, qviRegistrySaid,
OOR_SCHEMA_SAID,
rolePrefix,
oorData, oorEdge, leRules // Reuses LE rules
)
// QVI - get credential (with all its data)
prTitle("Granting Credential")
const oorCredential = await qviClient.credentials().get(credentialSaid);
// QVI - Ipex grant
const grantResponse = await ipexGrantCredential(
qviClient, qviAlias,
rolePrefix,
oorCredential
)
// ROLE - Wait for grant notification
const grantNotifications = await waitForAndGetNotification(roleClient, IPEX_GRANT_ROUTE)
const grantNotification = grantNotifications[0]
// ROLE - Admit Grant
prTitle("Admitting Grant")
const admitResponse = await ipexAdmitGrant(
roleClient, roleAlias,
qviPrefix,
grantNotification.a.d
)
// LE - Mark notification
await markNotificationRead(roleClient, grantNotification.i)
// QVI - Wait for admit notification
const admitNotifications = await waitForAndGetNotification(qviClient, IPEX_ADMIT_ROUTE)
const admitNotification = admitNotifications[0]
// QVI - Mark notification
await markNotificationRead(qviClient, admitNotification.i)
prContinue()
```
Issuing Credential
Issuing credential from AID "qvi" to AID "EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu"...
{
name: "credential.EB86XZdf_4OzaXJsXI6d1tMofyi8lWH8GGjRd85ZUjYT",
metadata: {
ced: {
v: "ACDC10JSON000605_",
d: "EB86XZdf_4OzaXJsXI6d1tMofyi8lWH8GGjRd85ZUjYT",
i: "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB",
ri: "ED7V4aCJrFccq8vtvExmQW12pIe25ZT381275ZgWLxwl",
s: "EBNaNu-M9P5cgrnfl2Fvymy4E_jvxxyjb70PRtiANlJy",
a: {
d: "EKSNM5FjW06oqlwQ0zb10jsxYLGM0DcIGc5Czpx1JtVx",
i: "EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu",
LEI: "875500ELOZEL05BVXV37",
personLegalName: "Jane Doe",
officialRole: "CEO",
dt: "2025-09-12T04:32:35.666000+00:00"
},
e: {
d: "EGCl9jnBuHKKwqNPCyqGXRmEXn78vyJFBuEplqRfQFGP",
auth: {
n: "ENuAxM6nDPLgaFARHz-w2V34Nv0lpup1iLZ3bFw1WfNS",
s: "EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E",
o: "I2I"
}
},
r: {
d: "EGZ97EjPSINR-O-KHDN_uw4fdrTxeuRXrqT5ZHHQJujQ",
usageDisclaimer: {
l: "Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled."
},
issuanceDisclaimer: {
l: "All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework."
}
}
},
depends: {
name: "witness.EFLHKvKxWlZzaYg0GFscNgGe1Od1X0KBm0fTXweBwUY6",
metadata: { pre: "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB", sn: 3 },
done: false,
error: null,
response: null
}
},
done: true,
error: null,
response: {
ced: {
v: "ACDC10JSON000605_",
d: "EB86XZdf_4OzaXJsXI6d1tMofyi8lWH8GGjRd85ZUjYT",
i: "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB",
ri: "ED7V4aCJrFccq8vtvExmQW12pIe25ZT381275ZgWLxwl",
s: "EBNaNu-M9P5cgrnfl2Fvymy4E_jvxxyjb70PRtiANlJy",
a: {
d: "EKSNM5FjW06oqlwQ0zb10jsxYLGM0DcIGc5Czpx1JtVx",
i: "EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu",
LEI: "875500ELOZEL05BVXV37",
personLegalName: "Jane Doe",
officialRole: "CEO",
dt: "2025-09-12T04:32:35.666000+00:00"
},
e: {
d: "EGCl9jnBuHKKwqNPCyqGXRmEXn78vyJFBuEplqRfQFGP",
auth: {
n: "ENuAxM6nDPLgaFARHz-w2V34Nv0lpup1iLZ3bFw1WfNS",
s: "EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E",
o: "I2I"
}
},
r: {
d: "EGZ97EjPSINR-O-KHDN_uw4fdrTxeuRXrqT5ZHHQJujQ",
usageDisclaimer: {
l: "Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled."
},
issuanceDisclaimer: {
l: "All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework."
}
}
}
}
}
Successfully issued credential with SAID: EB86XZdf_4OzaXJsXI6d1tMofyi8lWH8GGjRd85ZUjYT
Granting Credential
AID "qvi" granting credential to AID "EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu" via IPEX...
Successfully submitted IPEX grant from "qvi" to "EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu".
Waiting for notification with route "/exn/ipex/grant"...
[Retry] Grant notification not found on attempt #1 of 5
[Retry] Waiting 5000ms before next attempt...
Admitting Grant
AID "role" admitting IPEX grant "EAmtvBCyl9PdgjX9YP2QGVojc-oVGf0Q9wcHi5NFHf9_" from AID "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB"...
Successfully submitted IPEX admit for grant "EAmtvBCyl9PdgjX9YP2QGVojc-oVGf0Q9wcHi5NFHf9_".
Marking notification "0ADxagJEkAiMLsSMTbVJMfMq" as read...
Notification "0ADxagJEkAiMLsSMTbVJMfMq" marked as read.
Waiting for notification with route "/exn/ipex/admit"...
Marking notification "0ABkU4MU5PHrNUzJ1bTo-Ckq" as read...
Notification "0ABkU4MU5PHrNUzJ1bTo-Ckq" marked as read.
You can continue β
### Step 5: ECR AUTH Credential - LE issues an ECR authorization credential to the QVI
This flow mirrors the OOR authorization. The LE issues an Engagement Context Role (ECR) authorization to the QVI. This allows the QVI to issue credentials for non-official but contextually important roles (e.g., "Project Lead," "Authorized Signatory for Invoices"). The `ecrAuthEdge` again links to the LE's root credential to prove the source of the authorization.
```typescript
// Credential Data
const ecrAuthData = {
AID: '',
LEI: leData.LEI,
personLegalName: 'John Doe',
engagementContextRole: 'Managing Director',
};
const ecrAuthEdge = Saider.saidify({
d: '',
le: {
n: leCredential.sad.d,
s: leCredential.sad.s,
},
})[1];
const ecrAuthRules = Saider.saidify({
d: '',
usageDisclaimer: {
l: 'Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled.',
},
issuanceDisclaimer: {
l: 'All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework.',
},
privacyDisclaimer: {
l: 'Privacy Considerations are applicable to QVI ECR AUTH vLEI Credentials. It is the sole responsibility of QVIs as Issuees of QVI ECR AUTH vLEI Credentials to present these Credentials in a privacy-preserving manner using the mechanisms provided in the Issuance and Presentation Exchange (IPEX) protocol specification and the Authentic Chained Data Container (ACDC) specification. https://github.com/WebOfTrust/IETF-IPEX and https://github.com/trustoverip/tswg-acdc-specification.',
},
})[1];
// LE - Issue credential
prTitle("Issuing Credential")
const { credentialSaid: credentialSaid} = await issueCredential(
leClient, leAlias, leRegistrySaid,
ECR_AUTH_SCHEMA_SAID,
qviPrefix,
ecrAuthData, ecrAuthEdge, ecrAuthRules
)
// LE - get credential
const ecrAuthCredential = await leClient.credentials().get(credentialSaid);
// LE - Ipex grant
prTitle("Granting Credential")
const grantResponse = await ipexGrantCredential(
leClient, leAlias,
qviPrefix,
ecrAuthCredential
)
// QVI - Wait for grant notification
const grantNotifications = await waitForAndGetNotification(qviClient, IPEX_GRANT_ROUTE)
const grantNotification = grantNotifications[0]
// QVI - Admit Grant
prTitle("Admitting Grant")
const admitResponse = await ipexAdmitGrant(
qviClient, qviAlias,
lePrefix,
grantNotification.a.d
)
// QVI - Mark notification
await markNotificationRead(qviClient, grantNotification.i)
// LE - Wait for admit notification
const admitNotifications = await waitForAndGetNotification(leClient, IPEX_ADMIT_ROUTE)
const admitNotification = admitNotifications[0]
// LE - Mark notification
await markNotificationRead(leClient, admitNotification.i)
prContinue()
```
Issuing Credential
Issuing credential from AID "le" to AID "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB"...
{
name: "credential.EMjzuB4T9TsuTEpi-J2ECky2hD1Ah6Z1xG-hCCIqJL2B",
metadata: {
ced: {
v: "ACDC10JSON000816_",
d: "EMjzuB4T9TsuTEpi-J2ECky2hD1Ah6Z1xG-hCCIqJL2B",
i: "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ",
ri: "EIWmQXgTeIg3XK_ixkyZST4_h7L6AdEjtXe9iMrFU31r",
s: "EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g",
a: {
d: "EPY2Sc55vchUVMHQGVcs08BiQtxn_-mqQmp6esG_UHGO",
i: "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB",
AID: "",
LEI: "875500ELOZEL05BVXV37",
personLegalName: "John Doe",
engagementContextRole: "Managing Director",
dt: "2025-09-12T04:32:43.977000+00:00"
},
e: {
d: "EHW84Tb8rCQN71yG5TDxcmdH5mUuMnXEklYUw_5m-Rnp",
le: {
n: "EEbwZ1kvlmJmjS-w7wqMXUnHGH_jf7aGszTSyYrZsOYW",
s: "ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY"
}
},
r: {
d: "EKHMDCNFlMBaMdDOq5Pf_vGMxkTqrDMrTx_28cZZJCcW",
usageDisclaimer: {
l: "Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled."
},
issuanceDisclaimer: {
l: "All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework."
},
privacyDisclaimer: {
l: "Privacy Considerations are applicable to QVI ECR AUTH vLEI Credentials. It is the sole responsibility of QVIs as Issuees of QVI ECR AUTH vLEI Credentials to present these Credentials in a privacy-preserving manner using the mechanisms provided in the Issuance and Presentation Exchange (IPEX) protocol specification and the Authentic Chained Data Container (ACDC) specification. https://github.com/WebOfTrust/IETF-IPEX and https://github.com/trustoverip/tswg-acdc-specification."
}
}
},
depends: {
name: "witness.EMcPHib0rTkeFYkMHWbRmI0eRE4oYLjsdKHlEL0CeDmO",
metadata: { pre: "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ", sn: 3 },
done: false,
error: null,
response: null
}
},
done: true,
error: null,
response: {
ced: {
v: "ACDC10JSON000816_",
d: "EMjzuB4T9TsuTEpi-J2ECky2hD1Ah6Z1xG-hCCIqJL2B",
i: "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ",
ri: "EIWmQXgTeIg3XK_ixkyZST4_h7L6AdEjtXe9iMrFU31r",
s: "EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g",
a: {
d: "EPY2Sc55vchUVMHQGVcs08BiQtxn_-mqQmp6esG_UHGO",
i: "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB",
AID: "",
LEI: "875500ELOZEL05BVXV37",
personLegalName: "John Doe",
engagementContextRole: "Managing Director",
dt: "2025-09-12T04:32:43.977000+00:00"
},
e: {
d: "EHW84Tb8rCQN71yG5TDxcmdH5mUuMnXEklYUw_5m-Rnp",
le: {
n: "EEbwZ1kvlmJmjS-w7wqMXUnHGH_jf7aGszTSyYrZsOYW",
s: "ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY"
}
},
r: {
d: "EKHMDCNFlMBaMdDOq5Pf_vGMxkTqrDMrTx_28cZZJCcW",
usageDisclaimer: {
l: "Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled."
},
issuanceDisclaimer: {
l: "All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework."
},
privacyDisclaimer: {
l: "Privacy Considerations are applicable to QVI ECR AUTH vLEI Credentials. It is the sole responsibility of QVIs as Issuees of QVI ECR AUTH vLEI Credentials to present these Credentials in a privacy-preserving manner using the mechanisms provided in the Issuance and Presentation Exchange (IPEX) protocol specification and the Authentic Chained Data Container (ACDC) specification. https://github.com/WebOfTrust/IETF-IPEX and https://github.com/trustoverip/tswg-acdc-specification."
}
}
}
}
}
Successfully issued credential with SAID: EMjzuB4T9TsuTEpi-J2ECky2hD1Ah6Z1xG-hCCIqJL2B
Granting Credential
AID "le" granting credential to AID "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB" via IPEX...
Successfully submitted IPEX grant from "le" to "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB".
Waiting for notification with route "/exn/ipex/grant"...
[Retry] Grant notification not found on attempt #1 of 5
[Retry] Waiting 5000ms before next attempt...
Admitting Grant
AID "qvi" admitting IPEX grant "EE-5sBlA155Mfffn-Z0SnlVy5bH6xR-3d0_hKQcReHBE" from AID "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ"...
Successfully submitted IPEX admit for grant "EE-5sBlA155Mfffn-Z0SnlVy5bH6xR-3d0_hKQcReHBE".
Marking notification "0ACDrr8ijyEbegd63Vkg9hOM" as read...
Notification "0ACDrr8ijyEbegd63Vkg9hOM" marked as read.
Waiting for notification with route "/exn/ipex/admit"...
Marking notification "0ACtUyJQ5CtqU0AT0QUFGuN_" as read...
Notification "0ACtUyJQ5CtqU0AT0QUFGuN_" marked as read.
You can continue β
### Step 6 (Path 1): ECR Credential - LE directly issues an Engagement Context Role credential to the Role holder
The vLEI framework is flexible. For ECR credentials, the Legal Entity can bypass a QVI and issue them directly. This path demonstrates that flow. The `ecrEdge` links directly to the LE's own vLEI credential, signifying its direct authority to define and issue this role.
```typescript
// Credential Data
const ecrData = {
LEI: leData.LEI,
personLegalName: 'John Doe',
engagementContextRole: 'Managing Director',
};
const ecrEdge = Saider.saidify({
d: '',
le: {
n: leCredential.sad.d,
s: leCredential.sad.s,
},
})[1];
const ecrRules = Saider.saidify({
d: '',
usageDisclaimer: {
l: 'Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled.',
},
issuanceDisclaimer: {
l: 'All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework.',
},
privacyDisclaimer: {
l: 'It is the sole responsibility of Holders as Issuees of an ECR vLEI Credential to present that Credential in a privacy-preserving manner using the mechanisms provided in the Issuance and Presentation Exchange (IPEX) protocol specification and the Authentic Chained Data Container (ACDC) specification. https://github.com/WebOfTrust/IETF-IPEX and https://github.com/trustoverip/tswg-acdc-specification.',
},
})[1];
// lE - Issue credential
prTitle("Issuing Credential")
const { credentialSaid: credentialSaid} = await issueCredential(
leClient, leAlias, leRegistrySaid,
ECR_SCHEMA_SAID,
rolePrefix,
ecrData, ecrEdge, ecrRules,
true
)
// lE - get credential
const ecrCredential = await leClient.credentials().get(credentialSaid);
// lE - Ipex grant
prTitle("Granting Credential")
const grantResponse = await ipexGrantCredential(
leClient, leAlias,
rolePrefix,
ecrCredential
)
// role - Wait for grant notification
const grantNotifications = await waitForAndGetNotification(roleClient, IPEX_GRANT_ROUTE)
const grantNotification = grantNotifications[0]
// role - Admit Grant
prTitle("Admitting Grant")
const admitResponse = await ipexAdmitGrant(
roleClient, roleAlias,
lePrefix,
grantNotification.a.d
)
// role - Mark notification
await markNotificationRead(roleClient, grantNotification.i)
// le - Wait for admit notification
const admitNotifications = await waitForAndGetNotification(leClient, IPEX_ADMIT_ROUTE)
const admitNotification = admitNotifications[0]
// le - Mark notification
await markNotificationRead(leClient, admitNotification.i)
prContinue()
```
Issuing Credential
Issuing credential from AID "le" to AID "EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu"...
{
name: "credential.EHIjHiMWsCdoEIw1pUH842Cs6z2mBWnbBINmyZbh1nVN",
metadata: {
ced: {
v: "ACDC10JSON0007dc_",
d: "EHIjHiMWsCdoEIw1pUH842Cs6z2mBWnbBINmyZbh1nVN",
u: "0AAVPaoUap9sZUbR9TN1Ex4g",
i: "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ",
ri: "EIWmQXgTeIg3XK_ixkyZST4_h7L6AdEjtXe9iMrFU31r",
s: "EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw",
a: {
d: "EIesID91PCs6euUWp5C_IhjVuLXi_Vks21BVkXJOGR4w",
i: "EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu",
LEI: "875500ELOZEL05BVXV37",
personLegalName: "John Doe",
engagementContextRole: "Managing Director",
dt: "2025-09-12T04:32:52.443000+00:00"
},
e: {
d: "EHW84Tb8rCQN71yG5TDxcmdH5mUuMnXEklYUw_5m-Rnp",
le: {
n: "EEbwZ1kvlmJmjS-w7wqMXUnHGH_jf7aGszTSyYrZsOYW",
s: "ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY"
}
},
r: {
d: "EIfq_m1DI2IQ1MgHhUl9sq3IQ_PJP9WQ1LhbMscngDCB",
usageDisclaimer: {
l: "Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled."
},
issuanceDisclaimer: {
l: "All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework."
},
privacyDisclaimer: {
l: "It is the sole responsibility of Holders as Issuees of an ECR vLEI Credential to present that Credential in a privacy-preserving manner using the mechanisms provided in the Issuance and Presentation Exchange (IPEX) protocol specification and the Authentic Chained Data Container (ACDC) specification. https://github.com/WebOfTrust/IETF-IPEX and https://github.com/trustoverip/tswg-acdc-specification."
}
}
},
depends: {
name: "witness.EITWWclApC3ObeMYJxltB7EMAdf0mjqfYk3SaRlt80ID",
metadata: { pre: "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ", sn: 4 },
done: false,
error: null,
response: null
}
},
done: true,
error: null,
response: {
ced: {
v: "ACDC10JSON0007dc_",
d: "EHIjHiMWsCdoEIw1pUH842Cs6z2mBWnbBINmyZbh1nVN",
u: "0AAVPaoUap9sZUbR9TN1Ex4g",
i: "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ",
ri: "EIWmQXgTeIg3XK_ixkyZST4_h7L6AdEjtXe9iMrFU31r",
s: "EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw",
a: {
d: "EIesID91PCs6euUWp5C_IhjVuLXi_Vks21BVkXJOGR4w",
i: "EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu",
LEI: "875500ELOZEL05BVXV37",
personLegalName: "John Doe",
engagementContextRole: "Managing Director",
dt: "2025-09-12T04:32:52.443000+00:00"
},
e: {
d: "EHW84Tb8rCQN71yG5TDxcmdH5mUuMnXEklYUw_5m-Rnp",
le: {
n: "EEbwZ1kvlmJmjS-w7wqMXUnHGH_jf7aGszTSyYrZsOYW",
s: "ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY"
}
},
r: {
d: "EIfq_m1DI2IQ1MgHhUl9sq3IQ_PJP9WQ1LhbMscngDCB",
usageDisclaimer: {
l: "Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled."
},
issuanceDisclaimer: {
l: "All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework."
},
privacyDisclaimer: {
l: "It is the sole responsibility of Holders as Issuees of an ECR vLEI Credential to present that Credential in a privacy-preserving manner using the mechanisms provided in the Issuance and Presentation Exchange (IPEX) protocol specification and the Authentic Chained Data Container (ACDC) specification. https://github.com/WebOfTrust/IETF-IPEX and https://github.com/trustoverip/tswg-acdc-specification."
}
}
}
}
}
Successfully issued credential with SAID: EHIjHiMWsCdoEIw1pUH842Cs6z2mBWnbBINmyZbh1nVN
Granting Credential
AID "le" granting credential to AID "EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu" via IPEX...
Successfully submitted IPEX grant from "le" to "EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu".
Waiting for notification with route "/exn/ipex/grant"...
[Retry] Grant notification not found on attempt #1 of 5
[Retry] Waiting 5000ms before next attempt...
Admitting Grant
AID "role" admitting IPEX grant "EH5il6RJMXJg09SxVSNHyjYIM2tTxfUiJAZJtAaapXdD" from AID "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ"...
Successfully submitted IPEX admit for grant "EH5il6RJMXJg09SxVSNHyjYIM2tTxfUiJAZJtAaapXdD".
Marking notification "0ADZtE7A9V0HxMaty3dv7_jh" as read...
Notification "0ADZtE7A9V0HxMaty3dv7_jh" marked as read.
Waiting for notification with route "/exn/ipex/admit"...
Marking notification "0AC7ovRgvjbZtI-vGNKwt7tB" as read...
Notification "0AC7ovRgvjbZtI-vGNKwt7tB" marked as read.
You can continue β
### Step 6 (Path 2): ECR Credential - QVI issues another ECR credential using the AUTH credential
This is an alternate path for ECR issuance. Here, the QVI uses the `ECR AUTH` credential it received from the LE in **Step 5** to issue an ECR credential. Just like the OOR flow, the edge block uses the `I2I` operator, proving the QVI is acting on a specific, verifiable authorization from the Legal Entity.
```typescript
// Credential Data
const ecrEdgeByQvi = Saider.saidify({
d: '',
auth: {
n: ecrAuthCredential.sad.d,
s: ecrAuthCredential.sad.s,
o: 'I2I',
},
})[1];
// QVI - Issue credential
prTitle("Issuing Credential")
const { credentialSaid: credentialSaid} = await issueCredential(
qviClient, qviAlias, qviRegistrySaid,
ECR_SCHEMA_SAID,
rolePrefix,
ecrData, ecrEdgeByQvi, ecrRules,
true
)
// QVI - get credential (with all its data)
prTitle("Granting Credential")
const ecrByQviCredential = await qviClient.credentials().get(credentialSaid);
// QVI - Ipex grant
const grantResponse = await ipexGrantCredential(
qviClient, qviAlias,
rolePrefix,
ecrByQviCredential
)
// ROLE - Wait for grant notification
const grantNotifications = await waitForAndGetNotification(roleClient, IPEX_GRANT_ROUTE)
const grantNotification = grantNotifications[0]
// ROLE - Admit Grant
prTitle("Admitting Grant")
const admitResponse = await ipexAdmitGrant(
roleClient, roleAlias,
qviPrefix,
grantNotification.a.d
)
// LE - Mark notification
await markNotificationRead(roleClient, grantNotification.i)
// QVI - Wait for admit notification
const admitNotifications = await waitForAndGetNotification(qviClient, IPEX_ADMIT_ROUTE)
const admitNotification = admitNotifications[0]
// QVI - Mark notification
await markNotificationRead(qviClient, admitNotification.i)
prContinue()
```
Issuing Credential
Issuing credential from AID "qvi" to AID "EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu"...
{
name: "credential.EL9IVpoNSEOU_dWJAV-haVJL69RyIrNp73O-VnelD1X_",
metadata: {
ced: {
v: "ACDC10JSON0007e8_",
d: "EL9IVpoNSEOU_dWJAV-haVJL69RyIrNp73O-VnelD1X_",
u: "0AAC2HWaBkQ5Q3hzLYhAAQuu",
i: "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB",
ri: "ED7V4aCJrFccq8vtvExmQW12pIe25ZT381275ZgWLxwl",
s: "EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw",
a: {
d: "EIUYcnPicYW_EQI1mfn5YxamY6uOBCxC9AVuQSUQGiHk",
i: "EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu",
LEI: "875500ELOZEL05BVXV37",
personLegalName: "John Doe",
engagementContextRole: "Managing Director",
dt: "2025-09-12T04:33:00.901000+00:00"
},
e: {
d: "EIsG1uLLjuv-3PNH8ephy0myrnVFtbXIUdC1Cs-nEq6y",
auth: {
n: "EMjzuB4T9TsuTEpi-J2ECky2hD1Ah6Z1xG-hCCIqJL2B",
s: "EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g",
o: "I2I"
}
},
r: {
d: "EIfq_m1DI2IQ1MgHhUl9sq3IQ_PJP9WQ1LhbMscngDCB",
usageDisclaimer: {
l: "Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled."
},
issuanceDisclaimer: {
l: "All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework."
},
privacyDisclaimer: {
l: "It is the sole responsibility of Holders as Issuees of an ECR vLEI Credential to present that Credential in a privacy-preserving manner using the mechanisms provided in the Issuance and Presentation Exchange (IPEX) protocol specification and the Authentic Chained Data Container (ACDC) specification. https://github.com/WebOfTrust/IETF-IPEX and https://github.com/trustoverip/tswg-acdc-specification."
}
}
},
depends: {
name: "witness.EO4Fpx7u92KiGCZkcUCny4nVroekOJxVDwCav1oo_EW5",
metadata: { pre: "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB", sn: 4 },
done: false,
error: null,
response: null
}
},
done: true,
error: null,
response: {
ced: {
v: "ACDC10JSON0007e8_",
d: "EL9IVpoNSEOU_dWJAV-haVJL69RyIrNp73O-VnelD1X_",
u: "0AAC2HWaBkQ5Q3hzLYhAAQuu",
i: "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB",
ri: "ED7V4aCJrFccq8vtvExmQW12pIe25ZT381275ZgWLxwl",
s: "EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw",
a: {
d: "EIUYcnPicYW_EQI1mfn5YxamY6uOBCxC9AVuQSUQGiHk",
i: "EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu",
LEI: "875500ELOZEL05BVXV37",
personLegalName: "John Doe",
engagementContextRole: "Managing Director",
dt: "2025-09-12T04:33:00.901000+00:00"
},
e: {
d: "EIsG1uLLjuv-3PNH8ephy0myrnVFtbXIUdC1Cs-nEq6y",
auth: {
n: "EMjzuB4T9TsuTEpi-J2ECky2hD1Ah6Z1xG-hCCIqJL2B",
s: "EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g",
o: "I2I"
}
},
r: {
d: "EIfq_m1DI2IQ1MgHhUl9sq3IQ_PJP9WQ1LhbMscngDCB",
usageDisclaimer: {
l: "Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled."
},
issuanceDisclaimer: {
l: "All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework."
},
privacyDisclaimer: {
l: "It is the sole responsibility of Holders as Issuees of an ECR vLEI Credential to present that Credential in a privacy-preserving manner using the mechanisms provided in the Issuance and Presentation Exchange (IPEX) protocol specification and the Authentic Chained Data Container (ACDC) specification. https://github.com/WebOfTrust/IETF-IPEX and https://github.com/trustoverip/tswg-acdc-specification."
}
}
}
}
}
Successfully issued credential with SAID: EL9IVpoNSEOU_dWJAV-haVJL69RyIrNp73O-VnelD1X_
Granting Credential
AID "qvi" granting credential to AID "EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu" via IPEX...
Successfully submitted IPEX grant from "qvi" to "EHdZZzxpaPDxHkPNeAwxYje5ngW0GPzSQCqnutfO5Bbu".
Waiting for notification with route "/exn/ipex/grant"...
[Retry] Grant notification not found on attempt #1 of 5
[Retry] Waiting 5000ms before next attempt...
Admitting Grant
AID "role" admitting IPEX grant "EIL5P-3nLK8NTXH3CurFttmC5F4Ay5r77KOKSkgG2W4C" from AID "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB"...
Successfully submitted IPEX admit for grant "EIL5P-3nLK8NTXH3CurFttmC5F4Ay5r77KOKSkgG2W4C".
Marking notification "0ACHUs5HNpKKTvg5tcvIl8O3" as read...
Notification "0ACHUs5HNpKKTvg5tcvIl8O3" marked as read.
Waiting for notification with route "/exn/ipex/admit"...
Marking notification "0AAEkrU39oyti4pd-cWylNYS" as read...
Notification "0AAEkrU39oyti4pd-cWylNYS" marked as read.
You can continue β
## The vLEI Reporting Agent
Once credentials like the ones created in this chain are issued and held by their respective entities, a common next step is to present them for verification or auditing. The vLEI Audit Reporting Agent, known as Sally, is a component designed for this purpose.
Sally acts as a direct-mode validator. It receives presentations of vLEI credentials (like the QVI, vLEI and OOR credentials), cryptographically verifies their structure and integrity, and then performs a POST request to a pre-configured webhook URL. This allows external systems to receive trusted, real-time notifications about credential presentations and revocations within the vLEI ecosystem.
For more details about Sally go to its Github **[repository](https://github.com/GLEIF-IT/sally)**.
To continue with the example you need to start the sally service following the instructions below (β οΈ The command is programatically generated) in the root directory of these training materials so the correct docker compose file is found.
```typescript
// Ask user to start the sally service setting the proper root of trust for this run
prAlert(`Please run this command on you local machine in the vlei-trainings directory before continuing, and wait for the container to start:`)
prMessage(`GEDA_PRE=${gleifPrefix} docker compose up --build direct-sally -d`)
const isReady = confirm("Is the service running and ready to accept connections?");
if (isReady) {
prContinue()
} else {
throw new Error("β Script aborted by user. Please start the service and run the script again.");
}
```
Please run this command on you local machine in the vlei-trainings directory before continuing, and wait for the container to start:
GEDA_PRE=EECGZ7vbYzOM1FWicPFIot-4AiteMX6Xr5htEVAA5Iq2 docker compose up --build direct-sally -d
Is the service running and ready to accept connections? [y/N] y
You can continue β
βΉοΈ NOTE
When the sally service is started, the Root of Trust prefix is passed via the GEDA_PRE variable.
### The Presentation Workflow
The following code block performs the entire presentation flow in four main steps.
1. **Establishing Contact with Sally:** Before the Legal Entity client (`leClient`) can present its credentials, it must first know how to communicate with Sally. The first action in the code is `resolveOOBI(leClient, sallyOOBI, sallyAlias)`, which resolves Sally's OOBI to establish this connection.
2. **Running the Local Sally Service:** The code will then prompt you to start the local Sally service using a `docker compose` command. This command is critical for the demonstration:
- It starts a container running the Sally agent.
- It also starts a simple hook service that acts as the webhook endpoint, listening for and storing the reports that Sally will post.
- The `GEDA_PRE=${gleifPrefix}` variable passed to the command provides Sally with the Root of Trust AID for this specific notebook run. Sally requires this information to validate the entire credential chain, from the LE credential presented to it all the way back to its root anchor at GLEIF.
3. **Presenting the Credential:** The `presentToSally()` function uses `ipexGrantCredential` to send the `leCredential` to Sally's AID. This action is the `signify-ts` equivalent of using `kli ipex grant` for credential presentation and initiates the verification process within Sally.
4. **Verifying the Audit Report:** Finally, the `pollForCredential()` function simulates a webhook listener. Instead of running a full server, it simply polls the hook service where Sally sends its report. Upon receiving a successful `200 OK` response, it fetches and displays the JSON report, confirming that Sally received the presentation, successfully verified the trust chain, and dispatched its audit report.
```typescript
// Present to sally
const sallyOOBI = "http://direct-sally:9823/oobi"
const sallyPrefix = "ECLwKe5b33BaV20x7HZWYi_KUXgY91S41fRL2uCaf4WQ"
const sallyAlias = "sally"
// Ipex presentation of LE credential
async function presentToSally(){
prTitle("Presenting vLEI Credential to sally")
const grantResponse = await ipexGrantCredential(
leClient, leAlias,
sallyPrefix,
leCredential
)
}
// Poll webhook for LE credential data
const webhookUrl = `${"http://hook:9923"}/?holder=${lePrefix}`;
async function pollForCredential() {
const TIMEOUT_SECONDS = 25;
let present_result = 0;
const start = Date.now();
while (present_result !== 200) {
if ((Date.now() - start) / 1000 > TIMEOUT_SECONDS) {
prMessage(`TIMEOUT - Sally did not receive the Credential`);
break; // Exit the loop
}
// Run curl to get just the HTTP status code
try {
const command = new Deno.Command("curl", {
args: ["-s", "-o", "/dev/null", "-w", "%{http_code}", webhookUrl],
});
const { stdout } = await command.output();
const httpCodeStr = new TextDecoder().decode(stdout);
present_result = parseInt(httpCodeStr, 10) || 0; // Default to 0 if parsing fails
prMessage(`Received ${present_result} from Sally`);
} catch (error) {
prMessage(`[QVI] Polling command failed: ${error.message}`);
present_result = 0; // Reset on failure to avoid exiting loop
}
if (present_result !== 200) {
await sleep(1000);
}
}
if (present_result === 200) {
prTitle("Fetching Credential Info...");
const command = new Deno.Command("curl", {
args: ["-s", webhookUrl]
});
const { stdout } = await command.output();
const responseBody = new TextDecoder().decode(stdout);
try {
const jsonObject = JSON.parse(responseBody);
const formattedJson = JSON.stringify(jsonObject, null, 2);
prMessage(formattedJson);
} catch (error) {
prMessage("Response was not valid JSON. Printing raw body:");
prMessage(responseBody);
}
}
}
while(! await isServiceHealthy("http://direct-sally:9823/health")){
prMessage(`Please run this command on you local machine before continuing, and wait for the container to start:`)
prMessage(`GEDA_PRE=${gleifPrefix} docker compose up --build direct-sally -d`)
await sleep(5000);
}
await resolveOOBI(leClient, sallyOOBI, sallyAlias)
await resolveOOBI(qviClient, sallyOOBI, sallyAlias)
await presentToSally()
await pollForCredential()
prContinue()
```
Checking health at: http://direct-sally:9823/health
Received status: 200. Service is healthy.
Resolving OOBI URL: http://direct-sally:9823/oobi with alias sally
Successfully resolved OOBI URL. Response: OK
Contact "sally" added/updated.
Resolving OOBI URL: http://direct-sally:9823/oobi with alias sally
Successfully resolved OOBI URL. Response: OK
Contact "sally" added/updated.
Presenting vLEI Credential to sally
AID "le" granting credential to AID "ECLwKe5b33BaV20x7HZWYi_KUXgY91S41fRL2uCaf4WQ" via IPEX...
Successfully submitted IPEX grant from "le" to "ECLwKe5b33BaV20x7HZWYi_KUXgY91S41fRL2uCaf4WQ".
Received 404 from Sally
Received 404 from Sally
Received 404 from Sally
Received 404 from Sally
Received 404 from Sally
Received 404 from Sally
Received 200 from Sally
Fetching Credential Info...
{
"credential": "EEbwZ1kvlmJmjS-w7wqMXUnHGH_jf7aGszTSyYrZsOYW",
"type": "LE",
"issuer": "ENI8jRbuVRvHf9XsW5wudw_NBJkl5XTz11Qo7v-qwtIB",
"holder": "ELkTLFJYB0yoO-R2slbPlm3l6vEyxMCzCs6ovP7ii_vQ",
"LEI": "875500ELOZEL05BVXV37",
"personLegalName": "",
"officialRole": ""
}
You can continue β
π SUMMARY
This notebook provided a practical walkthrough of a simplified vLEI trust chain using Signify-ts, demonstrating:
- Hierarchical Trust: Each credential in the chain cryptographically references its authorizing credential, creating a verifiable link back to the Root of Trust (GLEIF).
- Multiple Issuance Paths: The vLEI ecosystem supports different issuance models, including direct issuance by a Legal Entity (for ECRs) and authorized issuance by a QVI on behalf of an LE (for OORs and ECRs).
- IPEX Protocol: The Issuance and Presentation Exchange protocol facilitates the secure delivery of credentials between parties using a grant/admit message flow.
- Schema Compliance: Every credential adheres to a specific, SAID-identified vLEI schema, ensuring interoperability and consistent data structures.
- Credential Chaining: The 'edges' section of an ACDC is used to reference the SAID of a source credential, explicitly defining the chain of authority.
- Audit Agent Interaction: A credential holder can present their ACDC to an external agent like Sally for verification and auditing. Sally validates the entire trust chain and notifies an external service via a webhook.
This represents a functional, albeit simplified, model of how the vLEI ecosystem issues verifiable credentials for legal entities and their roles while maintaining a robust and verifiable chain of trust.
[<- Prev (vLEI Ecosystem)](103_05_vLEI_Ecosystem.ipynb) | [Next (Integrating Chainlink CCID with vLEI) ->](220_10_Integrating_Chainlink_CCID_with_vLEI.ipynb)
# Known Issues
This document outlines several known issues that users may encounter while working with the KERI ecosystem. Understanding these issues can help in troubleshooting and setting expectations during development and testing.
## Issue 1: kli vc create Hangs with NI2I Operator
When creating a chained credential that uses the Not-Issuer-To-Issuee (`NI2I`) operator, the kli vc create command may hang indefinitely.
**Expected Behavior**
The command `kli vc create` should complete successfully when issuing a credential with an `NI2I` edge.
**Actual Behavior**
The command execution stalls after displaying the following log messages, and never completes
**Workaround**
There is currently no known workaround for this issue.
For more technical details and to track the status of this issue, please refer to:https://github.com/WebOfTrust/keripy/issues/1040
## Issue 2: KERIA Multisig State Synchronization Lag
In a multisig group managed by KERIA, members who are not required to sign an event (based on the signing threshold) may not have their local state updated after the event is completed by other members.
**Expected Behavior**
All members of a multisig group should be able to stay synchronized with the group's KEL, regardless of whether their signature was required for a specific event.
**Actual Behavior**
The problem is that multisig members in KERIA are not being updated with the latest state when they join a transaction after the fact. For example, if the signing threshold for a three-participant multisig group is set to 2-of-3 (e.g., ['1/2','1/2','1/2']), the operation completes as soon as the first two members sign. The third member, who did not sign, is not notified of the completed event and their local KEL for the group AID becomes outdated. They are unable to "see" the new event, such as a credential registry creation, that was anchored by the interaction.
**Workaround**
There is currently no known workaround for this issue. A temporary mitigation is to set signing thresholds to require signatures from all participants (e.g., 3-of-3), which forces all members to be involved and thus remain synchronized. However, this negates the flexibility and resilience benefits of partial thresholds.
For more technical details and to track the status of this issue, please refer to: https://github.com/WebOfTrust/keria/issues/316