# Welcome to vLEI Training - 101 This collection of Notebooks is designed to guide you through the foundational concepts of the [Key Event Receipt Infrastructure](https://trustoverip.github.io/tswg-keri-specification/) (KERI) and [Authentic Chained Data Containers](https://trustoverip.github.io/tswg-acdc-specification/) (ACDC) protocols, followed by the workings of the [verifiable Legal Entity Identifier](https://www.gleif.org/en/organizational-identity/introducing-the-verifiable-lei-vlei/introducing-the-vlei-ecosystem-governance-framework) (vLEI) ecosystem. We aim to equip you with the knowledge needed to build applications leveraging this powerful identity technology. After completing this training, you will: - Understand the KERI protocol - Understand the ACDC protocol - Understand the vLEI ecosystem - Have the basis to develop your own vLEI POC ## Prerequisites The training aims to be accessible, but having background knowledge will certainly smooth your learning journey. 1. **Command-Line Interface (CLI) Familiarity:** The training will involve using the KERI Command Line Interface (KLI). Therefore, having prior experience working with a terminal or command prompt (like Bash, Zsh, PowerShell, or Windows CMD) is highly beneficial. 2. **Conceptual Understanding of Digital Identity:** A general awareness of digital identity concepts and some understanding of the limitations of traditional systems will provide useful context for understanding KERI's purpose. 3. **Basic Cryptography Concepts:** Having a basic understanding of what public and private keys are and the general idea behind digital signatures and hash functions will give you a head start. 4. **Python Programming:** The training includes several Python scripts. 5. **TypeScript Programming:** Code snippets from the 102 module notebooks utilize TypeScript code. 6. **Docker Basics:** For setting up and troubleshooting more complex KERI environments or running components like witnesses or agents, a basic understanding of Docker concepts (containers, images, `docker-compose`) will be useful. ## Understanding Your Learning Environment This training series utilizes Jupyter Notebooks to provide an interactive and hands-on learning experience. Jupyter Notebooks allow for a mix of explanatory text, and live, executable code cells, creating a dynamic way to understand complex topics like KERI, ACDCs, and the vLEI ecosystem. ### Notebook Philosophy Each notebook in this series is designed to be largely **stand-alone for the concepts it introduces**, building upon the knowledge from previous notebooks. While conceptual links are strong, the code examples within a specific notebook are generally self-contained or rely on a clearly defined setup at the beginning of that notebook. Crucially, **cells within a single notebook are meant to be executed in sequence from top to bottom.** Variables, states, and environments created in earlier cells are often prerequisites for later cells to function correctly. Running cells out of order, or skipping cells, will likely lead to errors or unexpected behavior. ### Navigating Large Notebooks For longer notebooks, navigating can be made easier using the **Table of Contents (ToC)** feature. In Jupyter Lab, you can find this in the left sidebar. * Look for an icon that resembles a list or a document outline. Clicking this will open a navigable ToC based on the Markdown headings (H1, H2, H3, etc.) in the notebook. * This allows you to quickly jump to specific sections of the notebook, which is especially helpful when reviewing material or looking for particular topics. ### Interacting with Notebook Cells Jupyter Notebooks are composed of different types of cells, primarily: * **Markdown Cells:** These cells contain explanatory text, like the one you are reading now. They are formatted using Markdown syntax, which allows for rich text formatting, images, and links. You do not "run" Markdown cells in the same way as code cells, but they are rendered to display the formatted text. * **Code Cells:** These cells contain executable code. In this training series, you will encounter: * Shell commands (for `kli`): Prefixed with an exclamation mark (`!`), e.g., `!kli status`. * Python code: For scripting, examples, and utility functions. * TypeScript code: In the 102 module notebooks for `signify-ts` examples. **Running Code Cells:** To execute a code cell: 1. Select the cell by clicking on it. 2. Press `Shift + Enter` to run the current cell and automatically select the next cell. 3. Alternatively, you can click the "Run" button (a play icon ▢️) in the toolbar. When a code cell is running, an asterisk (`[*]`) will appear in the brackets to its left. Once execution is complete, a number (e.g., `[1]`) will replace the asterisk, indicating the order of execution. Any output from the code (text, errors, etc.) will be displayed directly below the cell. **Running All Cells:** If you want to run all cells in a notebook from top to bottom, especially after restarting the kernel or opening the notebook fresh, you can use the "Restart Kernel and Run All Cells" option. * In Jupyter Lab, this is found in the "Kernel" menu (`Kernel > Restart Kernel and Run All Cells...`) or as a button in the toolbar (represented by a double play icon ⏩). * This is a convenient way to ensure the entire notebook is executed in the correct order.
πŸ’‘ TIP
If you see `In [*]:` next to a cell for a long time, it means the code is still running. Some operations, especially those involving network communication or complex cryptographic processes, might take a few moments to complete.
### Managing the Notebook Kernel Each active notebook is connected to a "kernel," which is the computational engine that executes the code in the notebook's cells. * **Restarting the Kernel:** If you encounter persistent errors, or if you want to reset the notebook's state and start fresh (e.g., clear all variables), you can restart the kernel. * In Jupyter Lab, this is done via the "Kernel" menu: `Kernel > Restart Kernel...`. * Restarting the kernel will require you to re-run cells from the beginning to redefine variables and recreate the necessary state (or use "Restart Kernel and Run All Cells"). * **Interrupting the Kernel:** If a cell is taking too long to execute or you suspect it's stuck in an infinite loop, you can interrupt the kernel. * In Jupyter Lab, use `Kernel > Interrupt Kernel`. * You can also use the **Stop button** (A square ⏹️ icon) in the toolbar to interrupt the currently running cell. ### Clearing Output You can clear the output of a single cell or all cells in a notebook: * **Current Cell:** `Edit > Clear Output` (or right-click the cell). * **All Cells:** `Edit > Clear All Outputs`. This can be useful for decluttering the view or before re-running a notebook from scratch. ## Software Versions This material was created and tested to work with: - **[weboftrust/keri:1.2.6](https://github.com/WebOfTrust/keripy/releases/tag/1.2.6)** - **[gleif/keria:0.3.0](https://github.com/GLEIF-IT/keria/releases/tag/0.3.0)** - **[weboftrust/signify-ts:0.3.0-rc1](https://www.npmjs.com/package/signify-ts)** - **[weboftrust/vlei:1.0.0](https://github.com/WebOfTrust/vLEI/releases/tag/1.0.0)**
🧩 DID YOU KNOW?
KERI, ACDC, and the vLEI ecosystem offer a strong foundation for secure digital interactions. However, achieving truly strong security requires additional effort. Real-world safety depends on proper implementation and security practices. Even the best technology can be weakened by things like losing control of private keys, or people being tricked into giving away their access (social engineering). Achieving real security is about combining strong technology with sound operational security measures.
[<- Prev (TOC)](000_Table_of_Contents.ipynb) | [Next (Intro) ->](101_07_Introduction_to-KERI_ACDC_and_vLEI.ipynb) # Foundations: KERI, ACDC, and the vLEI Ecosystem
🎯 OBJECTIVE
Provide a high-level overview of the three foundational concepts we'll be covering during this training:
  • The KERI protocol for secure identifiers
  • The ACDC protocol for verifiable credentials
  • The GLEIF vLEI ecosystem, which applies these technologies to organizational identity.

    Consider this a starting point; we'll dive into the details, practical examples, and specific commands in the notebooks that follow.
  • ## The KERI Protocol **KERI** stands for **Key Event Receipt Infrastructure**, invented by [Dr. Samuel Smith](https://keri.one/131-2/). It's a decentralized key management infrastructure (DKMI) that aims to provide a secure and decentralized identity layer for the internet, focusing on establishing trust through cryptographic proof rather than relying solely on centralized authorities. It is a "never trust, always verify" security model with no share secrets, meaning no shared passwords, keys, or other types of cryptographic secrets. This means that KERI is a signed-everything model, meaning every communication between components is signed so that trust in each transmission can be verified by verifying its signature or by verifying the anchoring of data to a key event log (KEL). Core Ideas: * **Self Addressing Identifiers (SAIDs):** [Self addressing identifiers](https://trustoverip.github.io/tswg-keri-specification/#term:said) are a special type of content-addressable identifier where the identifier is based on and embedded within the data it refers to, making it self referential. The embedding happens after computing the digest in a two step digest and embedding process. See the [spec reference](https://trustoverip.github.io/tswg-said-specification/draft-ssmith-said.html). * **Autonomic Identifiers (AIDs):** KERI's foundation is built on [self-certifying identifiers](https://trustoverip.github.io/tswg-keri-specification/#self-certifying-identifier-scid) called AIDs. These identifiers are generated from and cryptographically bound to key pairs controlled by an entity, eliminating the need for a central registration authority for the identifier itself. An AID is a SAID derived from the first event (inception event) in a key event log (KEL). * **Key Event Logs (KELs):** Each AID has an associated KEL, which is a secure, append-only log of signed "key events" (like identifier creation, key rotation, etc.). This log provides a verifiable key history, or provenance, of the control over the AID. Anyone can verify the current authoritative keys for an AID by processing its KEL. * **End-Verifiability:** KERI emphasizes that identifier control and key events can be verified by anyone, anywhere, using only the KEL, without trusting intermediaries. * **Witnesses:** For high availability and resilience, the person controlling of keys for an AID (controller) can designate witnesses who receive, verify, and store key events. This both allows the controller to set security thresholds for event signing and also makes the KEL accessible when the controller is offline. * **And more:** KERI has many other advanced features, but we'll focus on the fundamentals in this introduction. ## The ACDC Protocol **ACDC** stands for **Authentic Chained Data Container**. It is KERI's native format for Verifiable Credentials (VCs), designed to work within KERI-based ecosystems. **Core Ideas:** * **Verifiable Credentials:** ACDCs are digital containers for claims or attributes (like a name, role, or authorization) that are issued by one identifier (AID) to another. * **Built on KERI:** ACDCs leverage AIDs for identifying issuers and issues. The validity and status (issued, revoked) of an ACDC are anchored to the issuer's Key Event Log (KEL) through a secondary log called a Transaction Event Log (TEL). * **Schemas & SAIDs:** Each ACDC conforms to a specific Schema, which defines its structure and data types. Both the schema and the ACDC instance itself are identified using SAIDs (Self-Addressing Identifiers), making them tamper-evident. * **Chaining (Edges):** ACDCs can be cryptographically linked together using "edges," forming verifiable chains or graphs of evidence (e.g., an approval credential linking back to the request credential). * **Rules:** ACDCs can optionally include embedded machine-readable rules or legal prose (like Ricardian Contracts). * **IPEX (Issuance and Presentation Exchange):** a [credential exchange protocol](https://trustoverip.github.io/tswg-acdc-specification/#issuance-and-presentation-exchange-ipex) defining a mechanism and workflow for how ACDCs are issued between parties and how they are presented for verification in a securely attributable way. This protocol also defines a workflow for [graduated disclosure](https://trustoverip.github.io/tswg-acdc-specification/#graduated-disclosure), a variant of selective disclosure that allows for progressive, selective unblinding of claims or attributes after an agreement has been negotiated between the discloser (holder) and the receiver (disclosee) of an ACDC. ## The GLEIF vLEI Ecosystem The **verifiable Legal Entity Identifier (vLEI)** is a system pioneered by the Global Legal Entity Identifier Foundation (GLEIF) to create a secure, digitized version of the traditional LEI used for organizational identity. It aims to enable automated authentication and verification of organizations globally. **Core Ideas:** * **Digital Counterpart to LEI:** The vLEI acts as a digitally verifiable representation of an organization's LEI code, enabling automated, machine-readable verification. * **Built on KERI/ACDC:** The vLEI infrastructure is built using the KERI protocol and represents vLEI credentials as ACDCs. This leverages KERI's security and ACDC's verifiable credential format. * **Trust Chain / Ecosystem:** The vLEI system establishes a chain of trust: * **GLEIF (Root of Trust):** GLEIF operates as the root of the ecosystem; its AID and KEL serve as the ultimate anchor for verifying the authority of QVIs. The root of trust uses **identifier delegation** to establish an authorization chain from the Root of Trust to the QVI through delegation chained KELs. * **Qualified vLEI Issuers (QVIs):** GLEIF uses its KERI identity to issue QVI credentials to a trusted network of QVIs. This means that both identifier delegation and credential issuance are used to delegate authority from GLEIF to QVIs for the purpose of allowing QVIs to issue vLEI credentials. * **Organizations:** QVIs are qualified to issue vLEI credentials, which represent the organization's identity, to legal entities. * **Organizational Role:** An organization holding a vLEI can then issue specific **vLEI Role Credentials** to individuals representing the organization in official or functional capacities (e.g., CEO, authorized signatory, supplier). These role credentials cryptographically bind the person's identity in that role to the organization's vLEI. * **Official Organizational Role (OOR) Credential:** A person representing a legal entity may be issued an OOR credential that indicates their official role in an organization. The rules for OOR credential issuance must follow the ISO 5009 Official Organization Role standard for official role names. * **Engagement Context Role (ECR) Credential:** An ECR credential indicates a person performs a given role for a company-defined context. It is a more permissive credential type where the name of the role is legal-entity specific. * **QVI Workflow:** The workflow centrally involves the QVIs. GLEIF qualifies these issuers. A QVI interacts with an organization to verify its identity information (linked to its traditional LEI) and then uses its verifiable delegated authority from GLEIF to issue the organization its primary vLEI credential. This QVI issuance step is crucial for establishing the organization's verifiable digital identity within the ecosystem. The vLEI ecosystem uses KERI and ACDC to extend the existing LEI system into the digital realm, creating a globally verifiable system for organizational identity that incorporates the roles individuals hold within an organization organizations, all anchored back to GLEIF as the root of trust.
    πŸ“ SUMMARY
    KERI provides the secure identifier layer, ACDC provides the credential format on top of KERI, and vLEI is a specific application of both for organizational identity.
    [<- Prev (Welcome)](101_05_Welcome_to_vLEI_Training_-_101.ipynb) | [Next (KERI Command Line Interface - KLI) ->](101_10_KERI_Command_Line_Interface.ipynb) # Understanding the KERI Command Line Interface (KLI)
    🎯 OBJECTIVE
    Introduce the KERI Command Line Interface (KLI) and demonstrate some of its basic utility commands.
    ## Using KLI in Notebooks Throughout these notebooks, you will interact with the KERI protocol using the **KLI**. The KLI is the standard text-based tool for managing identifiers and infrastructure directly from your computer's terminal. Since you are working within Jupyter notebooks, the KLI commands are written with an exclamation mark prefix (`!`). This tells the notebook environment to run the command in the underlying system shell, rather than as Python code. So, you'll frequently see commands structured like this: `!kli [options]` **What can you do with KLI?** The KLI provides a wide range of functionalities. Key capabilities include: - **Identifier management**: Management and creation of keystores and identifiers - **Utility functions**: Functions to facilitate KERI-related operations for debugging and troubleshooting. - **Credential management**: Creation of credentials - **Comunication operations**: Establishing connections between AIDs - **IPEX actions**: To issue and present credentials - **Run witness**: Start a witness process in order to receipt key events - **Others**: The KLI provides commands for most of the features available in the KERI and ACDC protocol implementations.
    ℹ️ NOTE
    There are UI based methods to manage Identifiers, known as wallets, but for the purpose of this training, the KLI offers a good compromise between ease of use and visibility of technical details.
    ## Overview of Basic Utilities Let's explore some helpful commands available in the **KERI Command Line Interface (KLI)**. This isn't a complete list of every command, but it covers some essential utilities that you'll find useful as you work with KERI. **KERI library version** ```python !kli version ``` Library version: 1.2.8 **Generate a salt**: Create a new random salt (or seed) in the fully-qualified [CESR](https://trustoverip.github.io/tswg-cesr-specification/) format. A salt is a random value used as an input when generating cryptographic key pairs to help ensure their uniqueness and security. What it means to be fully qualified is that the bytes in the cryptographic salt are ordered according to the CESR protocol. This ordering will be explained in a later training when CESR is introduced and explained. For now just think of CESR as a custom file format for KERI and ACDC data. ```python # This will output a qualified base64 string representing the salt !kli salt ``` 0ADSwyz06mraopjjyazL_XKf **Generate a passcode**: The passcode is used to encrypt your keystore, providing an additional layer of protection. ```python # This will output a random string suitable for use as an encryption passcode !kli passcode generate ``` v5yK8PGzjeYcKp5hxtvGZ **Print a timestamp**: Timestamps are typically used in operations involving multiple signers with what are called multi-signature (or "multisig") groups. ```python !kli time ``` 2025-07-18T00:16:08.975712+00:00 **Display help menu** ```python !kli -h ``` usage: kli [-h] command ... options: -h, --help show this help message and exit subcommands: command aid Print the AID for a given alias challenge clean Cleans and migrates a database and keystore contacts decrypt Decrypt arbitrary data for AIDs with Ed25519 p ... delegate did ends escrow event Print an event from an AID, or specific values ... export Export key events in CESR stream format incept Initialize a prefix init Create a database and keystore interact Create and publish an interaction event introduce Send an rpy /introduce message to recipient wi ... ipex kevers Poll events at controller for prefix list List existing identifiers local location mailbox migrate multisig nonce Print a new random nonce notifications oobi passcode query Request KEL from Witness rename Change the alias for a local identifier rollback Revert an unpublished interaction event at the ... rotate Rotate keys saidify Saidify a JSON file. salt Print a new random passcode sign Sign an arbitrary string ssh status View status of a local AID time Print a new time vc verify Verify signature(s) on arbitrary data version Print version of KLI watcher witness Additional commands will be introduced as they are used in upcoming trainings. [<- Prev (Intro)](101_07_Introduction_to-KERI_ACDC_and_vLEI.ipynb) | [Next (Controllers and Identifiers) ->](101_15_Controllers_and_Identifiers.ipynb) # KERI Core: Controllers, Identifiers, and Key Event Logs
    🎯 OBJECTIVE
    Explain the fundamental KERI concepts of Autonomic Identifiers (AIDs), the Controller entity, and the Key Event Log (KEL).
    Before we dive into creating identifiers and doing operations with the KLI, let's understand two fundamental concepts: **Identifiers** and the **Controller**. ## Autonomic Identifiers (AIDs) Identifiers are a generic term; they exist in many forms, but here we are concerned with digital identifiers. In a general sense, an identifier is a name, label, or sequence of characters used to uniquely identify something within a given context. Identifiers are useful to assign claims to something or to locate a resource. Common examples of identifiers are domain names, an email, an ID Number, and so on. KERI identifiers are called **Autonomic Identifiers (AIDs)**. They have properties that give them additional capabilities compared to traditional digital identifiers. Their most important attribute is to maintain a stable identifier over time while their controlling keys may be rotated to keep the identifier secure. There are many different properties of AIDs: - **Universally Unique:** Like standard UUIDs, AIDs are designed to be globally unique without needing a central issuing authority, thanks to their cryptographic foundation. Β  - **Provide asymmetric cryptography features:** Beyond being an identifier, AIDs provide signing and verification capabilities due to being build on public and private key pairs. - **Cryptographically Bound Control:** AIDs are bound to a set of cryptographic key pairs at time of creation, called the **inception event**, and later key pairs from a **rotation event**; this binding forms the basis of their security and allows the holder of the private key(s) to control the identifier and prove that control through digital signatures and a key event log (KEL). - - **Control Over Time:** AIDs are designed for persistent control. The identifier's control history and current authoritative keys are maintained in a verifiable **key event log (KEL)**, allowing anyone to determine the current authoritative keys and verify the control history. This enables keys to be rotated (changed) securely over time without abandoning the identifier itself, even if old keys are compromised. - **Self-Managed:** Unlike traditional identifiers (like usernames or domain names) that rely on central administrators or registries, an AID is managed directly by its owner(s) β€” known as the Controller β€” through cryptographic means (specifically, their private keys). This makes AIDs maximally decentralized, durectly controlled by end-users. - **Self-Certifying:** An AID inherently proves its own authenticity. Its validity stems directly from its cryptographic link to its controlling keys, established at its creation, not from an external authority vouching for it. - **Authenticates & Authorizes:** The cryptographic nature of an AID allows its Controller to directly prove their control (authenticate) and grant permissions (authorize actions or access related to the AID) without needing a third-party identity system. - **Multi-Signature Control (Multisig):** An AID does not have to be controlled by only one Controller. KERI supports configurations requiring multiple identifiers, using key pairs held by one or more Controllers, to cooperatively authorize actions. This can involve needing a specific number of signatures (e.g., 3 out of 5) or advanced weighted threshold multi-signature schemes. Β  - **Secure Key Rotation (Pre-rotation):** When keys controlling an AID need to be changed (rotated), KERI uses a highly secure [pre-rotation](https://trustoverip.github.io/tswg-keri-specification/#key-rotationpre-rotation) method. In each rotation event, a secure commitment is made to the next set of rotation keys that hides the actual next public keys by using a digest of each next key. This means the private keys for the next rotation remain unexposed and secure until they are actually needed, protecting the rotation process itself from attack. Β  - **Identifier Delegation:** A Controller of one AID can securely grant specific, often limited or revokable, authority as a delegator to another AID, the delegate. This is an important capability for scaling signing operations by using many delegated identifiers in parallel. Don't worry if these features raise many questions right now. We will explain the "how" behind them gradually in the sections to come. ## The Controller Role In KERI, the Controller is the entity that holds the private cryptographic key(s) associated with an Autonomic Identifier and is therefore responsible for managing it. This possession of the private key(s) is the source of its authority and control over the AID. Β  So, a Controller is an entity managing their identifiers and the key pairs for those identifiers. Some controller scenarios include: - Personal identity - An individual managing their own digital identity. - Organizational identity - An organization managing its official identifier. - Agentic identity - An autonomous piece of software or device managing its own identifier. - Delegated agentic identity - An autonomous piece of software acting on behalf of a person or organization. Β  - Multisignature identity - A group managing a shared identifier via multi-signature schemes. Participants could be people, organizations, or AI agents. The most important aspect is an entity has direct access to the private keys the AID is derived from. While the Controller holds authority over the AID it relies on software to operate and maintain it. In this training, you will first be using the KLI as the Controller’s tool for interacting with and managing AIDs. Later trainings will include using the Signify and KERIA tooling to interact with AIDs. ## Key Event Logs (KELs) - Never Trust, Always Verify The Controller's authority more than a trusted assertion, it is proven using cryptography through a verification process. Remember, KERI is a "never trust, always verify" protocol. No matter what statements a Controller makes they cannot be relied upon unless they can be cryptographically verified. KERI is a "signed everything" architecture with no shared secrets. This means no bearer tokens like JWT and OAuth have. Instead KERI uses cryptographic signature to create trust. The basis of this trust comes from Controllers signing statements with their private key pairs. This means Controllers possess the private keys associated with their AID. They use these keys to sign messages and authorize actions. The association between key pairs and an AID is initially formed by what is called the **inception event**, the first event in a **Key Event Log (KEL)**. Every significant action taken by a Controller regarding their AID, like creating the identifier (inception), changing its keys (rotation), or other interactions, is recorded as a **Key Event** in the KEL. These Key Events are stored sequentially in a **Key Event Log (KEL)**. Think of the KEL as the official history book for an AID. Like a blockchain, a KEL is a hash chained data structure. ### Key Event Log diagram ```mermaid graph RL %% Define event nodes ICP["Inception πŸ”‘"] ROT1["Rotation 1 πŸ”‘"] ROT2["Rotation 2 πŸ”‘"] IXN1["Interaction 1 βš“"] %% Define backward chaining ROT1 --> ICP ROT2 --> ROT1 IXN1 --> ROT2 %% Optional node styling for UML appearance classDef eventBox fill:#f5f5f5,stroke:#000,stroke-width:1px,rx:5px,ry:5px,font-size:14px; class ICP,ROT1,ROT2,IXN1, eventBox; ``` Here are some details about the KEL * It starts with the AID's "birth certificate" – the **Inception Event**. * Every subsequent authorized change (like a key rotation) is added as a new entry, cryptographically linked to the previous one. Each new event is signed by the keys referred to in the last rotation event, or from the inception event if no rotations have occurred yet. * Anyone can potentially view the KEL to verify the AID's history and current state, but only the Controller(s) can add new, valid events to it. * There may be multiple copies of a KEL; they can be distributed across a network of witnesses, a concept we will dive deeper into later. ## Advanced Control Mechanisms Control in KERI can be quite nuanced including single signature, multiple signature (multisig), and delegation in any given AID. While the Controller ultimately holds authority, they can sometimes grant specific permissions to others through delegation. Furthermore, the Controller responsibility may be shared across multiple controlling parties in a multisig AID. * **Signing vs. Rotation Authority**: A Controller might keep the power to change the AID's keys (rotation authority) but allow another entity (a "custodian") to perform more routine actions like signing messages (signing authority). * **Delegation**: A Controller can grant some level of authority to a completely separate Delegated Identifier. This allows for creating scalable signing infrastructure with delegation hierarchies that can model complex organizational or authority structures. We'll explore these advanced concepts like delegation and multisig configurations in later sections. # Types of Autonomic Identifiers ## Transferable AID A transferable AID may rotate keys and thus may have inception, rotation, and interaction events in its key event log. Most controllers that are not witnesses will use transferable AIDs. Any AID that issues credentials will be a transferable AID. - Example transferable AID: `EIkO4CUmYXukX4auGU9yaFoQaIicfVZkazQ0A3IO5biT` - Notice the 'E' at the start. ## Non-transferable AID A non-transferable AID cannot rotate keys and only ever has one event, the inception event, in its key event log. Use cases for non-transferable AIDs include witnesses, IoT devices, ephemeral identifiers, or anywhere that signing capabilities are needed where rotation capabilities are not. You can visually see the difference between a non-transferable AID and a transferable AID because a non-transferable AID starts with the "B" character as shown here: - `BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha` - Notice the 'B' at the start.
    πŸ“ SUMMARY

    Fundamental KERI concepts:

    In essence, Controllers use their private keys to manage AIDs, and all authoritative actions are recorded in the KEL.

    [<- Prev (Controllers and Identifiers)](101_10_KERI_Command_Line_Interface.ipynb) | [Next (Working with Keystores and AIDs with the KLI) ->](101_20_Working_with_Keystores_and_AIDs_via_KLI.ipynb) # KLI Operations: Managing Keystores and Identifiers
    🎯 OBJECTIVE
    Demonstrate how to create a KERI keystore and then manage identifiers within it using the kli init, kli incept, and kli list commands.
    ## Initializing Keystores Before you can create identifiers or perform many other actions with KLI, you need a keystore. The keystore is an encrypted data store that holds the keys for your identifiers. To initialize a keystore, you give it a name, protect it with a passcode, and provide a salt for generating the keys. The command to do this is `kli init`. Here's an example:
    πŸ’‘ TIP
  • If you run clear_keri(), the keystore directories are deleted.
  • This function is provided as a utility to clean your data and re-run the notebooks.
  • It will be called at the beginning of each notebook.
  • ```python # Imports and Utility functions from scripts.utils import clear_keri clear_keri() ``` Proceeding with deletion of '/usr/local/var/keri/' without confirmation. ⚠️ Path not found: /usr/local/var/keri/. Nothing to remove. ```python # Choose a name for your keystore keystore_name="my-first-key-store" # Use a strong, randomly generated passcode (using a predefined one here, but can be created with 'kli passcode generate') keystore_passcode="xSLg286d4iWiRg2mzGYca" # Use a random salt (using a predefined one here, but can be created with 'kli salt') keystore_salt="0ABeuT2dErMrqFE5Dmrnc2Bq" !kli init --name {keystore_name} \ --passcode {keystore_passcode} \ --salt {keystore_salt} ``` KERI Keystore created at: /usr/local/var/keri/ks/my-first-key-store KERI Database created at: /usr/local/var/keri/db/my-first-key-store KERI Credential Store created at: /usr/local/var/keri/reg/my-first-key-store aeid: BD-1udeJaXFzKbSUFb6nhmndaLlMj-pdlNvNoN562h3z The command sets up the necessary file structures for your keystore, so once executed, it's ready for you to create and manage Identifiers within it. ![](images/empty-keystore.png)
    ℹ️ NOTE
    ## Creating Identifiers (Inception) Now that your keystore is set, you can create your first identifier (AID) within it using the `kli incept` command. You'll need to provide: - `--name` and `--passcode`: Think of it as the keystore access credentials `keystore_name` and `keystore_passcode` - `--alias`: It will be difficult to recall an AID by its value. A human-readable `alias` is assigned using this parameter - `--icount` and `--isith`: the number of signing keys and the signing threshold, respectively. - Other parameters such as `--ncount`, `--nsith`, and `--toad` will be explained later. Executing `kli incept` will create the AID and output the prefix. This also means that the command will add the first event to the AID KEL, the inception event. Proceed and create your first AID: ```python # Choose a human-readable alias for your identifier within this keystore aid_alias = "my-first-aid" # Create (incept) the identifier !kli incept --name {keystore_name} \ --passcode {keystore_passcode} \ --alias {aid_alias} \ --icount 1 \ --isith 1 \ --ncount 0 \ --nsith 0 \ --toad 0 ``` Prefix BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC Public key 1: BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC ![](images/incepted-keystore.png) ## Understanding Prefixes The `kli incept` command generated an AID, which is represented by a unique string, e.g., `BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC`, known as the Prefix. While closely related, they represent different aspects of the identifier: - AID: This is the formal concept of the self-governing identifier, representing the entity and its control. - Prefix: This is the practical, usable string representation of the AID. It's derived directly from the AID's initial cryptographic keys and is constructed by combining: - A Derivation Code: Indicates the cryptographic suite (key type, signature algorithm, hashing algorithm) used. - The Encoded Public Key: The public portion of the initially generated key pair associated with the AID. **Prefix Self-Certification:** KERI AIDs are [self-certifying](https://trustoverip.github.io/tswg-keri-specification/#self-certifying-identifier-scid) in the sense that an AID does not rely on a trusted entity and instead relies only on the keys its identifier is derived from to provide verifiability for statements made (signed) by the controller of an AID. This works because: 1. The identifier's prefix is derived from the set of public keys that are included in the inception event. The prefix is the self addressing identifier (SAID), a kind of digest, of the inception event. This provides a strong cryptographic binding between the AID prefix and the keys used to generate the inception event. 2. The inception event and initial keypairs, together with the key event log and any successive keypairs resulting from rotations, are sufficient to verify any signed statement made by the AID controller. Because of this relationship between keypairs, the inception event, and the key event log, anyone who has the prefix and the KEL can cryptographically verify signatures made by a given AID with the matching private key from any given point in the history of a KEL. This verifiability establishes authenticity for all actions taken by an AID without needing to check with outside authorities or registries, meaning they are self-certifying. ### Security precaution for live transactions **Keep in mind, as a security precaution**, signature verification with a prefix and a KEL is most securely done with the most recent key that is currently authorized for the AID, as in the latest set of keys given the inception and all rotations. Key rotation changes the authorized key, requiring reference to the AID's KEL for up-to-date verification. Historical signatures may still be verified, yet to ensure proper security during a live transaction the latest controlling keypairs should always be used for signature verification. This means signatures from old keypairs, during a live transaction, should always be rejected when verifying signatures of an in-progress transaction. Such an approach is appropriate because there is no way to know if an attacker has compromised old keypairs and is using old keys to sign the new transaction events. To adopt the highest security posture then usage of the latest keypair according to the KEL should **always** be required.
    πŸ“ SUMMARY
  • The AID is the secure, self-managed identifier
  • The prefix is the actual text string you use to represent that AID, whose structure makes the AID's self-certifying property work
  • The alias (my-first-aid in our example) is just a local nickname within your keystore to easily refer to the prefix
  • The terms AID, identifier, prefix, and alias tend to be used interchangeably
  • ℹ️ NOTE
    As you may have figured out, most of the kli commands require a keystore. Assume from now on that --name and --passcode refer to the keystore access.
    ## Displaying Identifier Status You can check the status of the identifier you just created using `kli status` and its `alias`. This command will show details about the AID's current state, including its Alias, prefix, sequence number, public keys, and additional information. More details on what all this data means will be explained later ```python # Check the status of the AID using its alias !kli status --name {keystore_name} \ --passcode {keystore_passcode} \ --alias {aid_alias} ``` Alias: my-first-aid Identifier: BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC Seq No: 0 Witnesses: Count: 0 Receipts: 0 Threshold: 0 Public Keys: 1. BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC ## Displaying Key Event Logs (KELs) You can use `kli status` with the `--verbose` parameter to show the key event log. ```python !kli status --name {keystore_name} \ --passcode {keystore_passcode} \ --alias {aid_alias} \ --verbose ``` Alias: my-first-aid Identifier: BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC Seq No: 0 Witnesses: Count: 0 Receipts: 0 Threshold: 0 Public Keys: 1. BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC Witnesses: { "v": "KERI10JSON0000fd_", "t": "icp", "d": "EG23dnLAUA4ywPcu2qbokplb2cb1XlIOw24iIKYtR3v4", "i": "BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC", "s": "0", "kt": "1", "k": [ "BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC" ], "nt": "0", "n": [], "bt": "0", "b": [], "c": [], "a": [] } Here are some descriptions of the KEL fields (see the [spec](https://trustoverip.github.io/tswg-keri-specification/#keri-data-structures-and-labels)): - `v`: Version String - `t`: Message type (`icp` means inception) - `i`: AID Prefix that created the event ("issuer" of the event) - `s`: sequence number of the event, always zero for the inception event since it is the first event - `kt`: Keys Signing Threshold (the `isith` value used in `kli inception`) - `k`: List of public keys that are Signing Keys (You get as many keys as defined by the `icount` value used in `kli inception`) - `nt`: Next Signing Threshold (rotation signing threshold), zero in this case. This will be explored in an upcoming lesson. - `n`: List of public key **digests** that are rotation keys authorized to perform rotations. Since there are no rotation keys specified here then this identifier may never rotate and may be considered to have rotated to "null" on its first event, meaning it can only ever be used for signing. - `bt`: Backer (witness) Threshold - the number of backer (witness) receipts the event must have in order to be considered accepted by the controller and valid. - `b`: Backer (witness) list - the AID prefixes of the backers (witnesses) that are authorized by the controller to generate witness receipts for this event and any after it, until changed by a rotation event. - `c`: configuration traits - not used here - `a`: anchors (seals) - list of field maps used to anchor data in a key event
    πŸ“š REFERENCE
    To see the full details of the key event fields, refer to KERI Data Structures and Labels
    ## Listing Identifiers in a Keystore You can also list all the identifiers managed within this keystore. To illustrate this, let's create an additional Identifier ```python !kli incept --name {keystore_name} \ --passcode {keystore_passcode} \ --alias "my-second-aid" \ --icount 1 \ --isith 1 \ --ncount 0 \ --nsith 0 \ --toad 0 ``` Prefix BBuVNJvbJD2WNduQ0JUGRVGb6uKYrF5bO5T4gdGt_ezO Public key 1: BBuVNJvbJD2WNduQ0JUGRVGb6uKYrF5bO5T4gdGt_ezO Now use `kli list` to list all the identifiers managed by the keystore ```python # List all Identifiers in the keystore !kli list --name {keystore_name} --passcode {keystore_passcode} ``` my-second-aid (BBuVNJvbJD2WNduQ0JUGRVGb6uKYrF5bO5T4gdGt_ezO) my-first-aid (BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC) ![](images/two-aids.png)
    πŸ“ SUMMARY

    The basics of managing KERI identifiers using the KLI:

    [<- Prev (Controllers and Identifiers)](101_15_Controllers_and_Identifiers.ipynb) | [Next (Signatures) ->](101_25_Signatures.ipynb) # Digital Signatures in KERI
    🎯 OBJECTIVE
    Explain digital signatures, how to verify a digital signature using the KLI verify command, and understand how tampering affects signature validity.
    ## Fundamentals of Digital Signatures Having explored KERI Identifiers (AIDs) and their management, we now focus on digital signatures. This section explains what digital signatures are, their crucial properties, and how they operate within KERI. A digital signature is a cryptographic mechanism used to provide assurance about the authenticity and integrity of digital data. It serves a similar purpose to a handwritten signature but offers significantly stronger guarantees through cryptography. The process generally involves three stages: 1. **Signing:** * The signer (e.g., an AID Controller) takes the information they want to sign. * They create a condensed representation of the information, known as a digest, by using a hash function. * A note on terminology: While "hash" is commonly used to refer to both the function and its output, for clarity in this text, we will use "hash function" to refer to the algorithm itself and "digest" to refer to its output. * Using their unique private signing key, they apply a signing algorithm to the digest generated from the raw data. Signing means encrypting the digest of the raw data with the private key. The result is a digital signature of the digest. * Only someone possessing the private key can generate a valid signature (digest) for that key. 2. **Attaching:** * The generated signature is typically attached to the original information. In the case of KERI this signature is encoded in the [Composable Event Streaming Representation](https://trustoverip.github.io/tswg-cesr-specification/) (CESR) encoding format. 3. **Verification:** * Anyone receiving the information and signature can verify its validity using the signer's corresponding public key. * The verifier applies a verification algorithm using the original information, the signature, and the corresponding public key from the correct point in history of a KEL. * This algorithm is the complement of the signing process. It uses the public key to mathematically check the signature against the digest of the raw information. This means using the public key to decrypt the signature to get back to the original digest. Then the digest from the decrypted signature is compared to the digest of the raw data. If the digests match then the verification succeeds and fails otherwise. * **Outcome:** * **Valid Signature:** If the signature verification succeeds, the verifier has high confidence in the information's authenticity, integrity, and non-repudiability and can trust the data and its originator. * **Invalid Signature:** If the signature fails verification the information may have been tampered with, the signature might be corrupt, or the legitimate holder of the private key didn't generate it. Thus the verifier should not trust the data. Successful verification confirms: * **Authenticity:** The information originated from the owner of the key pair. * **Integrity:** The information has not been altered since it was signed. * **non-repudiability**: The signer cannot successfully deny signing the information. Because generating the signature requires the private key (which should be kept secret by the owner), a valid signature serves as strong evidence of the signer's action. ## Verification Process in KERI In KERI, digital signatures are fundamental for establishing trust and verifying the authenticity of Key Events and other interactions associated with an AID. They cryptographically link actions and data back to the identifier's controlling keys. While the verification algorithm is standard, the key challenge for a Verifier is obtaining the correct public key(s) that were authoritative for the AID when the information was signed. The Verifier must perform these steps: 1. **Identify the Authoritative Public Key(s):** * For an AID's inception event, the AID prefix is derived from the initial public key(s) (leveraging KERI's self-certifying nature). * For subsequent events (like rotations or interactions), the Verifier must consult the AID's Key Event Log to get the most up to date controlling key pair(s). The KEL provides the history of key changes, allowing the Verifier to determine which public key(s) were valid at the specific point in time the event or message was signed. 2. **Perform Cryptographic Verification:** * Once the correct public key(s) are identified, the Verifier uses them, along with the received data and signature, in the standard cryptographic verification algorithm (as described earlier). This reliance on the KEL to track key state over time is crucial for maintaining the security of interactions with KERI identifiers long after their initial creation.
    ℹ️ NOTE
    There's a subtle difference between a Verifier (who checks cryptographic correctness according to KERI rules) and a Validator (who might perform broader checks, including business logic, and broader trust policies in addition to verification). In KERI discussions, "Verifier" typically emphasizes the cryptographic checks.
    ## KLI Examples: Signing and Verifying Let's see how signing and verification work using the KLI commands. ### Initial Setup First, create a keystore and an identifier. ```python # Imports and Utility functions from scripts.utils import clear_keri clear_keri() keystore_name="signature-keystore" passcode="xSLg286d4iWiRg2mzGYca" salt="0ABeuT2dErMrqFE5Dmrnc2Bq" aid_alias = "aid-signature" !kli init --name {keystore_name} \ --passcode {passcode} \ --salt {salt} !kli incept --name {keystore_name} \ --passcode {passcode} \ --alias {aid_alias} \ --icount 1 \ --isith 1 \ --ncount 0 \ --nsith 0 \ --toad 0 ``` 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/signature-keystore KERI Database created at: /usr/local/var/keri/db/signature-keystore KERI Credential Store created at: /usr/local/var/keri/reg/signature-keystore aeid: BD-1udeJaXFzKbSUFb6nhmndaLlMj-pdlNvNoN562h3z Prefix BCtRkWLNdWNRvB8L5gYMaLkanJQWi8wGbmmAtEw9XSWw Public key 1: BCtRkWLNdWNRvB8L5gYMaLkanJQWi8wGbmmAtEw9XSWw ### Signing Data Now, sign a simple text message using the private key associated with the `aid-signature` identifier. To do so use the command `kli sign` presented below: ```python !kli sign --name {keystore_name} \ --passcode {passcode} \ --alias {aid_alias} \ --text "hello world" ``` 1. AABjrlljacVpT8kDsvzv3qCVR1iiwJ-XPaAiKDURCH_vdrkgJgLK4i9h2Qv-xxmT2UxCSif0C-Ovvx-xp2vVDJUB The output is the digital signature generated for the text "hello world" using the private key of the AID. This digital signature is encoded in text format with the CESR encoding protocol, the core cryptographic primitive, text, and binary encoding protocol used in the KERI and ACDC protocols. ### Verifying a Valid Signature You can now use the `kli verify` command to check if the signature is valid for the given message and identifier (prefix). The relevant parameters here are: - `--prefix`: The prefix of the signer - `--text`: original text - `--signature`: signature to verify ```python !kli verify --name {keystore_name} \ --passcode {passcode} \ --alias {aid_alias} \ --prefix BCtRkWLNdWNRvB8L5gYMaLkanJQWi8wGbmmAtEw9XSWw \ --text "hello world" \ --signature AABjrlljacVpT8kDsvzv3qCVR1iiwJ-XPaAiKDURCH_vdrkgJgLK4i9h2Qv-xxmT2UxCSif0C-Ovvx-xp2vVDJUB ``` Signature 1 is valid. The command confirms the signature is valid. It used the public key associated with the prefix to verify the signature against the provided text. ### Impact of Tampering What happens if the signature is altered even slightly? The next command has the last character of the signature modified from "B" to "C" which will cause verification to fail. Try to verify again. ```python !kli verify --name {keystore_name} \ --passcode {passcode} \ --alias {aid_alias} \ --prefix BCtRkWLNdWNRvB8L5gYMaLkanJQWi8wGbmmAtEw9XSWw \ --text "hello world" \ --signature AABjrlljacVpT8kDsvzv3qCVR1iiwJ-XPaAiKDURCH_vdrkgJgLK4i9h2Qv-xxmT2UxCSif0C-Ovvx-xp2vVDJUC # Tampered last character ``` ERR: Signature 1 is invalid. As expected, the verification fails. Even a tiny change invalidates the signature, demonstrating the integrity protection it provides.
    πŸ“ SUMMARY
    [<- Prev (Working with Keystores and AIDs via KLI)](101_20_Working_with_Keystores_and_AIDs_via_KLI.ipynb) | [Next (Rotation) ->](101_30_Key_Rotation.ipynb) # Key Rotation and Pre-rotation
    🎯 OBJECTIVE
    Understand the importance of key rotation, learn about the pre-rotation mechanism, and see how to execute and verify a rotation using KLI commands.
    ## Importance of Key Rotation Key rotation in a scalable identity system while the identifier remains stable is the hard problem from cryptography and distributed systems that KERI solves. The need to rotate keys guided the entire design of KERI and deeply impacted the vLEI system architecture. This is because securing identity and data involves more than just signing data; robust long-term security for an identity and any data it signs relies on key rotation. The ability to rotate keys is a fundamental security practice that involves changing over time the cryptographic keys associated with an identifier. Rotating keys is not just about changing them arbitrarily; it's a crucial practice for several reasons: - **Security Hygiene and Limiting Exposure:** Keys used frequently are more exposed to potential compromise (e.g., residing in memory). Regularly rotating to new keys limits the time window an attacker has if they manage to steal a current key - **Cryptographic Agility:** Cryptographic algorithms evolve. Vulnerabilities are found in older ones, and stronger new ones emerge (like post-quantum algorithms). Key rotation allows an identifier to smoothly transition to updated cryptography without changing the identifier itself - **Recovery and Delegation:** You might need to recover control of an identifier if the current keys are lost or compromised, or delegate authority to another entity. Both scenarios typically involve establishing new keys, which is achieved through rotation events ## Understanding Establishment Events Before diving into key rotation, it's helpful to explain Establishment Events. Not all events recorded in a KEL are the same. Some events specifically define or change the set of cryptographic keys that are authorized to control an identifier (AID) at a particular point in time. These crucial events are called Establishment Events. The two primary types are: Β  - **Inception Event (icp):** The very first event that creates the AID and establishes its initial controlling keys - **Rotation Event (rot):** An event that changes the controlling keys from the set established by the previous Establishment Event to a new set These Establishment Events form the backbone of an AID's security history, allowing anyone to verify which keys had control at what time. Other event types exist (like interaction events), but they rely on the authority defined by the latest Establishment Event. Interaction events rely on the signing authority of the keys referenced in the latest establishment event. ## The Pre-Rotation Mechanism KERI utilizes a strategy called pre-rotation, which decouples the act of key rotation from the preparation for it. With pre-rotation, the cryptographic commitment (a digest of the public keys) for the next key set is embedded within the current key establishment event. This means the next keys can be generated and secured in advance, separate from the currently active operational keys. This pre-commitment acts as a safeguard, as the active private key doesn't grant an attacker the ability to perform the next rotation, as they won't have the corresponding pre-committed private key.
    ℹ️ NOTE
    A potential question arises: "If the next keys are kept in the same place as the active operational keys, doesn't that defeat the purpose?" Pre-rotation enables stronger security by decoupling preparation from rotation, but realizing this benefit depends on sound operational practices. Specifically, the pre-committed keys must be stored more securely than the active ones. KERI provides the mechanism; effective key management brings it to life.
    ## Performing Key Rotation with KLI Next, you will complete a key rotation example. Start by setting up a keystore and an identifier. ```python # Imports and Utility functions from scripts.utils import clear_keri clear_keri() keystore_name="rotation-keystore" keystore_passcode="xSLg286d4iWiRg2mzGYca" salt="0ABeuT2dErMrqFE5Dmrnc2Bq" # Alias for non-transferable AID aid_alias_non_transferable = "aid-non-transferable" # Initialize the keystore !kli init --name {keystore_name} --passcode {keystore_passcode} --salt {salt} # Incept the AID !kli incept --name {keystore_name} \ --passcode {keystore_passcode} \ --alias {aid_alias_non_transferable} \ --icount 1 \ --isith 1 \ --ncount 1 \ --nsith 1 \ --toad 0 ``` 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/rotation-keystore KERI Database created at: /usr/local/var/keri/db/rotation-keystore KERI Credential Store created at: /usr/local/var/keri/reg/rotation-keystore aeid: BD-1udeJaXFzKbSUFb6nhmndaLlMj-pdlNvNoN562h3z Prefix BEG5uWt6xB94bIkdGUCjYcBf_ryDgPa7t1GUtVc7lerw Public key 1: BEG5uWt6xB94bIkdGUCjYcBf_ryDgPa7t1GUtVc7lerw Now, attempt to rotate the keys for this AID, using the command `kli rotate`. You will see an error message ```python !kli rotate --name {keystore_name} --alias {aid_alias_non_transferable} --passcode {keystore_passcode} ``` ERR: Attempt to rotate nontransferable pre=BEG5uWt6xB94bIkdGUCjYcBf_ryDgPa7t1GUtVc7lerw. The error message says we tried to rotate a nontransferable prefix. What does this mean? ### Transferable vs. Non-Transferable AIDs Not all KERI identifiers are designed to have their keys rotated. By default, `kli incept` creates a non-transferable identifier. Here is the difference: **Non-Transferable AID:** - Key rotation is not possible. Think of it as a fixed set of keys for an identifier. - Its control is permanently bound to the initial set of keys established at inception. - The prefix is derived from these initial keys. - As a special case, when only a single key pair was used to create a non-transferable AID the public key is directly derivable from the AID prefix itself. - This is useful for use cases where you want to avoid sending KELs of non-transferable AIDs and instead infer the one-event KEL and associated public key from the AID. **Transferable AID:** - Key rotation is possible. - Its control can be transferred (rotated) to new sets of keys over time. - It uses the pre-rotation mechanism, committing to the next set of keys in each rotation event. - The prefix is derived from the initial keys. Although authoritative keys will change upon each rotation the prefix will remain the same. This allows the identifier to remain stable even as its underlying controlling keys change. How does KERI know the difference? The difference lies in the parameters set during the AID's inception event. Let's look at the inception event data for the non-transferable AID we just created: ```python !kli status --name {keystore_name} --alias {aid_alias_non_transferable} --passcode {keystore_passcode} --verbose ``` Alias: aid-non-transferable Identifier: BEG5uWt6xB94bIkdGUCjYcBf_ryDgPa7t1GUtVc7lerw Seq No: 0 Witnesses: Count: 0 Receipts: 0 Threshold: 0 Public Keys: 1. BEG5uWt6xB94bIkdGUCjYcBf_ryDgPa7t1GUtVc7lerw Witnesses: { "v": "KERI10JSON0000fd_", "t": "icp", "d": "EC8pCWrNEdrLD64K1Z7qlYQp7mp6Dq7n30Ze6ElP49pO", "i": "BEG5uWt6xB94bIkdGUCjYcBf_ryDgPa7t1GUtVc7lerw", "s": "0", "kt": "1", "k": [ "BEG5uWt6xB94bIkdGUCjYcBf_ryDgPa7t1GUtVc7lerw" ], "nt": "0", "n": [], "bt": "0", "b": [], "c": [], "a": [] } Look closely at the JSON output at the end (representing the inception event). You'll find these key fields: - `"nt": "0"`: The threshold required to authorize the next key set is zero. - `"n": []`: The list of digests for the next public keys is empty. These two fields mark the AID as non-transferable. No commitment to future keys was made. ### Incepting and Rotating a Transferable Identifier To enable key rotation, we need to explicitly create a transferable AID using the `--transferable` option during inception and using `--ncount` and `--nsith` equal to 1 (or greater). This tells KLI to: - Generate not just the initial keys, but also the next set of keys (pre-rotated keys). - Set the appropriate nt (Next Key Signing Threshold, defined by `nsith`) in the inception event. - Include the digests of the next public keys in the n field of the inception event. Now create a transferable AID: ```python # Alias for our transferable AID aid_alias_transferable = "aid-transferable" # Create the identifier WITH the --transferable flag !kli incept --name {keystore_name} \ --passcode {keystore_passcode} \ --alias {aid_alias_transferable} \ --icount 1 \ --isith 1 \ --ncount 1 \ --nsith 1 \ --toad 0 \ --transferable ``` Prefix EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM Public key 1: DOdymqdtGJzeoRRSL9C8Suni6ebPaSqQfuEUM_JFkPQx Now, check its status and inception event: ```python !kli status --name {keystore_name} \ --passcode {keystore_passcode} \ --alias {aid_alias_transferable} \ --verbose ``` Alias: aid-transferable Identifier: EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM Seq No: 0 Witnesses: Count: 0 Receipts: 0 Threshold: 0 Public Keys: 1. DOdymqdtGJzeoRRSL9C8Suni6ebPaSqQfuEUM_JFkPQx Witnesses: { "v": "KERI10JSON00012b_", "t": "icp", "d": "EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM", "i": "EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM", "s": "0", "kt": "1", "k": [ "DOdymqdtGJzeoRRSL9C8Suni6ebPaSqQfuEUM_JFkPQx" ], "nt": "1", "n": [ "EO95Pwm8WYG_dIS2-H6LGoXmzOEEnbRljeIjy-Hd7aVx" ], "bt": "0", "b": [], "c": [], "a": [] } Compare the JSON output for this transferable AID's inception event with the previous one. You'll notice key differences: - `"nt": "1"` the next Key Signing Threshold is now 1 - `"n": ["EO95Pwm8WYG_dIS2-H6LGoXmzOEEnbRljeIjy-Hd7aVx"]` The presence of a key digest means that this AID is transferable and has pre-rotated keys ready. ### Performing the Rotation With the commitment to the next keys in place, we can now successfully rotate the key of the transferable AID. ```python !kli rotate --name {keystore_name} \ --passcode {keystore_passcode} \ --alias {aid_alias_transferable} ``` Prefix EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM New Sequence No. 1 Public key 1: DOkM4enfZoc7w8oVdkXzRaVoCdz8f9aAm2u4kA5CHNcQ ### Examining the Rotation (rot) Event The kli rotate command performed the key rotation by creating and signing a new establishment event of type `rot`. Let's examine the state of the AID after the rotation: ```python !kli status --name {keystore_name} \ --passcode {keystore_passcode} \ --alias {aid_alias_transferable} \ --verbose ``` Alias: aid-transferable Identifier: EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM Seq No: 1 Witnesses: Count: 0 Receipts: 0 Threshold: 0 Public Keys: 1. DOkM4enfZoc7w8oVdkXzRaVoCdz8f9aAm2u4kA5CHNcQ Witnesses: { "v": "KERI10JSON00012b_", "t": "icp", "d": "EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM", "i": "EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM", "s": "0", "kt": "1", "k": [ "DOdymqdtGJzeoRRSL9C8Suni6ebPaSqQfuEUM_JFkPQx" ], "nt": "1", "n": [ "EO95Pwm8WYG_dIS2-H6LGoXmzOEEnbRljeIjy-Hd7aVx" ], "bt": "0", "b": [], "c": [], "a": [] } { "v": "KERI10JSON000160_", "t": "rot", "d": "EMZIjwx8mBQpTbKa4q-daoxu0Rv5oX-KR0Q3JbQOJG3Z", "i": "EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM", "s": "1", "p": "EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM", "kt": "1", "k": [ "DOkM4enfZoc7w8oVdkXzRaVoCdz8f9aAm2u4kA5CHNcQ" ], "nt": "1", "n": [ "EJ9DtlVWW6TKPU0AcXBhx3YYDR5FuF9zXqJQqmqJngU8" ], "bt": "0", "br": [], "ba": [], "a": [] } Observe the following changes in the output: - **Event Type (t):** The latest event shows `"t": "rot"`, indicating it's a rotation event. - **Digest said (d):** This is the digest of the event block. - **Sequence Number (s):** The `s` value in the latest event has incremented (from "0" to "1"). Each rotation event increases the sequence number. - **Current Keys (k):** The public key(s) listed in the `k` field of the latest event have changed. They are revealed as public keys instead of the digest previously listed in the `n` field of the inception event. The previously committed pre-rotated keys are now the active signing keys. - **Next Keys Signing Threshold (nt):** Is 1, as defined by the `--nsith` parameter during inception - **New Next Keys (n):** The `n` field in the rotation event contains a new key digest. The rotation process automatically generated the next set of keys for the next potential rotation and committed them. - **Prefix (i):** has not changed. **Understanding the rot Event** - A `rot` event is an Establishment Event. Like the inception (`icp`) event, it defines the authoritative key state of an AID at a specific point in its history (sequence number). - Its primary function is to change the key state. It transitions control from the keys established in the previous establishment event to the keys that were pre-rotated (committed to via the n field) in that previous event. - It simultaneously establishes the commitment (n field and nt threshold) for the next rotation cycle. - This chaining of events (icp -> rot -> rot -> ...) forms the Key Event Log, and the ability to verify this log using receipts from witnesses is a fundamental concept within KERI. You have now successfully rotated the keys for a transferable KERI identifier!
    πŸ“ SUMMARY

    Key rotation is essential for security hygiene, cryptographic agility, and enabling recovery or delegation. KERI uses a "pre-rotation" strategy where the commitment (digest) for the next set of keys is included in the current key establishment event (`icp` or `rot`). This secures the rotation process even if the currently active key is compromised.

    Performing a rotation (kli rotate) creates a rot event, increments the sequence number, activates the previously pre-rotated keys (revealing them in the k field), and commits to a new set of keys (digest in the n field), all while keeping the AID prefix unchanged. This chained process forms part of the Key Event Log (KEL).
    [<- Prev (Signatures)](101_25_Signatures.ipynb) | [Next (Modes, OOBIs, and Witnesses) ->](101_35_Modes_oobis_and_witnesses.ipynb) # KERI Infrastructure: Modes, OOBIs, and Witnesses
    🎯 OBJECTIVE
    Explain KERI's Direct and Indirect modes and the key components enabling Indirect Mode: Out-of-Band Introductions (OOBIs) for discovery, Mailboxes for asynchronous communication, Witnesses for availability and consistency, and the Threshold of Accountable Duplicity (TOAD) for defining signing thresholds.
    ## Operational Modes: Direct and Indirect KERI provides a secure way to manage identifiers and track control using verifiable logs of key events (KEL). How these logs are shared and verified between the controller and someone verifying that identifier depends on one of the two operational modes: Direct and Indirect. ### Direct Mode Direct Mode is a controller-to-controller communication approach, similar to a direct conversation, or like making an HTTP request from a client to a server. In this mode the source controller shares their Key Event Log directly with a destination controller through an HTTP or TCP request. Thedestination controlleracts as a validator by verifying the KEL events and their signatures to ensure integrity. The destination controller can choose to establish trust based solely on verifying the signatures of the source controller on its KEL. This is a lower security posture than relying on a watcher network, yet may be an appropriate choice for a use case. It is also a simple way to start using KERI and allows quick bootstrapping of nodes in a system because validators directly receive and verifies the KEL. This mode is an option for interactions where both parties can connect directly, even if only occasionally, and need to be online to exchange new events or updates.Β 
    🧩 DID YOU KNOW?

    Future Note: Watcher Networks for Direct Mode Verification Thresholds

    While watchers are not yet widely used in the KREI ecosystem landscape, using a watcher network to set a verification threshold is one way to increase the security of a direct mode installation. A watcher or watcher network may be used by the validating controller to compare the KEL being received from the source controller with the view of the KEL that the watcher network has. This is similar to how verifier nodes in distributed consensus systems, like a blockchain, will verify block history with multiple nodes prior to accepting a new block.
    #### Example of Direct Mode The vLEI Reporting API component called [sally](https://github.com/GLEIF-IT/sally) is a direct mode validator component that receives credential presentations in the vLEI ecosystem. It receives KELs, ACDCs (credentials) directly from a presenter, verifies them, and validates them. #### Direct Mode Wrap up Although we haven't done any interaction so far, all the things we have done until this point fit within the direct mode approach. ### Indirect Mode Indirect Mode is the asyncronous approach leveraging mailboxes for communication and witnesses for highly-available KELs, similar to using a public bulletin board instead of direct messaging. It’s for scenarios where the controller may be sometimes offline or needs to serve many validators at once. Rather than relying on direct communication, it introduces infrastructure to both allow a controller to receive messages while offline, the mailbox, and to make the KEL reliably accessible from witnesses. Verifiability extends beyond the controller’s signature to signed event receipts produced by witnesses, called witness receipts. This additional verification capability relies on a network of Witnesses, chosen by the controller, that verify, return signed receits of, and store key events. When combined with the two factor authentication (2FA) capability then witnesses increase the security of an AID. This mode is ideal for public identifiers used from mobile devices and web browsers, one-to-many interactions, or any situation where the controller can’t be constantly online. #### Indirect Mode Wrap Up Most elements of the KERI ecosystem use indirect mode. Unless you know you need direct mode then you should be using indirect mode as your default. ## OOBIs: Discovery Mechanism When an AID controller is operating in either mode, you need a way to tell others where they can find information about it, like its Key Event Log (KEL) or the schema of an ACDC. This is where Out-of-Band Introductions (OOBIs) come in. They function as an address of the way to communicate with a controller or to retrieve a resource. **What is an OOBI?** An OOBI is a **discovery mechanism** used in KERI used to discover controllers or resources. Its primary uses are to link a specific KERI AID to a network location (a URL or URI) where information about that identifier can potentially be found and also to declare the location a resource is hosted such as a JSON Schema document for an ACDC or a CESR stream for a well-known credential. ### Example OOBI The simplest form of an OOBI pairs a SAID, either an AID or the SAID of a document, with a URL. For example: `("http://8.8.5.6:8080/oobi", "EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM", "controller")` This OOBI suggests that controller information related to the AID `EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM` might be available at the service endpoint `http://8.8.5.6:8080/oobi`. The URL representation may be one of any of the following: Blind OOBI (no AID at the end) interpreted as a controller OOBI: - `http://8.8.5.6:8080/oobi` Controller OOBI with no role: - `http://8.8.5.6:8080/oobi/EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM` Controller OOBI with the specific role at the end: - (`http://8.8.5.6:8080/oobi/EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM/controller` ### Kinds of OOBIs There are four similar kinds of OOBIs, controller OOBIs, witness OOBIs, agent OOBIs, and data OOBIs. For controller OOBIs there are three variants, the blind OOBI, the no-role OOBI, and the full OOBI. #### Controller OOBI A controller OOBI is a service endpoint that a controller uses to advertise where its KEL may be retrieved from and where it may receive data. This is typically used by a witness or a direct mode agent. When witnesses are declared in an inception event they will typically have had their controller OOBI resolved Examples: - Blind OOBI: `http://8.8.5.6:8080/oobi` - AID and no role: `http://8.8.5.6:8080/oobi/EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM` - AID and role: `http://10.0.0.1:9823/oobi/ECLwKe5b33BaV20x7HZWYi_KUXgY91S41fRL2uCaf4WQ/controller` #### Witness OOBI A witness OOBI is a service endpoint authorized and used by a controller to designate a witness as a mailbox for a given controller. It means that the witness runs a mailbox that receives messages on behalf of a controller so that the controller may poll for and receive messages when it comes back online. They look like this: - `http://10.0.0.1:5645/oobi/EA69Z5sR2kr-05QmZ7v3VuMq8MdhVupve3caHXbhom0D/witness/BM35JN8XeJSEfpxopjn5jr7tAHCE5749f0OobhMLCorE` This OOBI means that the controller with AID `EA69Z5sR2kr-05QmZ7v3VuMq8MdhVupve3caHXbhom0D` is using the witness with AID `BM35JN8XeJSEfpxopjn5jr7tAHCE5749f0OobhMLCorE` as its mailbox. #### Agent OOBIs An Agent OOBI, used in the KERIA multitenant agent server, is similar to a witness OOBI in that it is a service endpoint authorized and used by a controller to designate an agent as a mailbox for a controller. Where an Agent OOBI differs from a witness OOBI is that an agent OOBI also indicates which specific agent was authorized to act as an agent for a given Signify Controller. It looks like this: - `http://keria2:3902/oobi/ECls3BaUOAtZNO3Ejb4zCv-fybh_hk3iNQMZJVdItr5W/agent/EAueTIcNo9FYqBvtT2QSH-zKFW3TMJGrxEETuIyW2CLF` #### Data OOBIs A data OOBI shows a location to resolve what is typically either a JSON file or a CESR stream, though may be any resource identified by a self-addressing identifier (SAID). Data OOBIs are usually used for ACDC credential schemas, which are JSON files, or CESR streams for well-known ACDC credentials in order to speed up credential verification by hosting common parts of a verification chain in well-known locations. For example, the QVI JSON schema identified with the SAID `EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao` is made available at the following URL on the `10.0.0.1` host. - `http://10.0.0.1:7723/oobi/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao` ### Role of the Service at an OOBI Endpoint What an OOBI means is that a controller has designated and cryptographically authorized a particular service endpoint (web URL) as the location that controller will receive requests at whether for OOBI resolution, key state requests, or for direct CESR stream transmissions. **Key Points** - OOBIs Facilitate Discovery (Out-of-Band): They may use existing internet infrastructure (web servers, QR codes, etc.) to share potential (url, aid) links. This happens outside of KERI's core trust guarantees. - OOBIs Themselves Are NOT Trusted: Receiving an OOBI does not guarantee the URL-AID link is valid or that the data at the URL is legitimate. - Trust Requires KERI Verification (In-Band): After using an OOBI URL to retrieve data (like a Key Event Log), you must use KERI's standard cryptographic verification methods (checking signatures, verifying event history) to establish trust. In short, OOBIs help you find potential information; verification ensures you can trust it. ## Mailboxes Mailboxes are a simple store and forward mechanism where one controller receives messages on behalf of another. As the primary enabler of indirect mode, mailboxes are the always online presence that continues to receive messages for a controller while that controller is offline or unavailable.
    ℹ️ NOTE
    Currently mailboxes are combined with witnesses in the KERIpy implementation of witnesses. When a transferable identifier declares a witness in the inception event then that witness will also be used as a mailbox for the controller to receive messages from other controllers.
    Similarly, KERIA agents also serve as mailboxes for Signify Controllers.
    To receive messages from mailboxes a controller polls all of its witness mailboxes. Polling all of the mailboxes is currently necessary because when messages are sent from a source controller to a destination controller then one witness is selected at random from the list of witnesses that the source controller has for the destination controller. The message is not sent to every mailbox for the destination. Thus, every mailbox must be polled in order to discover new messages.
    ℹ️ NOTE
    When separate mailboxes are completed and supported in the KERIpy reference implementation then a controller may declare and use only one mailbox. This will simplify mailbox management for controllers that use more than one witness as there will be then only one mailbox and it will be deployed separately from the witness.
    ## Role of Witnesses Witnesses are entities designated by the controller within their AID key event log, acting much like trusted notaries. Their role is to receive key events directly from the controller, verify the controller’s signature, and check that each event aligns with the event history they have recorded for that AID. Once a witness confirms an event is valid and encounters it for the first time, it generates a **receipt** by signing the event (Witnesses also have their own AID). The witness then stores both the original event and its receipt, alongside receipts from other witnesses, in a local copy of the KEL known as the **Key Event Receipt Log (KERL)**. Witnesses play a critical role in ensuring the system’s reliability and integrity. They provide availability by forming a distributed service that validators can query to access the KEL of a given prefix, even if the controller itself is unavailable. Additionally, they help ensure consistency: since honest witnesses only sign the first valid version of an event at a given sequence number they observe, it becomes significantly harder for a controller to present conflicting log versions (**duplicity**). It's important to note that witnesses are software components. For the system to improve security and availability, the witness should be deployed independently, ideally operated by different entities, on different infrastructure, from both the controller and each other. ## TOAD: Ensuring Accountability A key challenge in maintaining the integrity of an identifier's history is preventing the controller from presenting conflicting versions of events. This situation, known as **duplicity**, occurs if a controller improperly signs two or more different key events purporting to be at the same sequence number in their Key Event Log (KEL) – for example, signing two different rotation events both claiming to be sequence number 3. Such conflicting statements undermine trust in the identifier's true state and control. Reasons for duplicity may be due to malicious intent or operational errors. KERI addresses this partly through the behavior of witnesses, which only sign the first valid event they see per sequence number, and partially through watchers which keep a duplicate copy of a KEL for a given controller so they may detect when a malicious controller tries to change history by changing a key event at a given sequence number that has already occurred. KERI assigns *accountability* for an event, and thus any potential duplicity (change of history), based on a signing threshold of witnesses for a given event, called the **Threshold of Accountable Duplicity (TOAD)**. This signing threshold quantifies the level of agreement needed to assign accountability to a controller for a given event, and thus any potential duplicity. The TOAD is specified in the inception event for an AID and can be changed in each rotation event. We have seen this parameter before when calling `kli incept`. The `toad` value represents the minimum number of unique witness receipts the controller considers sufficient to accept accountability for a key event. By gathering receipts that meet or exceed this controller-defined threshold (`toad`), validators gain assurance that the event history they are watching is the one the controller stands behind and is broadly agreed upon by the witness network. Crucially, while the `toad` defines the controller's threshold for their accountability, a validator may independently establish its own, often higher, threshold watchers that must agree on the history of a KEL to accept an event as fully validated according to its trust policy. These two threshold mechanisms, the TOAD for a signing threshold and a watcher threshold, allowing for distinct controller accountability and validator trust levels, are key to KERI's robust security model and fault tolerance, helping distinguish between minor issues and significant, actionable inconsistencies.
    πŸ“ SUMMARY
    KERI provides two operational modes for sharing Key Event Logs (KELs). Key Infrastructure for Indirect Mode: Accountability and Trust:
    [<- Prev (Key Rotation)](101_30_Key_Rotation.ipynb) | [Next (Witnesses) ->](101_40_Witnesses.ipynb) # KLI Operations: Configuring AID Witnesses
    🎯 OBJECTIVE
    Demonstrate how to configure witnesses and the Threshold of Accountable Duplicity (TOAD) in a configuration file and use it to create an AID
    ## Verifying the Demo Witness Network Now that you understand Witnesses and oobis, let's see some practical usage. Within the deployment of these notebooks, we have included a demo witness network. It is composed of three witnesses: - `http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha` - `http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM` - `http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX` (These witnesses are predefined (**[wan.json](config/witness-demo-docker/wan.json), [wes.json](config/witness-demo-docker/wes.json), [wil.json](config/witness-demo-docker/wil.json)**); that's why we know the prefixes beforehand) To verify the witness network is working, let's query the KEL of one of them using its oobi and `curl`.
    ℹ️ NOTE
    You can include request parameters on the end of an OOBI and when it is resolved they will be added as contact information for the controller whose OOBI is being resolved, like the ?name=Wan&tag=witness section in the OOBI.
    "http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness"
    This will add the following properties to the contact data for the OOBI: This is a useful technique for enriching a contact in your contact database with human-friendly attributes.
    ```python !curl -s http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha ``` {"v":"KERI10JSON0000fd_","t":"icp","d":"EIkO4CUmYXukX4auGU9yaFoQaIicfVZkazQ0A3IO5biT","i":"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","s":"0","kt":"1","k":["BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha"],"nt":"0","n":[],"bt":"0","b":[],"c":[],"a":[]}-VAn-AABAAAMlb78gUo1_gPDwxbXyERk2sW8B0mtiNuACutAygnY78PHYUjbPj1fSY1jyid8fl4-TXgLXPnDmeoUs1UO-H0A-EAB0AAAAAAAAAAAAAAAAAAAAAAA1AAG2025-07-18T00c07c10d306680p00c00{"v":"KERI10JSON0000fd_","t":"rpy","d":"EHkrUtl8Nt7nZjJ8mApuG80us9E_td3oa4V-oW2clB2K","dt":"2024-12-31T14:06:30.123456+00:00","r":"/loc/scheme","a":{"eid":"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","scheme":"http","url":"http://witness-demo:5642/"}}-VAi-CABBBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha0BDkVOk5p25Rhim3LkhYXmDNNiUcZkgCp_BWvEB45q6f_pKJBYYlpUABpci5DMzBNXlz4RvK8ImKVc_cH-0D8Q8D{"v":"KERI10JSON0000fb_","t":"rpy","d":"EDSjg0HilC3L4I_eI53C3_6lW9I6pPbR4SWGgoOmDhMb","dt":"2024-12-31T14:06:30.123456+00:00","r":"/loc/scheme","a":{"eid":"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","scheme":"tcp","url":"tcp://witness-demo:5632/"}}-VAi-CABBBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha0BDLG3-HNp-kclaNamqIRI46rNcAhpIEQBDON2HO28r9zO-6S53_w7AA_Q4Weg4eAjvTGiXiNExhO86elrIEd74F{"v":"KERI10JSON000116_","t":"rpy","d":"EBBDzl8D5gFgFkVXaB-XNQlCem-4y5JywPcueWAMRfCp","dt":"2024-12-31T14:06:30.123456+00:00","r":"/end/role/add","a":{"cid":"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","role":"controller","eid":"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha"}}-VAi-CABBBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha0BDt7alD1tA9x_9vVMKxY1Ne113qJ-xDdCyThnAh6_c13Rsrb9WW5HlKyQxyW5DVXWJjQ65yzME5kCLBiJWYBKEL The command should have returned a KEL; you should be able to recognize part of it. It starts with `{"v": "KERI10JSON0000fd_", "t": "icp"...`. If so, the witness network is up and running. You will see that the response contains JSON and a cryptic text format that looks like `-VAn-AABAAAMl`. This is a CESR string, something we will get into in a later training. ## Keystore Initialization with Witness Configuration Let's continue with the example. As usual, we need to create a keystore, but this time we are going to do something different. We are going to use a configuration file to provide the OOBIs of the witnesses to the keystore. The content of the configuration file can be seen here: **[Keystore configuration file](config/keri/cf/keystore_init_config.json)** ```python # Imports and Utility functions from scripts.utils import clear_keri clear_keri() keystore_name="tests-keystore" keystore_passcode="xSLg286d4iWiRg2mzGYca" salt="0ABeuT2dErMrqFE5Dmrnc2Bq" !kli init --name {keystore_name} --passcode {keystore_passcode} --salt {salt} \ --config-dir ./config \ --config-file keystore_init_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/tests-keystore KERI Database created at: /usr/local/var/keri/db/tests-keystore KERI Credential Store created at: /usr/local/var/keri/reg/tests-keystore aeid: BD-1udeJaXFzKbSUFb6nhmndaLlMj-pdlNvNoN562h3z 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 ## Listing keystore contacts As you can see, the initialization has loaded the oobis. You can also check the loaded witness information by calling the `kli contact list` command ```python !kli contacts list --name {keystore_name} --passcode {keystore_passcode} ``` { "id": "BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha", "alias": "Wan", "oobi": "http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness", "challenges": [], "wellKnowns": [] } { "id": "BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX", "alias": "Wil", "oobi": "http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wil&tag=witness", "challenges": [], "wellKnowns": [] } { "id": "BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM", "alias": "Wes", "oobi": "http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wes&tag=witness", "challenges": [], "wellKnowns": [] } ## Incepting an AID with Witness Configuration Next, you can incept a new AID. Use a configuration file again. The content of the configuration file (**[aid configuration file](config/aid_inception_config.json)**) can be seen here: ```json { "transferable": true, "wits": ["BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM"], "toad": 1, "icount": 1, "ncount": 1, "isith": "1", "nsith": "1" } ``` Notable highlights in this configuration are the inclusion of the witnesses' prefixes and the `toad` set to 1 Here is the `incept` command: ```python aid_alias_transferable = "aid-transferable" !kli incept --name {keystore_name} --alias {aid_alias_transferable} --passcode {keystore_passcode} \ --file ./config/aid_inception_config.json ``` Waiting for witness receipts... Prefix EJq-DYl9EQVlY1lShETUWLQuDEcVdRkWXfkkGBNDugjZ Public key 1: DOdymqdtGJzeoRRSL9C8Suni6ebPaSqQfuEUM_JFkPQx Check the status of the AID using `kli status` ```python !kli status --name {keystore_name} --alias {aid_alias_transferable} --passcode {keystore_passcode} --verbose ``` Alias: aid-transferable Identifier: EJq-DYl9EQVlY1lShETUWLQuDEcVdRkWXfkkGBNDugjZ Seq No: 0 Witnesses: Count: 2 Receipts: 2 Threshold: 1 Public Keys: 1. DOdymqdtGJzeoRRSL9C8Suni6ebPaSqQfuEUM_JFkPQx Witnesses: 1. BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha 2. BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM { "v": "KERI10JSON000188_", "t": "icp", "d": "EJq-DYl9EQVlY1lShETUWLQuDEcVdRkWXfkkGBNDugjZ", "i": "EJq-DYl9EQVlY1lShETUWLQuDEcVdRkWXfkkGBNDugjZ", "s": "0", "kt": "1", "k": [ "DOdymqdtGJzeoRRSL9C8Suni6ebPaSqQfuEUM_JFkPQx" ], "nt": "1", "n": [ "EO95Pwm8WYG_dIS2-H6LGoXmzOEEnbRljeIjy-Hd7aVx" ], "bt": "1", "b": [ "BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha", "BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM" ], "c": [], "a": [] } In this status, you will find a few new things: - The witnesses section has a count of 2 and mentions two receipts and a threshold of 1 - The KEL has the `b` field populated with the witnesses' prefixes - The `bt` threshold is set to 1 (toad) Guidance for setting the proper threshold, "toad," for the number of witnesses may be found in the KERI Algorithm for Witness Agreement ([KAWA](https://trustoverip.github.io/tswg-keri-specification/#keris-algorithm-for-witness-agreement-kawa)). Generally this means having roughly between 2/3 and 3/4 of witness nodes available, though the equation and table shown below give a precise definition of how to calculate TOAD. In the below chart N = the number of witnessess and M = the number that TOAD should be set to in order to have a strong guarantee of witness agreement even in the face of faulty witnesses. ![image.png](101_40_Witnesses_files/f6dc13ce-4878-4e73-9ca3-ed81773283a7.png)
    πŸ“ SUMMARY
    We used the demo witness network to provide receipts for the inception event of an identifier.
    Witnesses are specified during AID inception using a configuration file by listing their AID prefixes in the wits property of the inception configuration and by setting the Threshold of Accountable Duplicity toad property to a non-zero number. The kli incept command utilizes this file to create the AID, embedding the witness information into the inception event. Checking the AID status with kli status ... --verbose reveals the witness prefixes in the b field, the TOAD in the bt field, and any received witness receipts.
    The table listed above may be used as guidance for setting the TOAD to ensure a strong guarantee of witness agreement.
    # KLI Operations: Connecting Controllers
    🎯 OBJECTIVE
    Explain how to establish a secure, mutually authenticated connection between two KERI controllers using Out-of-Band Introductions (OOBIs) and challenge/response protocol to enhance trust.
    ## Initial Controller Setup So far, we have only done basic operations with AIDs in an isolated way. That has limited use in practical applications; after all, establishing identity verification only becomes meaningful when interacting with others. In KERI, this interaction starts with controllers needing to discover and securely connect with each other. In our context, this means we need to establish connections between controllers. We've already seen a similar process when pairing transferable AIDs with witnesses. Now, let's explore how two controllers (a and b) can connect using Out-of-Band Introductions (OOBIs) and enhance trust with **challenge/response**. ### Keystore Initialization For the example, you need to use two different keystores called `keystore-a` and `keystore-b`, both initialized using the `keystore_init_config.json` configuration. This means they will both load the same initial set of three witness contacts, providing witness endpoints where each controller's KEL (and thus key state) can be published and retrieved when identifiers are created using inception later. ```python # Imports and Utility functions from scripts.utils import clear_keri clear_keri() keystore_a_name="keystore_a" keystore_a_passcode="xSLg286d4iWiRg2mzGYca" salt_a="0ABeuT2dErMrqFE5Dmrnc2Bq" !kli init --name {keystore_a_name} --passcode {keystore_a_passcode} --salt {salt_a} \ --config-dir ./config \ --config-file keystore_init_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/keystore_a KERI Database created at: /usr/local/var/keri/db/keystore_a KERI Credential Store created at: /usr/local/var/keri/reg/keystore_a aeid: BD-1udeJaXFzKbSUFb6nhmndaLlMj-pdlNvNoN562h3z 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 ```python keystore_b_name="keystore_b" keystore_b_passcode="LLF1NYii5L7jTMvw4gDar" salt_b="0ADzG7sbUyw-MYIoUyQe5wxB" !kli init --name {keystore_b_name} --passcode {keystore_b_passcode} --salt {salt_b} \ --config-dir ./config \ --config-file keystore_init_config.json ``` KERI Keystore created at: /usr/local/var/keri/ks/keystore_b KERI Database created at: /usr/local/var/keri/db/keystore_b KERI Credential Store created at: /usr/local/var/keri/reg/keystore_b aeid: BPJYwdaLcdcbB6pTpRal-IhbV_Vb8bD6vq_qiMFojHNG 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 ### Identifier Inception Now, you need AIDs to represent the controllers. Create one transferable AID in each keystore, aliased `aid_a` and `aid_b` respectively. Use the aid_inception_config.json file, which specifies the initial set of witnesses for both AIDs. (While they share witnesses here, controllers could use different witness sets). ```python aid_a = "aid_a" !kli incept --name {keystore_a_name} \ --passcode {keystore_a_passcode} \ --alias {aid_a} \ --file ./config/aid_inception_config.json ``` Waiting for witness receipts... Prefix EML-Hx1ivj6CSkPTM80xCqFmabG9l9ZrVxPe9omW2cWl Public key 1: DDiMxDbmRMjC0mDSkzlwEbYveGozxRXXIsFUo3ixQaU4 ```python aid_b = "aid_b" !kli incept --name {keystore_b_name} \ --passcode {keystore_b_passcode} \ --alias {aid_b} \ --file ./config/aid_inception_config.json ``` Waiting for witness receipts... Prefix EAJR7SlFds3hQpH8kj8HySFRdhW6DcC7m9KdELNJIUma Public key 1: DHEa1ktRvZUjdRitkgJ5u3tNjitiw9Ba0cgz-fMhTS4c ## OOBI Exchange for Discovery With your AIDs established, you need a way for them to find each other. Remember, each witness, in the current implementation, uses each of its witnesses both as a KEL publication mechanis and as a mailbox to receive messages on behalf of the controller. To tell other controllers where to find this witness mailbox the local controller must provide a way to connect to the witness and the mailbox. This is where Out-of-Band Introductions (OOBIs) come in. You have used OOBIs before; to recapitulate, an OOBI is a specialized URL associated with an AID and how to reach one of its endpoints (like a witness or mailbox). ### Generating OOBI URLs Use the `kli oobi generate` command to create OOBIs for your AIDs. Specify which AID (`--alias`) within which keystore (`--name`) should generate the OOBI, and importantly, the role associated with the endpoint included in the OOBI URL. Here, `--role witness` means the OOBI URL will point to one of the AID's designated witnesses, providing an indirect way to fetch the AID's KEL. This role also, as of the current implementation, also includes the witness acting as a mailbox. There is a separate `--role mailbox` that may be used yet is not covered in this particular training. Use `--role witness` for now. You will see a separate OOBI generated for each witness. ```python !kli oobi generate --name {keystore_a_name} \ --passcode {keystore_a_passcode} \ --alias {aid_a} \ --role witness ``` http://witness-demo:5642/oobi/EML-Hx1ivj6CSkPTM80xCqFmabG9l9ZrVxPe9omW2cWl/witness http://witness-demo:5643/oobi/EML-Hx1ivj6CSkPTM80xCqFmabG9l9ZrVxPe9omW2cWl/witness ```python !kli oobi generate --name {keystore_b_name} \ --passcode {keystore_b_passcode} \ --alias {aid_b} \ --role witness ``` http://witness-demo:5642/oobi/EAJR7SlFds3hQpH8kj8HySFRdhW6DcC7m9KdELNJIUma/witness http://witness-demo:5643/oobi/EAJR7SlFds3hQpH8kj8HySFRdhW6DcC7m9KdELNJIUma/witness Note that the command returns multiple OOBIs, one for each witness endpoint configured for the AID. Any of these can be used to initiate contact. For simplicity, we'll capture the first OOBI URL generated for each AID into the variables `oobi_a` and `oobi_b`. ```python # Imports and Utility functions from scripts.utils import exec command_a = f"kli oobi generate --name {keystore_a_name} --alias {aid_a} --passcode {keystore_a_passcode} --role witness" oobi_a = exec(command_a) print(f"OOBI A: {oobi_a}") command_b = f"kli oobi generate --name {keystore_b_name} --alias {aid_b} --passcode {keystore_b_passcode} --role witness" oobi_b = exec(command_b) print(f"OOBI B: {oobi_b}") ``` OOBI A: http://witness-demo:5642/oobi/EML-Hx1ivj6CSkPTM80xCqFmabG9l9ZrVxPe9omW2cWl/witness OOBI B: http://witness-demo:5642/oobi/EAJR7SlFds3hQpH8kj8HySFRdhW6DcC7m9KdELNJIUma/witness ### Resolving OOBI URLs Now that `aid_a` and `aid_b` each have an OOBI, they need to resolve them. The `kli oobi resolve` command handles this. What happens when an OOBI is resolved? That depends on the type of OOBI. An OOBI resolution for HTTP OOBIs performs an HTTP GET request on the URL. Resolving controller or witness OOBIs returns the key event log for the AID specified in the OOBI URL. For example, when `keystore_a` resolves `oobi_b`, its uses the URL to contact the specified witness. The witness provides the KEL for `aid_b`. `keystore_a` then verifies the entire KEL cryptographically, ensuring its integrity and confirming the public keys associated with `aid_b`. A human-readable alias `--oobi-alias` is assigned for easy reference later. The same process happens when `keystore_b` resolves `oobi_a`. ```python !kli oobi resolve --name {keystore_a_name} \ --passcode {keystore_a_passcode} \ --oobi-alias {aid_b} \ --oobi {oobi_b} ``` http://witness-demo:5642/oobi/EAJR7SlFds3hQpH8kj8HySFRdhW6DcC7m9KdELNJIUma/witness resolved ```python !kli oobi resolve --name {keystore_b_name} \ --passcode {keystore_b_passcode} \ --oobi-alias {aid_a} \ --oobi {oobi_a} ``` http://witness-demo:5642/oobi/EML-Hx1ivj6CSkPTM80xCqFmabG9l9ZrVxPe9omW2cWl/witness resolved ### Listing contacts After successful resolution, the other AID appears in the keystore's contact list. You can verify this using `kli contacts list`. You'll see the newly resolved AID alongside the witnesses loaded during the keystore initialization. This confirms that the keystore now knows the other AID's identifier prefix and has verified its KEL. ```python !kli contacts list --name {keystore_a_name} \ --passcode {keystore_a_passcode} ``` { "id": "BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha", "alias": "Wan", "oobi": "http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness", "challenges": [], "wellKnowns": [] } { "id": "BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX", "alias": "Wil", "oobi": "http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wil&tag=witness", "challenges": [], "wellKnowns": [] } { "id": "BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM", "alias": "Wes", "oobi": "http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wes&tag=witness", "challenges": [], "wellKnowns": [] } { "id": "EAJR7SlFds3hQpH8kj8HySFRdhW6DcC7m9KdELNJIUma", "alias": "aid_b", "oobi": "http://witness-demo:5642/oobi/EAJR7SlFds3hQpH8kj8HySFRdhW6DcC7m9KdELNJIUma/witness", "challenges": [], "wellKnowns": [] } ## Authenticating Control with Challenge-Response Resolving an OOBI and verifying the KEL is a crucial first step. It confirms that the AID exists and that its key state history is cryptographically sound. However, it doesn't definitively prove that the entity you just connected with over the network is the legitimate controller you intend to interact with. You've verified the identifier, but not necessarily the authenticity of the current operator at the other end of the connection. Network connections can be vulnerable to Man-in-the-Middle (MITM) attacks or other deceptions. This is where the challenge-response mechanism becomes essential. It provides a way to verify that the controller on the other side genuinely possesses the private keys corresponding to the public keys in the KEL you just verified. This adds a critical layer of authentication on top of the OOBI discovery process. This is how it works: One party (the challenger, say `aid_b`) generates a random challenge phrase. The challenger sends this phrase to the other party (`aid_a`) through an Out-of-Band (OOB) channel. This means using a communication method different from the KERI network connection (e.g., a video call chat, phone call, secure email) to prevent an attacker on the KERI network channel from intercepting or modifying the challenge. Using the same channel for both the challenge words and the response defeats the purpose of protecting against MITM attacks because MITM attacks occur "in-band" on a given channel so you must use a separate, "out-of-band" communication channel, such as a video chat, to exchange the challenge phrase. The challenged party (`aid_a`) receives the phrase and uses their current private key to sign it. `aid_a` then sends the original phrase and the resulting signature back to `aid_b` over the KERI connection, typically using general internet infrastructure. Next, `aid_b` verifies two things: - that the returned phrase matches the one originally sent, and - that the signature correctly verifies against the current signing public key associated with `aid_a` in its verified KEL. If the verification succeeds, `aid_b` now has strong assurance that they are communicating with the entity that truly controls `aid_a`'s private keys. This process is typically done mutually, with `aid_a` also challenging `aid_b` to gain strong confidence in the controller of `aid_b`'s keys. You can generate the challenge phrases using `kli challenge generate`. The code below will store them in variables for later use in the commands. ```python print("Example challenge phrase:") !kli challenge generate --out string print("\nChallenge phrases A and B:\n") phrase_a = exec("kli challenge generate --out string") print(f"Challenge Phrase A: {phrase_a}") phrase_b = exec("kli challenge generate --out string") print(f"Challenge Phrase B: {phrase_b}") ``` Example challenge phrase: robot match tomato increase similar resist swap opinion lounge walnut strategy glove Challenge phrases A and B: Challenge Phrase A: banner worth air salad snow topic fresh feed razor decorate pair innocent Challenge Phrase B: eagle truly sail depth cover faint essay hybrid identify link purity refuse Now, simulate the OOB exchange: `aid_b` sends `phrase_b` to `aid_a`, and `aid_a` sends `phrase_a` to `aid_b`. Each party then uses `kli challenge respond` to sign the phrase they received and `kli challenge verify` to check the response from the other party. ```python print(phrase_a) !kli challenge respond --name {keystore_b_name} \ --passcode {keystore_b_passcode} \ --alias {aid_b} \ --words "{phrase_a}" \ --recipient {aid_a} ``` banner worth air salad snow topic fresh feed razor decorate pair innocent ```python !kli challenge verify --name {keystore_a_name} \ --passcode {keystore_a_passcode} \ --alias {aid_a} \ --words "{phrase_a}" \ --signer {aid_b} ``` Checking mailboxes for any challenge responses. . Signer aid_b successfully responded to challenge words: '['banner', 'worth', 'air', 'salad', 'snow', 'topic', 'fresh', 'feed', 'razor', 'decorate', 'pair', 'innocent']' ```python print(phrase_b) !kli challenge respond --name {keystore_a_name} \ --passcode {keystore_a_passcode} \ --alias {aid_a} \ --words "{phrase_b}" \ --recipient {aid_b} ``` eagle truly sail depth cover faint essay hybrid identify link purity refuse ```python !kli challenge verify --name {keystore_b_name} \ --passcode {keystore_b_passcode} \ --alias {aid_b} \ --words "{phrase_b}" \ --signer {aid_a} ``` Checking mailboxes for any challenge responses. . Signer aid_a successfully responded to challenge words: '['eagle', 'truly', 'sail', 'depth', 'cover', 'faint', 'essay', 'hybrid', 'identify', 'link', 'purity', 'refuse']' Successful verification on both sides mutually establishes cryptographically strong authenticated control of the identifiers on both sides of the interaction. This significantly increases the trust level between the two controllers far beyond the verifiability granted by sharing key histories (KELs) during the initial connection through mutual OOBI resolution. After the challenge response and verification process each party knows they are interacting with the legitimate key holders for each respective AID.
    πŸ“ SUMMARY
    After initial discovery (often via OOBIs), KERI controllers can enhance trust by verifying active control of private keys using a challenge-response protocol. This involves each controller generating a unique challenge phrase (kli challenge generate). One controller (aid_a) then responds to the other's challenge (phrase_b) by signing it (kli challenge respond), and the second controller (aid_b) verifies this response (kli challenge verify). This process is repeated reciprocally. Successful verification by both parties confirms they are interacting with the legitimate key holders for each AID.
    # KLI Operations: Creating and Managing Delegated AIDs
    🎯 OBJECTIVE
    Understand the concept of delegated AIDs, where one Autonomic Identifier (AID), the delegator, grants specific authority to another AID, the delegate. This is an illustration of [cooperative delegation](https://trustoverip.github.io/tswg-keri-specification/#cooperative-delegation), meaning both parties work together to form a strongly cryptographically bound delegation relationship between the delegator and the delegate. This notebook demonstrates how to create and manage delegated AIDs using the KERI Command Line Interface (KLI), covering: 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): BQ_y56Nf59ID_b7pzb796
    ℹ️ 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: EA2GfmC5zd3xGSaQ7YKlSOPdJW_xbJPwwKde1skuqJK4 Client AID Keys: [ "DDFnKCWyYVvzbzkQMZdV1tUk9lUgFgIBSN0m6ldc8Sem" ] Client AID Next Keys Digest: [ "EB8oU5swFmt9h7G0G8xZkoDE49CqkFndRJ3S_-DakH8v" ] Agent AID Prefix: EFSlxW9lxUHXMPBSFmNeMrVvR6z-HcPxyqeR65ePsd2W Agent AID Type: dip Agent AID Delegator: EA2GfmC5zd3xGSaQ7YKlSOPdJW_xbJPwwKde1skuqJK4 **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: EA2GfmC5zd3xGSaQ7YKlSOPdJW_xbJPwwKde1skuqJK4 Agent AID Prefix: EFSlxW9lxUHXMPBSFmNeMrVvR6z-HcPxyqeR65ePsd2W Agent AID Delegator: EA2GfmC5zd3xGSaQ7YKlSOPdJW_xbJPwwKde1skuqJK4
    πŸ“ SUMMARY
    To connect to a KERIA agent using SignifyTS:
    1. Initialize the library with await ready().
    2. 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.
    3. 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.
    4. 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.
    5. 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.EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", metadata: { pre: "EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", 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.EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", metadata: { pre: "EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", sn: 0 }, done: true, error: null, response: { v: "KERI10JSON0001b7_", t: "icp", d: "EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", i: "EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", s: "0", kt: "1", k: [ "DBZMCILo9tIeKrLu7eT6yJ3m1wAmcj39zIIIJ5_N6F1q" ], nt: "1", n: [ "EGT65gwP7FXPirO43m42U2UYkAqeOR2HVzOX0UN8YImN" ], bt: "2", b: [ "BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha", "BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM", "BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX" ], c: [], a: [] } } Successfully created AID with prefix: EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy Witnesses specified: ["BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM","BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX"] Icp op name: witness.EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy Inception operation { name: "witness.EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", metadata: { pre: "EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", sn: 0 }, done: true, error: null, response: { v: "KERI10JSON0001b7_", t: "icp", d: "EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", i: "EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", s: "0", kt: "1", k: [ "DBZMCILo9tIeKrLu7eT6yJ3m1wAmcj39zIIIJ5_N6F1q" ], nt: "1", n: [ "EGT65gwP7FXPirO43m42U2UYkAqeOR2HVzOX0UN8YImN" ], 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.EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", "metadata": { "pre": "EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", "sn": 0 }, "done": true, "error": null, "response": { "v": "KERI10JSON0001b7_", "t": "icp", "d": "EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", "i": "EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", "s": "0", "kt": "1", "k": [ "DBZMCILo9tIeKrLu7eT6yJ3m1wAmcj39zIIIJ5_N6F1q" ], "nt": "1", "n": [ "EGT65gwP7FXPirO43m42U2UYkAqeOR2HVzOX0UN8YImN" ], "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.EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy Inception operation { name: "witness.EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", metadata: { pre: "EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", sn: 0 }, done: true, error: null, response: { v: "KERI10JSON0001b7_", t: "icp", d: "EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", i: "EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", s: "0", kt: "1", k: [ "DBZMCILo9tIeKrLu7eT6yJ3m1wAmcj39zIIIJ5_N6F1q" ], nt: "1", n: [ "EGT65gwP7FXPirO43m42U2UYkAqeOR2HVzOX0UN8YImN" ], 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.EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", metadata: { pre: "EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy", 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.EDwkavjyiOOSpOzHVyZZTtBNyI88EjdXTW_i1uaNC4vy 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.ELbfbbLMcKinMyOrQMDm8kDBaxfqHRhruA1ZF7EZS4hF", "metadata": { "pre": "ELbfbbLMcKinMyOrQMDm8kDBaxfqHRhruA1ZF7EZS4hF", "sn": 0 }, "done": true, "error": null, "response": { "v": "KERI10JSON000159_", "t": "icp", "d": "ELbfbbLMcKinMyOrQMDm8kDBaxfqHRhruA1ZF7EZS4hF", "i": "ELbfbbLMcKinMyOrQMDm8kDBaxfqHRhruA1ZF7EZS4hF", "s": "0", "kt": "1", "k": [ "DFSyQ-vnJVrCk57iBZQbepk8wwKpyxjkP5Z1p6yvdKRi" ], "nt": "1", "n": [ "EEj31OxmloZi8OL0PLPK20m_9I5yy7VGk5CCOV-aIuIk" ], "bt": "1", "b": [ "BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha" ], "c": [], "a": [] } } ]
    πŸ“ SUMMARY
    To create a new AID using Signify-ts and a KERIA agent:
    1. 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).
    2. 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.
    3. 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.
    4. Operations can be listed with client.operations().list() and deleted with client.operations().delete(operationName).
    5. 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.
    ```typescript ``` # 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: EABWImOm9hIeZT2STeaobbcZw7yt3ITw5uEEDsiwXPLA Client B AID Pre: EIGa_qsf1GLVT2NXnXteLCN_-B0DKmGaV39p57eNCAOQ
    ℹ️ 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 EPz83YNKcZ4yW9NcVvqAPGu5MVqf8K1NcZRMhGJOH5EU for AID EABWImOm9hIeZT2STeaobbcZw7yt3ITw5uEEDsiwXPLA Client B: Assigned 'agent' role to KERIA Agent EFlcYHTOLZBoBETrCAE-xljLag8PgDCZTDtJuzo6_myU for AID EIGa_qsf1GLVT2NXnXteLCN_-B0DKmGaV39p57eNCAOQ ## 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/EABWImOm9hIeZT2STeaobbcZw7yt3ITw5uEEDsiwXPLA/agent/EPz83YNKcZ4yW9NcVvqAPGu5MVqf8K1NcZRMhGJOH5EU Client B (Betty) generated OOBI for aidB: http://keria:3902/oobi/EIGa_qsf1GLVT2NXnXteLCN_-B0DKmGaV39p57eNCAOQ/agent/EFlcYHTOLZBoBETrCAE-xljLag8PgDCZTDtJuzo6_myU ### 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/EIGa_qsf1GLVT2NXnXteLCN_-B0DKmGaV39p57eNCAOQ/agent/EFlcYHTOLZBoBETrCAE-xljLag8PgDCZTDtJuzo6_myU", id: "EIGa_qsf1GLVT2NXnXteLCN_-B0DKmGaV39p57eNCAOQ", ends: { agent: { "EFlcYHTOLZBoBETrCAE-xljLag8PgDCZTDtJuzo6_myU": { 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/EABWImOm9hIeZT2STeaobbcZw7yt3ITw5uEEDsiwXPLA/agent/EPz83YNKcZ4yW9NcVvqAPGu5MVqf8K1NcZRMhGJOH5EU", id: "EABWImOm9hIeZT2STeaobbcZw7yt3ITw5uEEDsiwXPLA", ends: { agent: { EPz83YNKcZ4yW9NcVvqAPGu5MVqf8K1NcZRMhGJOH5EU: { 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: [ "gaze", "clever", "install", "jump", "captain", "piano", "dignity", "whale", "elephant", "endorse", "copper", "trumpet" ] Client B's challenge words for Alfred: [ "six", "foil", "pill", "art", "cinnamon", "indoor", "warrior", "slim", "awful", "rather", "knife", "advance" ] ### 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: EIGa_qsf1GLVT2NXnXteLCN_-B0DKmGaV39p57eNCAOQ) responding to Alfred's (aidA: EABWImOm9hIeZT2STeaobbcZw7yt3ITw5uEEDsiwXPLA) challenge... Betty's response sent. Alfred (aidA) verifying Betty's (aidB) response... Alfred: Betty's response verified. SAID of exn: EN5L5ZYljzorITTNpqk4Trsmsrs-3yadrVSrm2f7nGVM Alfred: Marked Betty's contact as authenticated. [ { alias: "Betty_Contact_for_Alfred", oobi: "http://keria:3902/oobi/EIGa_qsf1GLVT2NXnXteLCN_-B0DKmGaV39p57eNCAOQ/agent/EFlcYHTOLZBoBETrCAE-xljLag8PgDCZTDtJuzo6_myU", id: "EIGa_qsf1GLVT2NXnXteLCN_-B0DKmGaV39p57eNCAOQ", ends: { agent: { "EFlcYHTOLZBoBETrCAE-xljLag8PgDCZTDtJuzo6_myU": { 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-07-18T00:27:43.369000+00:00", words: [ "gaze", "clever", "install", "jump", "captain", "piano", "dignity", "whale", "elephant", "endorse", "copper", "trumpet" ], said: "EN5L5ZYljzorITTNpqk4Trsmsrs-3yadrVSrm2f7nGVM", 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: EABWImOm9hIeZT2STeaobbcZw7yt3ITw5uEEDsiwXPLA) responding to Betty's (aidB: EIGa_qsf1GLVT2NXnXteLCN_-B0DKmGaV39p57eNCAOQ) challenge... Alfred's response sent. Betty (aidB) verifying Alfred's (aidA) response... Betty: Alfred's response verified. SAID of exn: EEw1jfkIz0L1af2bsiLGC9f1WDOpxzit7glRWG4_dWfB Betty: Marked Alfred's contact as authenticated. [ { alias: "Alfred_Contact_for_Betty", oobi: "http://keria:3902/oobi/EABWImOm9hIeZT2STeaobbcZw7yt3ITw5uEEDsiwXPLA/agent/EPz83YNKcZ4yW9NcVvqAPGu5MVqf8K1NcZRMhGJOH5EU", id: "EABWImOm9hIeZT2STeaobbcZw7yt3ITw5uEEDsiwXPLA", ends: { agent: { EPz83YNKcZ4yW9NcVvqAPGu5MVqf8K1NcZRMhGJOH5EU: { 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-07-18T00:27:43.832000+00:00", words: [ "six", "foil", "pill", "art", "cinnamon", "indoor", "warrior", "slim", "awful", "rather", "knife", "advance" ], said: "EEw1jfkIz0L1af2bsiLGC9f1WDOpxzit7glRWG4_dWfB", 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):
    1. Initial Setup: Each client was initialized, booted its KERIA agent, connected, and created an Autonomic Identifier(aidA for Alfred, aidB for Betty).
    2. 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().
    3. 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.
    4. 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.
    # 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): D_PNwZhM1_uoAH4UBsIc1 Client boot process initiated with KERIA agent. Client AID Prefix: EOkc0Lot12bbeQDpGLJEaWTtQvePbdY6OXWCqWuXzffp Agent AID Prefix: EEDrzjMKTOe34W994F_trCgbT0QHDGJZLhol60dlU71E Initiating AID inception for alias: aidA Successfully created AID with prefix: EEXUwD91ZA0fAxySpV-3AuxEBWwN1DzUkrD_f-k5zgV7 Assigning 'agent' role to KERIA Agent EEDrzjMKTOe34W994F_trCgbT0QHDGJZLhol60dlU71E 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/EEXUwD91ZA0fAxySpV-3AuxEBWwN1DzUkrD_f-k5zgV7/agent/EEDrzjMKTOe34W994F_trCgbT0QHDGJZLhol60dlU71E Using Passcode (bran): Bfx2oZDxfeDwLbjavNWbx Client boot process initiated with KERIA agent. Client AID Prefix: EMI5ibqHucIzvCuCcCTYPafPDLXytGCO4gD5KEyQM8Qw Agent AID Prefix: EHPHjDYmvgIqOUKxajaie6t1Atm5S27VtKpabPTTqvvd Resolving OOBI URL: http://keria:3902/oobi/EEXUwD91ZA0fAxySpV-3AuxEBWwN1DzUkrD_f-k5zgV7/agent/EEDrzjMKTOe34W994F_trCgbT0QHDGJZLhol60dlU71E with alias aidA Successfully resolved OOBI URL. Response: OK Contact "aidA" added/updated. Client and AID setup complete. Client A created AID: EEXUwD91ZA0fAxySpV-3AuxEBWwN1DzUkrD_f-k5zgV7 Client B resolved OOBI for AID: EEXUwD91ZA0fAxySpV-3AuxEBWwN1DzUkrD_f-k5zgV7 ## 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: [ "DGV_RzrcS1CTduuzzN_Rbi9BW4sDNf46d3cL0bXlpd96" ] New keys: [ "DLEwcLHSfhJdJNgfDoRB8vZM4TGdV5VdTSLFkD-gVxWN" ] Previous next-key digest: [ "EDKZ9aVnJf0_CFGvTmWbtyHHMxP2FwGZJpg6DnpFomiY" ] New next-key digest: [ "EPJF07p6ozvfhUOzpj58TLjfQxomAQ42pwQYjFrjB95s" ] ### 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.
    # 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): AKZY1fNBwmlOKq3flQKKF Client boot process initiated with KERIA agent. Client AID Prefix: EBeTToCL9Bu5T4cG3trMBPGtQefG6P2ubR8ItatYAaMz Agent AID Prefix: ENQeKR27rBqEQichHWRN6f-cFIVUOPYVFx8Kcsx1GuOc Initiating AID inception for alias: issuerAid Successfully created AID with prefix: EInqPd7K84Dfo4DzwPeITao_CX3DAuvP4e9EcTZ8ryYY Assigning 'agent' role to KERIA Agent ENQeKR27rBqEQichHWRN6f-cFIVUOPYVFx8Kcsx1GuOc 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/EInqPd7K84Dfo4DzwPeITao_CX3DAuvP4e9EcTZ8ryYY/agent/ENQeKR27rBqEQichHWRN6f-cFIVUOPYVFx8Kcsx1GuOc Using Passcode (bran): DFBHt1TdQF62vcmrYhME1 Client boot process initiated with KERIA agent. Client AID Prefix: EPOlStDvCocLPQK3k69c3X8wr6isLGj6YVm5iV6YkEBb Agent AID Prefix: EOMGu_PusfnTv1Q7W2uLcPI35s0aHIzuSaF6KX09fzpx Initiating AID inception for alias: holderAid Successfully created AID with prefix: EKbgh2bOcETApqRbnUd5A_MTQC6ahiNgSRkuLNpR5X0- Assigning 'agent' role to KERIA Agent EOMGu_PusfnTv1Q7W2uLcPI35s0aHIzuSaF6KX09fzpx 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/EKbgh2bOcETApqRbnUd5A_MTQC6ahiNgSRkuLNpR5X0-/agent/EOMGu_PusfnTv1Q7W2uLcPI35s0aHIzuSaF6KX09fzpx Using Passcode (bran): Aq0kul5oYDcDmixxgjR7A Client boot process initiated with KERIA agent. Client AID Prefix: ECzzvkokAPW27PRqD-NXKbKVSpv38VJrM6Vy5BhqIFNo Agent AID Prefix: ELllLsBHd8qhMvY10pniW3awVn3rilMEKv6kNnEpQg6I Initiating AID inception for alias: verifierAid Successfully created AID with prefix: EANP3JMLUsU5ngCxl4p4vtywC2K8Bd8ab2Fg60ikKEU- Assigning 'agent' role to KERIA Agent ELllLsBHd8qhMvY10pniW3awVn3rilMEKv6kNnEpQg6I 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/EANP3JMLUsU5ngCxl4p4vtywC2K8Bd8ab2Fg60ikKEU-/agent/ELllLsBHd8qhMvY10pniW3awVn3rilMEKv6kNnEpQg6I Resolving OOBI URL: http://keria:3902/oobi/EKbgh2bOcETApqRbnUd5A_MTQC6ahiNgSRkuLNpR5X0-/agent/EOMGu_PusfnTv1Q7W2uLcPI35s0aHIzuSaF6KX09fzpx with alias holderContact Successfully resolved OOBI URL. Response: OK Contact "holderContact" added/updated. Resolving OOBI URL: http://keria:3902/oobi/EInqPd7K84Dfo4DzwPeITao_CX3DAuvP4e9EcTZ8ryYY/agent/ENQeKR27rBqEQichHWRN6f-cFIVUOPYVFx8Kcsx1GuOc with alias issuerContact Successfully resolved OOBI URL. Response: OK Contact "issuerContact" added/updated. Resolving OOBI URL: http://keria:3902/oobi/EKbgh2bOcETApqRbnUd5A_MTQC6ahiNgSRkuLNpR5X0-/agent/EOMGu_PusfnTv1Q7W2uLcPI35s0aHIzuSaF6KX09fzpx with alias holderContact Successfully resolved OOBI URL. Response: OK Contact "holderContact" added/updated. Resolving OOBI URL: http://keria:3902/oobi/EANP3JMLUsU5ngCxl4p4vtywC2K8Bd8ab2Fg60ikKEU-/agent/ELllLsBHd8qhMvY10pniW3awVn3rilMEKv6kNnEpQg6I 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 EInqPd7K84Dfo4DzwPeITao_CX3DAuvP4e9EcTZ8ryYY. Registry creation response: { "anchor": { "i": "EA4LjjIE2SfG5ZLvWNMomRoBL25sOzeYXTYlyFckLiEH", "s": "0", "d": "EA4LjjIE2SfG5ZLvWNMomRoBL25sOzeYXTYlyFckLiEH" } } Registry: Name='issuerRegistry', SAID (regk)='EA4LjjIE2SfG5ZLvWNMomRoBL25sOzeYXTYlyFckLiEH' ### 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: "ELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j1", i: "EInqPd7K84Dfo4DzwPeITao_CX3DAuvP4e9EcTZ8ryYY", ri: "EA4LjjIE2SfG5ZLvWNMomRoBL25sOzeYXTYlyFckLiEH", s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK", a: { d: "EIpTPqAX3CjYsFJn13OSRLgTojc5N5ecpp_dRzeIQ094", i: "EKbgh2bOcETApqRbnUd5A_MTQC6ahiNgSRkuLNpR5X0-", eventName: "GLEIF Summit", accessLevel: "staff", validDate: "2026-10-01", dt: "2025-07-18T00:20:21.979000+00:00" } }, atc: "-IABELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j10AAAAAAAAAAAAAAAAAAAAAAAELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j1", iss: { v: "KERI10JSON0000ed_", t: "iss", d: "EHi5JRGXifVZUsdGv9sXOCLkfRNrmte5NuJhGa7kJVzM", i: "ELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j1", s: "0", ri: "EA4LjjIE2SfG5ZLvWNMomRoBL25sOzeYXTYlyFckLiEH", dt: "2025-07-18T00:20:21.979000+00:00" }, issatc: "-VAS-GAB0AAAAAAAAAAAAAAAAAAAAAACEOZa4v9bZNNSorDp8c-BBYTTmEYsYSwgv7RcIvFD3fAA", pre: "EInqPd7K84Dfo4DzwPeITao_CX3DAuvP4e9EcTZ8ryYY", 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: "ELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j1", s: "0", d: "EHi5JRGXifVZUsdGv9sXOCLkfRNrmte5NuJhGa7kJVzM", ri: "EA4LjjIE2SfG5ZLvWNMomRoBL25sOzeYXTYlyFckLiEH", ra: {}, a: { s: 2, d: "EOZa4v9bZNNSorDp8c-BBYTTmEYsYSwgv7RcIvFD3fAA" }, dt: "2025-07-18T00:20:21.979000+00:00", et: "iss" }, anchor: { pre: "ELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j1", sn: 0, d: "EHi5JRGXifVZUsdGv9sXOCLkfRNrmte5NuJhGa7kJVzM" }, anc: { v: "KERI10JSON00013a_", t: "ixn", d: "EOZa4v9bZNNSorDp8c-BBYTTmEYsYSwgv7RcIvFD3fAA", i: "EInqPd7K84Dfo4DzwPeITao_CX3DAuvP4e9EcTZ8ryYY", s: "2", p: "EII-kMX52ep_cekP2CELNAQ99X7sChcoo67oZdOpHmdj", a: [ { i: "ELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j1", s: "0", d: "EHi5JRGXifVZUsdGv9sXOCLkfRNrmte5NuJhGa7kJVzM" } ] }, ancatc: [ "-VBq-AABAAA_XsnPbTyvPYzGr0mNCY3759ZBSQcS0y2A02XPMr7PuRFm0h4HJfoh5WkY6mopBUtm_4xfG4_hkpR6nXS4-DIF-BADAADmd8Q1mtGQdFVhTGr5XKYGdY-dhORcwOd1Af2Yu0sNc-cD8UU_8Hkib-eF-JBNV5DORjFYw1J5HzFaJWeeqYAMABBtAfjzr8spBZdst0j0khZi7L42LzWSZK0kHbOCNZMHvtfrrQj3sPk85el1LDn43VWCXyUfBrDnSwgZqr51d0IIACAbSx-B3qKgWFOPAgexz2yNwr8Ma7bEejs3UCZSmGnMtHBvz4Urkdhvk7PYDwd8ksU1PJz5cbnP3S8elKoyUwsH-EAB0AAAAAAAAAAAAAAAAAAAAAAA1AAG2025-07-18T00c20c22d083667p00c00" ] } ### 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`). 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: "ELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j1", s: "0", d: "EHi5JRGXifVZUsdGv9sXOCLkfRNrmte5NuJhGa7kJVzM", ri: "EA4LjjIE2SfG5ZLvWNMomRoBL25sOzeYXTYlyFckLiEH", ra: {}, a: { s: 2, d: "EOZa4v9bZNNSorDp8c-BBYTTmEYsYSwgv7RcIvFD3fAA" }, dt: "2025-07-18T00:20:21.979000+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: "0AByDAn4HYgPHtuJrvCig30Q", dt: "2025-07-18T00:20:24.773780+00:00", r: false, a: { r: "/exn/ipex/grant", d: "EJ0Kd6gWZnQlCnK-3GnQQMbwank0vAvpmN4ss_XlhhdK", m: "" } } The grant referenced by the notification { exn: { v: "KERI10JSON00057f_", t: "exn", d: "EJ0Kd6gWZnQlCnK-3GnQQMbwank0vAvpmN4ss_XlhhdK", i: "EInqPd7K84Dfo4DzwPeITao_CX3DAuvP4e9EcTZ8ryYY", rp: "EKbgh2bOcETApqRbnUd5A_MTQC6ahiNgSRkuLNpR5X0-", p: "", dt: "2025-07-18T00:20:24.405000+00:00", r: "/ipex/grant", q: {}, a: { i: "EKbgh2bOcETApqRbnUd5A_MTQC6ahiNgSRkuLNpR5X0-", m: "" }, e: { acdc: { v: "ACDC10JSON0001c4_", d: "ELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j1", i: "EInqPd7K84Dfo4DzwPeITao_CX3DAuvP4e9EcTZ8ryYY", ri: "EA4LjjIE2SfG5ZLvWNMomRoBL25sOzeYXTYlyFckLiEH", s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK", a: { d: "EIpTPqAX3CjYsFJn13OSRLgTojc5N5ecpp_dRzeIQ094", i: "EKbgh2bOcETApqRbnUd5A_MTQC6ahiNgSRkuLNpR5X0-", eventName: "GLEIF Summit", accessLevel: "staff", validDate: "2026-10-01", dt: "2025-07-18T00:20:21.979000+00:00" } }, iss: { v: "KERI10JSON0000ed_", t: "iss", d: "EHi5JRGXifVZUsdGv9sXOCLkfRNrmte5NuJhGa7kJVzM", i: "ELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j1", s: "0", ri: "EA4LjjIE2SfG5ZLvWNMomRoBL25sOzeYXTYlyFckLiEH", dt: "2025-07-18T00:20:21.979000+00:00" }, anc: { v: "KERI10JSON00013a_", t: "ixn", d: "EOZa4v9bZNNSorDp8c-BBYTTmEYsYSwgv7RcIvFD3fAA", i: "EInqPd7K84Dfo4DzwPeITao_CX3DAuvP4e9EcTZ8ryYY", s: "2", p: "EII-kMX52ep_cekP2CELNAQ99X7sChcoo67oZdOpHmdj", a: [ [Object] ] }, d: "EAaFqBzUELUVCGLFoubm-PoJrEPS8YjnrxhFXY8IzM-U" } }, pathed: { acdc: "-IABELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j10AAAAAAAAAAAAAAAAAAAAAAAEHi5JRGXifVZUsdGv9sXOCLkfRNrmte5NuJhGa7kJVzM", iss: "-VAS-GAB0AAAAAAAAAAAAAAAAAAAAAAAEOZa4v9bZNNSorDp8c-BBYTTmEYsYSwgv7RcIvFD3fAA", anc: "-VBq-AABAAA_XsnPbTyvPYzGr0mNCY3759ZBSQcS0y2A02XPMr7PuRFm0h4HJfoh5WkY6mopBUtm_4xfG4_hkpR6nXS4-DIF-BADAADmd8Q1mtGQdFVhTGr5XKYGdY-dhORcwOd1Af2Yu0sNc-cD8UU_8Hkib-eF-JBNV5DORjFYw1J5HzFaJWeeqYAMABBtAfjzr8spBZdst0j0khZi7L42LzWSZK0kHbOCNZMHvtfrrQj3sPk85el1LDn43VWCXyUfBrDnSwgZqr51d0IIACAbSx-B3qKgWFOPAgexz2yNwr8Ma7bEejs3UCZSmGnMtHBvz4Urkdhvk7PYDwd8ksU1PJz5cbnP3S8elKoyUwsH-EAB0AAAAAAAAAAAAAAAAAAAAAAA1AAG2025-07-18T00c20c22d083667p00c00" } } ### 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: "0AByDAn4HYgPHtuJrvCig30Q", dt: "2025-07-18T00:20:24.773780+00:00", r: true, a: { r: "/exn/ipex/grant", d: "EJ0Kd6gWZnQlCnK-3GnQQMbwank0vAvpmN4ss_XlhhdK", m: "" } } ] } Credential as stored by Holder: { sad: { v: "ACDC10JSON0001c4_", d: "ELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j1", i: "EInqPd7K84Dfo4DzwPeITao_CX3DAuvP4e9EcTZ8ryYY", ri: "EA4LjjIE2SfG5ZLvWNMomRoBL25sOzeYXTYlyFckLiEH", s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK", a: { d: "EIpTPqAX3CjYsFJn13OSRLgTojc5N5ecpp_dRzeIQ094", i: "EKbgh2bOcETApqRbnUd5A_MTQC6ahiNgSRkuLNpR5X0-", eventName: "GLEIF Summit", accessLevel: "staff", validDate: "2026-10-01", dt: "2025-07-18T00:20:21.979000+00:00" } }, atc: "-IABELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j10AAAAAAAAAAAAAAAAAAAAAAAELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j1", iss: { v: "KERI10JSON0000ed_", t: "iss", d: "EHi5JRGXifVZUsdGv9sXOCLkfRNrmte5NuJhGa7kJVzM", i: "ELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j1", s: "0", ri: "EA4LjjIE2SfG5ZLvWNMomRoBL25sOzeYXTYlyFckLiEH", dt: "2025-07-18T00:20:21.979000+00:00" }, issatc: "-VAS-GAB0AAAAAAAAAAAAAAAAAAAAAACEOZa4v9bZNNSorDp8c-BBYTTmEYsYSwgv7RcIvFD3fAA", pre: "EInqPd7K84Dfo4DzwPeITao_CX3DAuvP4e9EcTZ8ryYY", 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: "ELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j1", s: "0", d: "EHi5JRGXifVZUsdGv9sXOCLkfRNrmte5NuJhGa7kJVzM", ri: "EA4LjjIE2SfG5ZLvWNMomRoBL25sOzeYXTYlyFckLiEH", ra: {}, a: { s: 2, d: "EOZa4v9bZNNSorDp8c-BBYTTmEYsYSwgv7RcIvFD3fAA" }, dt: "2025-07-18T00:20:21.979000+00:00", et: "iss" }, anchor: { pre: "ELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j1", sn: 0, d: "EHi5JRGXifVZUsdGv9sXOCLkfRNrmte5NuJhGa7kJVzM" }, anc: { v: "KERI10JSON00013a_", t: "ixn", d: "EOZa4v9bZNNSorDp8c-BBYTTmEYsYSwgv7RcIvFD3fAA", i: "EInqPd7K84Dfo4DzwPeITao_CX3DAuvP4e9EcTZ8ryYY", s: "2", p: "EII-kMX52ep_cekP2CELNAQ99X7sChcoo67oZdOpHmdj", a: [ { i: "ELnSh4dIcGKK3CfB_NsuaLOOat4GR0KFNLfwQssKv0j1", s: "0", d: "EHi5JRGXifVZUsdGv9sXOCLkfRNrmte5NuJhGa7kJVzM" } ] }, ancatc: [ "-VBq-AABAAA_XsnPbTyvPYzGr0mNCY3759ZBSQcS0y2A02XPMr7PuRFm0h4HJfoh5WkY6mopBUtm_4xfG4_hkpR6nXS4-DIF-BADAADmd8Q1mtGQdFVhTGr5XKYGdY-dhORcwOd1Af2Yu0sNc-cD8UU_8Hkib-eF-JBNV5DORjFYw1J5HzFaJWeeqYAMABBtAfjzr8spBZdst0j0khZi7L42LzWSZK0kHbOCNZMHvtfrrQj3sPk85el1LDn43VWCXyUfBrDnSwgZqr51d0IIACAbSx-B3qKgWFOPAgexz2yNwr8Ma7bEejs3UCZSmGnMtHBvz4Urkdhvk7PYDwd8ksU1PJz5cbnP3S8elKoyUwsH-EAB0AAAAAAAAAAAAAAAAAAAAAAA1AAG2025-07-18T00c20c24d926728p00c00" ] } ### 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: "0ABYvw7RdLb-uFEp7KYRtPQ9", dt: "2025-07-18T00:20:30.374198+00:00", r: true, a: { r: "/exn/ipex/admit", d: "EHhpugqbltwwKI1tKOODD7ifmfXbQlOs66bLfDUQGA8Q", 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.
    # 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): CTTExa6fEIhIh0Jq5QJDD Client boot process initiated with KERIA agent. Client AID Prefix: EOdFEcUwyAoXxuwRbOxcT5yjncZ194FKrukR6u3X_QEd Agent AID Prefix: EJUgs6Etl_L1PYMMHSF2RFUx64dIzuYHokK2kZcxneWb Initiating AID inception for alias: issuerAid Successfully created AID with prefix: EEEwwKTA3HkVtUOVT9sdOhL9QCxb_9W1wLEHNf4mXYNq Assigning 'agent' role to KERIA Agent EJUgs6Etl_L1PYMMHSF2RFUx64dIzuYHokK2kZcxneWb 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/EEEwwKTA3HkVtUOVT9sdOhL9QCxb_9W1wLEHNf4mXYNq/agent/EJUgs6Etl_L1PYMMHSF2RFUx64dIzuYHokK2kZcxneWb Creating Holder... Using Passcode (bran): A4Z0bYjeY8j-5Z2R-wn-p Client boot process initiated with KERIA agent. Client AID Prefix: EKGvaGNVbBj5wAXs2auZsyHi_JopfWCBMuhOOKHAHFZ2 Agent AID Prefix: EO0YkeUQc4W8HHo7kXQAz5-xSn2h45sFLLFevfCihJZg Initiating AID inception for alias: holderAid Successfully created AID with prefix: EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o Assigning 'agent' role to KERIA Agent EO0YkeUQc4W8HHo7kXQAz5-xSn2h45sFLLFevfCihJZg 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/EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o/agent/EO0YkeUQc4W8HHo7kXQAz5-xSn2h45sFLLFevfCihJZg Creating Verifier... Using Passcode (bran): Bb45shXEuVABXq-RyDa4y Client boot process initiated with KERIA agent. Client AID Prefix: EJx4oHxGbvDWhkPWMwlDiycyQbOtoPvReSjlPVhsQXxr Agent AID Prefix: EH-iKhnXI5Wqj3Vam2SXDSFVKIA0G8tB8pTpPqMzEvqY Initiating AID inception for alias: verifierAid Successfully created AID with prefix: EPDCTQyZAcJsL5GEEAaOSOHkL1Qat1Pw_mxRJKVkLt6N Assigning 'agent' role to KERIA Agent EH-iKhnXI5Wqj3Vam2SXDSFVKIA0G8tB8pTpPqMzEvqY 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/EPDCTQyZAcJsL5GEEAaOSOHkL1Qat1Pw_mxRJKVkLt6N/agent/EH-iKhnXI5Wqj3Vam2SXDSFVKIA0G8tB8pTpPqMzEvqY Created issuer, holder, and verifier AIDs Resolving OOBI URL: http://keria:3902/oobi/EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o/agent/EO0YkeUQc4W8HHo7kXQAz5-xSn2h45sFLLFevfCihJZg with alias holderContact Successfully resolved OOBI URL. Response: OK Contact "holderContact" added/updated. Resolving OOBI URL: http://keria:3902/oobi/EEEwwKTA3HkVtUOVT9sdOhL9QCxb_9W1wLEHNf4mXYNq/agent/EJUgs6Etl_L1PYMMHSF2RFUx64dIzuYHokK2kZcxneWb with alias issuerContact Successfully resolved OOBI URL. Response: OK Contact "issuerContact" added/updated. Resolving OOBI URL: http://keria:3902/oobi/EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o/agent/EO0YkeUQc4W8HHo7kXQAz5-xSn2h45sFLLFevfCihJZg with alias holderContact Successfully resolved OOBI URL. Response: OK Contact "holderContact" added/updated. Resolving OOBI URL: http://keria:3902/oobi/EPDCTQyZAcJsL5GEEAaOSOHkL1Qat1Pw_mxRJKVkLt6N/agent/EH-iKhnXI5Wqj3Vam2SXDSFVKIA0G8tB8pTpPqMzEvqY with alias verifierContact Successfully resolved OOBI URL. Response: OK Contact "verifierContact" added/updated. Resolving OOBI URL: http://keria:3902/oobi/EPDCTQyZAcJsL5GEEAaOSOHkL1Qat1Pw_mxRJKVkLt6N/agent/EH-iKhnXI5Wqj3Vam2SXDSFVKIA0G8tB8pTpPqMzEvqY 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: EAr2KedLIvtpFPABdwZnVRbtdpUmobJj4hXDipV0jbDg issuing credential to holder Issuing credential from AID "issuerAid" to AID "EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o"... { name: "credential.EMSxMtiyDFvJsz5lXxH6lrpfYWOAipEnzbL4jqjIMng9", metadata: { ced: { v: "ACDC10JSON0001c4_", d: "EMSxMtiyDFvJsz5lXxH6lrpfYWOAipEnzbL4jqjIMng9", i: "EEEwwKTA3HkVtUOVT9sdOhL9QCxb_9W1wLEHNf4mXYNq", ri: "EAr2KedLIvtpFPABdwZnVRbtdpUmobJj4hXDipV0jbDg", s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK", a: { d: "EDxoYp2mlqK_mHGz8KFpokQmImVPG4KooFvHDSWy-5qd", i: "EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o", eventName: "GLEIF Summit", accessLevel: "staff", validDate: "2026-10-01", dt: "2025-07-18T00:20:43.044000+00:00" } }, depends: { name: "witness.EOZor6-eTOY2lDuKWQb_Okw7o8DE6_QIz98TWN202EfD", metadata: { pre: "EEEwwKTA3HkVtUOVT9sdOhL9QCxb_9W1wLEHNf4mXYNq", sn: 2 }, done: false, error: null, response: null } }, done: true, error: null, response: { ced: { v: "ACDC10JSON0001c4_", d: "EMSxMtiyDFvJsz5lXxH6lrpfYWOAipEnzbL4jqjIMng9", i: "EEEwwKTA3HkVtUOVT9sdOhL9QCxb_9W1wLEHNf4mXYNq", ri: "EAr2KedLIvtpFPABdwZnVRbtdpUmobJj4hXDipV0jbDg", s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK", a: { d: "EDxoYp2mlqK_mHGz8KFpokQmImVPG4KooFvHDSWy-5qd", i: "EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o", eventName: "GLEIF Summit", accessLevel: "staff", validDate: "2026-10-01", dt: "2025-07-18T00:20:43.044000+00:00" } } } } Successfully issued credential with SAID: EMSxMtiyDFvJsz5lXxH6lrpfYWOAipEnzbL4jqjIMng9 granting credential to holder AID "issuerAid" granting credential to AID "EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o" via IPEX... Successfully submitted IPEX grant from "issuerAid" to "EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o". 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 "EPentYfXMccOk9m9f4Gw8AIDG-3wmtnnJhwJM1pFG_fx" from AID "EEEwwKTA3HkVtUOVT9sdOhL9QCxb_9W1wLEHNf4mXYNq"... Successfully submitted IPEX admit for grant "EPentYfXMccOk9m9f4Gw8AIDG-3wmtnnJhwJM1pFG_fx". Holder admitting credential Marking notification "0AAxfE3hByvxIMc8tSWB9iYg" as read... Notification "0AAxfE3hByvxIMc8tSWB9iYg" marked as read. Issuer receiving admit... Waiting for notification with route "/exn/ipex/admit"... Marking notification "0ADH3e9X-D5ayiYUOHeNNYNL" as read... Notification "0ADH3e9X-D5ayiYUOHeNNYNL" 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:

    1. IPEX Grant: The issuer or holder using an IPEX Grant to share the credential with a verifier.
    2. 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.
    3. 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.") ``` Holder received Apply Notification: { i: "0ACXlaITTLV1e_kfybXNllke", dt: "2025-07-18T00:20:50.042878+00:00", r: false, a: { r: "/exn/ipex/apply", d: "EHtaGwkhDiLXaV9d5eR1qzs3_jFhVdqUCH4k8xtSTbRu", m: "" } } Details of Apply Exchange received by Holder: { exn: { v: "KERI10JSON0001a0_", t: "exn", d: "EHtaGwkhDiLXaV9d5eR1qzs3_jFhVdqUCH4k8xtSTbRu", i: "EPDCTQyZAcJsL5GEEAaOSOHkL1Qat1Pw_mxRJKVkLt6N", rp: "EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o", p: "", dt: "2025-07-18T00:20:49.707000+00:00", r: "/ipex/apply", q: {}, a: { i: "EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o", 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: "0AAxfE3hByvxIMc8tSWB9iYg", dt: "2025-07-18T00:20:44.212030+00:00", r: true, a: { r: "/exn/ipex/grant", d: "EPentYfXMccOk9m9f4Gw8AIDG-3wmtnnJhwJM1pFG_fx", m: "" } }, { i: "0ACXlaITTLV1e_kfybXNllke", dt: "2025-07-18T00:20:50.042878+00:00", r: true, a: { r: "/exn/ipex/apply", d: "EHtaGwkhDiLXaV9d5eR1qzs3_jFhVdqUCH4k8xtSTbRu", 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: "EMSxMtiyDFvJsz5lXxH6lrpfYWOAipEnzbL4jqjIMng9", i: "EEEwwKTA3HkVtUOVT9sdOhL9QCxb_9W1wLEHNf4mXYNq", ri: "EAr2KedLIvtpFPABdwZnVRbtdpUmobJj4hXDipV0jbDg", s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK", a: { d: "EDxoYp2mlqK_mHGz8KFpokQmImVPG4KooFvHDSWy-5qd", i: "EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o", eventName: "GLEIF Summit", accessLevel: "staff", validDate: "2026-10-01", dt: "2025-07-18T00:20:43.044000+00:00" } }, atc: "-IABEMSxMtiyDFvJsz5lXxH6lrpfYWOAipEnzbL4jqjIMng90AAAAAAAAAAAAAAAAAAAAAAAEMSxMtiyDFvJsz5lXxH6lrpfYWOAipEnzbL4jqjIMng9", iss: { v: "KERI10JSON0000ed_", t: "iss", d: "EOC5gTJru7-sKumMD-NAI2939oNseIhAUgQXM_Z62TzD", i: "EMSxMtiyDFvJsz5lXxH6lrpfYWOAipEnzbL4jqjIMng9", s: "0", ri: "EAr2KedLIvtpFPABdwZnVRbtdpUmobJj4hXDipV0jbDg", dt: "2025-07-18T00:20:43.044000+00:00" }, issatc: "-VAS-GAB0AAAAAAAAAAAAAAAAAAAAAACEOZor6-eTOY2lDuKWQb_Okw7o8DE6_QIz98TWN202EfD", pre: "EEEwwKTA3HkVtUOVT9sdOhL9QCxb_9W1wLEHNf4mXYNq", 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: "EMSxMtiyDFvJsz5lXxH6lrpfYWOAipEnzbL4jqjIMng9", s: "0", d: "EOC5gTJru7-sKumMD-NAI2939oNseIhAUgQXM_Z62TzD", ri: "EAr2KedLIvtpFPABdwZnVRbtdpUmobJj4hXDipV0jbDg", ra: {}, a: { s: 2, d: "EOZor6-eTOY2lDuKWQb_Okw7o8DE6_QIz98TWN202EfD" }, dt: "2025-07-18T00:20:43.044000+00:00", et: "iss" }, anchor: { pre: "EMSxMtiyDFvJsz5lXxH6lrpfYWOAipEnzbL4jqjIMng9", sn: 0, d: "EOC5gTJru7-sKumMD-NAI2939oNseIhAUgQXM_Z62TzD" }, anc: { v: "KERI10JSON00013a_", t: "ixn", d: "EOZor6-eTOY2lDuKWQb_Okw7o8DE6_QIz98TWN202EfD", i: "EEEwwKTA3HkVtUOVT9sdOhL9QCxb_9W1wLEHNf4mXYNq", s: "2", p: "EMxUEjfQnPW9qDGbq3P3QxohAY_Dopm_EI6nuDSjacnh", a: [ { i: "EMSxMtiyDFvJsz5lXxH6lrpfYWOAipEnzbL4jqjIMng9", s: "0", d: "EOC5gTJru7-sKumMD-NAI2939oNseIhAUgQXM_Z62TzD" } ] }, ancatc: [ "-VBq-AABAADRl8Ztnu7VsAgsXg9DhqbhP5-7vCMpVQySlaittHd_NkKmR1kUnodTb4sU9JIi8WhqhDUhHor3vor3BZ8n8zID-BADAAAHfm_oJwguxKijBg9gTNW2xvxWSRlimvwNz_VptplFB35iryITOoNpKQBHHPLI5QruAZHBFuWTk0ZuVYKMtzULABApvAriAq7M2i5y2wGo6c9pod1ZXZq-s-wYlSyoTcPNGzSo7bt2mKsPgeV3R1zlcWPuW4qJWITs-LS-Mt57WOEGACC6ujPUnJd18cpSgSCm9HEvglu8vSgdGfm5JEK4CwMiw6P7iuUrNkkL-d9b-fBqoIi3pVypwJMLVSmH5xduawsF-EAB0AAAAAAAAAAAAAAAAAAAAAAA1AAG2025-07-18T00c20c44d364361p00c00" ] } ] βœ… 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: "0ACBwV9ijnrKbyh49ri6s9Op", dt: "2025-07-18T00:20:50.543461+00:00", r: false, a: { r: "/exn/ipex/offer", d: "EBGcJ9X-Trb0Zp8hBqQrjX682P0TxRIVP5h0XDBGJyRL", m: "" } } Details of Offer Exchange received by Verifier: { exn: { v: "KERI10JSON000376_", t: "exn", d: "EBGcJ9X-Trb0Zp8hBqQrjX682P0TxRIVP5h0XDBGJyRL", i: "EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o", rp: "EPDCTQyZAcJsL5GEEAaOSOHkL1Qat1Pw_mxRJKVkLt6N", p: "EHtaGwkhDiLXaV9d5eR1qzs3_jFhVdqUCH4k8xtSTbRu", dt: "2025-07-18T00:20:50.175000+00:00", r: "/ipex/offer", q: {}, a: { i: "EPDCTQyZAcJsL5GEEAaOSOHkL1Qat1Pw_mxRJKVkLt6N", m: "" }, e: { acdc: { v: "ACDC10JSON0001c4_", d: "EMSxMtiyDFvJsz5lXxH6lrpfYWOAipEnzbL4jqjIMng9", i: "EEEwwKTA3HkVtUOVT9sdOhL9QCxb_9W1wLEHNf4mXYNq", ri: "EAr2KedLIvtpFPABdwZnVRbtdpUmobJj4hXDipV0jbDg", s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK", a: { d: "EDxoYp2mlqK_mHGz8KFpokQmImVPG4KooFvHDSWy-5qd", i: "EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o", eventName: "GLEIF Summit", accessLevel: "staff", validDate: "2026-10-01", dt: "2025-07-18T00:20:43.044000+00:00" } }, d: "EAalXInUR9IvSa0v9UzgdiNAYYbC6HHVv3UGuXc7Mzmz" } }, pathed: {} } Verifier's notifications after marking offer as read: { start: 0, end: 0, total: 1, notes: [ { i: "0ACBwV9ijnrKbyh49ri6s9Op", dt: "2025-07-18T00:20:50.543461+00:00", r: true, a: { r: "/exn/ipex/offer", d: "EBGcJ9X-Trb0Zp8hBqQrjX682P0TxRIVP5h0XDBGJyRL", 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 "EPDCTQyZAcJsL5GEEAaOSOHkL1Qat1Pw_mxRJKVkLt6N" via IPEX... Successfully submitted IPEX grant from "holderAid" to "EPDCTQyZAcJsL5GEEAaOSOHkL1Qat1Pw_mxRJKVkLt6N". βœ… 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: "0ABHQg8nlWFD5VLQJWxpqduQ", dt: "2025-07-18T00:20:56.388669+00:00", r: false, a: { r: "/exn/ipex/grant", d: "EAkudEkKGmyZnpmyrY66hvAkbvzWingYXf_b9UX4apUe", m: "" } } Details of ACDC embedded in the Grant Exchange received by Verifier: { v: "ACDC10JSON0001c4_", d: "EMSxMtiyDFvJsz5lXxH6lrpfYWOAipEnzbL4jqjIMng9", i: "EEEwwKTA3HkVtUOVT9sdOhL9QCxb_9W1wLEHNf4mXYNq", ri: "EAr2KedLIvtpFPABdwZnVRbtdpUmobJj4hXDipV0jbDg", s: "EGUPiCVO73M9worPwR3PfThAtC0AJnH5ZgwsXf6TzbVK", a: { d: "EDxoYp2mlqK_mHGz8KFpokQmImVPG4KooFvHDSWy-5qd", i: "EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o", eventName: "GLEIF Summit", accessLevel: "staff", validDate: "2026-10-01", dt: "2025-07-18T00:20:43.044000+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 Gdmit 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 "EAkudEkKGmyZnpmyrY66hvAkbvzWingYXf_b9UX4apUe" from AID "EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o"... Successfully submitted IPEX admit for grant "EAkudEkKGmyZnpmyrY66hvAkbvzWingYXf_b9UX4apUe". Verifier admitting credential Marking notification "0ABHQg8nlWFD5VLQJWxpqduQ" as read... Notification "0ABHQg8nlWFD5VLQJWxpqduQ" 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: "0ABHQg8nlWFD5VLQJWxpqduQ", dt: "2025-07-18T00:20:56.388669+00:00", r: true, a: { r: "/exn/ipex/grant", d: "EAkudEkKGmyZnpmyrY66hvAkbvzWingYXf_b9UX4apUe", 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 "0ABhWj6OM9r3zIMRqAO-q4f9" as read... Notification "0ABhWj6OM9r3zIMRqAO-q4f9" 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: "EMSxMtiyDFvJsz5lXxH6lrpfYWOAipEnzbL4jqjIMng9", s: "0", d: "EOC5gTJru7-sKumMD-NAI2939oNseIhAUgQXM_Z62TzD", ri: "EAr2KedLIvtpFPABdwZnVRbtdpUmobJj4hXDipV0jbDg", ra: {}, a: { s: 2, d: "EOZor6-eTOY2lDuKWQb_Okw7o8DE6_QIz98TWN202EfD" }, dt: "2025-07-18T00:20:43.044000+00:00", et: "iss" } βœ… Credential status after revocation: { vn: [ 1, 0 ], i: "EMSxMtiyDFvJsz5lXxH6lrpfYWOAipEnzbL4jqjIMng9", s: "1", d: "EBKIGih2bq1KpsuHgSizCEtVwN_-UtCUAIsIq9YRs8oH", ri: "EAr2KedLIvtpFPABdwZnVRbtdpUmobJj4hXDipV0jbDg", ra: {}, a: { s: 3, d: "EKaLCpABdCQCfuiMIVS-E5tBYQ-e6FQb79PgY4dNSQqa" }, dt: "2025-07-18T00:21:02.104000+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 "EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o" via IPEX... Successfully submitted IPEX grant from "issuerAid" to "EOYY0hXXWhxmwQXqPjMq_om7seKMxXIZeJ2GWyAmUJ-o". βœ… 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 "EGRpTc9C0pzOp1CuyGa2Z5Yfwk0odgl2wSqS6bZ1djXB" from AID "EEEwwKTA3HkVtUOVT9sdOhL9QCxb_9W1wLEHNf4mXYNq"... Successfully submitted IPEX admit for grant "EGRpTc9C0pzOp1CuyGa2Z5Yfwk0odgl2wSqS6bZ1djXB". Holder admitting credential Marking notification "0AA1HC0XTazUK1r8dC9IZan4" as read... Notification "0AA1HC0XTazUK1r8dC9IZan4" 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 "0AAqf5QAi6VBCLfO87HoukLl" as read... Notification "0AAqf5QAi6VBCLfO87HoukLl" 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 "EPDCTQyZAcJsL5GEEAaOSOHkL1Qat1Pw_mxRJKVkLt6N" via IPEX... Successfully submitted IPEX grant from "issuerAid" to "EPDCTQyZAcJsL5GEEAaOSOHkL1Qat1Pw_mxRJKVkLt6N". βœ… 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 "EGabIsDe18eV_ki1DKF0Ue7y2FUuKGBx4ZNbV-x3tSpq" from AID "EEEwwKTA3HkVtUOVT9sdOhL9QCxb_9W1wLEHNf4mXYNq"... Successfully submitted IPEX admit for grant "EGabIsDe18eV_ki1DKF0Ue7y2FUuKGBx4ZNbV-x3tSpq". Verifier admitting credential Marking notification "0AB2WiYRVG7QAAybjSL_iHBY" as read... Notification "0AB2WiYRVG7QAAybjSL_iHBY" 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.

    # 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 # 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 Verification Chain ### 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. GLEIF Root of Trust and Delegated Identifier 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. ![vLEI Chain of Verifiable Authority](./images/chain-of-authority.png) 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. ![vLEI Legal Entity Credential](./images/le-credential.png) ### 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.
    # 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. ![vLEI Verification Chain](./images/vlei-verification-chain.png) ## 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: EK_qCRmuU45_q4QUr_paYswM302iNV_KNADxZWa9_hup Creating GLEIF AID Initiating AID inception for alias: gleif Successfully created AID with prefix: EKOhpyKyDF4QV7lWP3ZQogr9U5ob2IAy7KrtFuNSio22 Assigning 'agent' role to KERIA Agent EK_qCRmuU45_q4QUr_paYswM302iNV_KNADxZWa9_hup for AID alias gleif Successfully assigned 'agent' role for AID alias gleif. Generating OOBI for AID alias gleif with role agent Generated OOBI URL: http://keria:3902/oobi/EKOhpyKyDF4QV7lWP3ZQogr9U5ob2IAy7KrtFuNSio22/agent/EK_qCRmuU45_q4QUr_paYswM302iNV_KNADxZWa9_hup GLEIF Prefix: EKOhpyKyDF4QV7lWP3ZQogr9U5ob2IAy7KrtFuNSio22 Using Passcode (bran): DODop2H7J8hD5ftVUJa_f Client boot process initiated with KERIA agent. Client AID Prefix: EBlZaHK_OAIujHTD5d_ExnRHzhGJ3OMaFP88yJDSReri Agent AID Prefix: EKm8xdGrUIix7JZfb-Uo0kLYIpgG9vNIfXt2Xknme5UP Initiating AID inception for alias: qvi Successfully created AID with prefix: EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb Assigning 'agent' role to KERIA Agent EKm8xdGrUIix7JZfb-Uo0kLYIpgG9vNIfXt2Xknme5UP 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/EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb/agent/EKm8xdGrUIix7JZfb-Uo0kLYIpgG9vNIfXt2Xknme5UP QVI Prefix: EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb Using Passcode (bran): B-5uJ3GH6ettWRz2JyByq Client boot process initiated with KERIA agent. Client AID Prefix: EFI5zB2uKlqeVzCXGdanj-vbo9IZz1BApVy2WPaVNE_e Agent AID Prefix: EBIX_9LsD826eLCZuzSMNdf7AEwvkIjV6Linksg9NEOj Initiating AID inception for alias: le Successfully created AID with prefix: EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG Assigning 'agent' role to KERIA Agent EBIX_9LsD826eLCZuzSMNdf7AEwvkIjV6Linksg9NEOj 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/EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG/agent/EBIX_9LsD826eLCZuzSMNdf7AEwvkIjV6Linksg9NEOj LE Prefix: EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG Using Passcode (bran): B9IqWPGVA9olcdsSv1FDu Client boot process initiated with KERIA agent. Client AID Prefix: EIbad3DE1QoPUB0ycEF5rmn0uPQLfrJfzjURbsEefs7I Agent AID Prefix: ENem7qd6RY4Kk1Nr0FFOVfq8l7qosxZ9dM3dBi7xBQ1Y Initiating AID inception for alias: role Successfully created AID with prefix: EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD Assigning 'agent' role to KERIA Agent ENem7qd6RY4Kk1Nr0FFOVfq8l7qosxZ9dM3dBi7xBQ1Y 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/EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD/agent/ENem7qd6RY4Kk1Nr0FFOVfq8l7qosxZ9dM3dBi7xBQ1Y ROLE Prefix: EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD Resolving OOBIs Resolving OOBI URL: http://keria:3902/oobi/EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb/agent/EKm8xdGrUIix7JZfb-Uo0kLYIpgG9vNIfXt2Xknme5UP with alias qvi Resolving OOBI URL: http://keria:3902/oobi/EKOhpyKyDF4QV7lWP3ZQogr9U5ob2IAy7KrtFuNSio22/agent/EK_qCRmuU45_q4QUr_paYswM302iNV_KNADxZWa9_hup with alias gleif Resolving OOBI URL: http://keria:3902/oobi/EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG/agent/EBIX_9LsD826eLCZuzSMNdf7AEwvkIjV6Linksg9NEOj with alias le Resolving OOBI URL: http://keria:3902/oobi/EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD/agent/ENem7qd6RY4Kk1Nr0FFOVfq8l7qosxZ9dM3dBi7xBQ1Y with alias role Resolving OOBI URL: http://keria:3902/oobi/EKOhpyKyDF4QV7lWP3ZQogr9U5ob2IAy7KrtFuNSio22/agent/EK_qCRmuU45_q4QUr_paYswM302iNV_KNADxZWa9_hup with alias gleif Resolving OOBI URL: http://keria:3902/oobi/EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb/agent/EKm8xdGrUIix7JZfb-Uo0kLYIpgG9vNIfXt2Xknme5UP with alias qvi Resolving OOBI URL: http://keria:3902/oobi/EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD/agent/ENem7qd6RY4Kk1Nr0FFOVfq8l7qosxZ9dM3dBi7xBQ1Y with alias role Resolving OOBI URL: http://keria:3902/oobi/EKOhpyKyDF4QV7lWP3ZQogr9U5ob2IAy7KrtFuNSio22/agent/EK_qCRmuU45_q4QUr_paYswM302iNV_KNADxZWa9_hup with alias gleif Resolving OOBI URL: http://keria:3902/oobi/EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG/agent/EBIX_9LsD826eLCZuzSMNdf7AEwvkIjV6Linksg9NEOj with alias le Resolving OOBI URL: http://keria:3902/oobi/EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb/agent/EKm8xdGrUIix7JZfb-Uo0kLYIpgG9vNIfXt2Xknme5UP 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 "gleif" added/updated. Contact "qvi" added/updated. Contact "le" added/updated. Contact "qvi" added/updated. Contact "role" added/updated. Contact "gleif" added/updated. Contact "qvi" added/updated. Contact "role" added/updated. Contact "le" added/updated. Contact "gleif" added/updated. Creating Credential Registries Creating GLEIF Registry Creating credential registry "gleifRegistry" for AID alias "gleif"... Successfully created credential registry: EMVSIrBzP0BuAnmuIcE2UwyG_5L8FAIAV3ChB6-PFmy2 Creating credential registry "qviRegistry" for AID alias "qvi"... Successfully created credential registry: ECpgAt4SKlKvNL90AE-fkOZ1OH7mvhMP7iCmq_PNUM2r Creating credential registry "leRegistry" for AID alias "le"... Successfully created credential registry: EMASG0VnzsqD6M05aisJn_2WpiGtq-R60oTjC7V9db7K 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 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. 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 "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb"... { name: "credential.ELpnnEMY6qbGwb4aQJraUVyhjsDFb7ayByMq60C8I9an", metadata: { ced: { v: "ACDC10JSON000197_", d: "ELpnnEMY6qbGwb4aQJraUVyhjsDFb7ayByMq60C8I9an", i: "EKOhpyKyDF4QV7lWP3ZQogr9U5ob2IAy7KrtFuNSio22", ri: "EMVSIrBzP0BuAnmuIcE2UwyG_5L8FAIAV3ChB6-PFmy2", s: "EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao", a: { d: "EO43S-TIuVUvT_XvE0QUyiT9zANM2FbPs22yGkvRB39l", i: "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb", LEI: "254900OPPU84GM83MG36", dt: "2025-07-18T00:31:56.279000+00:00" } }, depends: { name: "witness.EGUapx9fn_BYEaTANhwYfRyTMGHTtGz1bVPygSySNg3W", metadata: { pre: "EKOhpyKyDF4QV7lWP3ZQogr9U5ob2IAy7KrtFuNSio22", sn: 2 }, done: false, error: null, response: null } }, done: true, error: null, response: { ced: { v: "ACDC10JSON000197_", d: "ELpnnEMY6qbGwb4aQJraUVyhjsDFb7ayByMq60C8I9an", i: "EKOhpyKyDF4QV7lWP3ZQogr9U5ob2IAy7KrtFuNSio22", ri: "EMVSIrBzP0BuAnmuIcE2UwyG_5L8FAIAV3ChB6-PFmy2", s: "EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao", a: { d: "EO43S-TIuVUvT_XvE0QUyiT9zANM2FbPs22yGkvRB39l", i: "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb", LEI: "254900OPPU84GM83MG36", dt: "2025-07-18T00:31:56.279000+00:00" } } } } Successfully issued credential with SAID: ELpnnEMY6qbGwb4aQJraUVyhjsDFb7ayByMq60C8I9an Granting Credential AID "gleif" granting credential to AID "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb" via IPEX... Successfully submitted IPEX grant from "gleif" to "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb". 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 "ENvz5X-iphyW3KEZvfB_ZLwJeH43HjB5bDVmjHvkgSvT" from AID "EKOhpyKyDF4QV7lWP3ZQogr9U5ob2IAy7KrtFuNSio22"... Successfully submitted IPEX admit for grant "ENvz5X-iphyW3KEZvfB_ZLwJeH43HjB5bDVmjHvkgSvT". Marking notification "0AChz-5XOMYg3E1-SHyhjecm" as read... Notification "0AChz-5XOMYg3E1-SHyhjecm" marked as read. Waiting for notification with route "/exn/ipex/admit"... Marking notification "0AD8ui7uQv7Z4SDJZFg0TSAY" as read... Notification "0AD8ui7uQv7Z4SDJZFg0TSAY" 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 "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG"... { name: "credential.EBhRb6NzmCprA7p1TubnPfaDkqDIPbSwuG1sUh7DalWq", metadata: { ced: { v: "ACDC10JSON0005c8_", d: "EBhRb6NzmCprA7p1TubnPfaDkqDIPbSwuG1sUh7DalWq", i: "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb", ri: "ECpgAt4SKlKvNL90AE-fkOZ1OH7mvhMP7iCmq_PNUM2r", s: "ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY", a: { d: "ENE7ovo4z4SZzchSzEgY0GWIYW2D54_6Xyfz1JiKEsPx", i: "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG", LEI: "875500ELOZEL05BVXV37", dt: "2025-07-18T00:32:04.753000+00:00" }, e: { d: "EDgnY4jpaVFuz_Zr8prijPxDJDdytlrkBgQcslSF8DqZ", qvi: { n: "ELpnnEMY6qbGwb4aQJraUVyhjsDFb7ayByMq60C8I9an", 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.EDwyo79LSUBNwhjHJP_SKs6ebsiKLx7NvHnZeKRfUuDb", metadata: { pre: "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb", sn: 2 }, done: false, error: null, response: null } }, done: true, error: null, response: { ced: { v: "ACDC10JSON0005c8_", d: "EBhRb6NzmCprA7p1TubnPfaDkqDIPbSwuG1sUh7DalWq", i: "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb", ri: "ECpgAt4SKlKvNL90AE-fkOZ1OH7mvhMP7iCmq_PNUM2r", s: "ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY", a: { d: "ENE7ovo4z4SZzchSzEgY0GWIYW2D54_6Xyfz1JiKEsPx", i: "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG", LEI: "875500ELOZEL05BVXV37", dt: "2025-07-18T00:32:04.753000+00:00" }, e: { d: "EDgnY4jpaVFuz_Zr8prijPxDJDdytlrkBgQcslSF8DqZ", qvi: { n: "ELpnnEMY6qbGwb4aQJraUVyhjsDFb7ayByMq60C8I9an", 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: EBhRb6NzmCprA7p1TubnPfaDkqDIPbSwuG1sUh7DalWq Granting Credential AID "qvi" granting credential to AID "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG" via IPEX... Successfully submitted IPEX grant from "qvi" to "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG". 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 "EAq7_-f8j84vCS7gi-7yt3fA7PSLc23_vzgFFDhpN1C0" from AID "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb"... Successfully submitted IPEX admit for grant "EAq7_-f8j84vCS7gi-7yt3fA7PSLc23_vzgFFDhpN1C0". Marking notification "0AAzXfgoOf8-LLdWqhybq009" as read... Notification "0AAzXfgoOf8-LLdWqhybq009" marked as read. Waiting for notification with route "/exn/ipex/admit"... Marking notification "0AByAZ3_x_bYkOw07wC2ELR4" as read... Notification "0AByAZ3_x_bYkOw07wC2ELR4" 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 "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb"... { name: "credential.EP6oTQDfrpVVFH0CAgpmNIoJmKC_-xYXgH1n6akLBy_g", metadata: { ced: { v: "ACDC10JSON000602_", d: "EP6oTQDfrpVVFH0CAgpmNIoJmKC_-xYXgH1n6akLBy_g", i: "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG", ri: "EMASG0VnzsqD6M05aisJn_2WpiGtq-R60oTjC7V9db7K", s: "EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E", a: { d: "EP9Q1ajBjgHtIGpP5ndWcHBDQL7NbTaE3ll4oQfcsNvk", i: "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb", AID: "", LEI: "875500ELOZEL05BVXV37", personLegalName: "Jane Doe", officialRole: "CEO", dt: "2025-07-18T00:32:13.066000+00:00" }, e: { d: "EJiyyaCGlPPGA3CvqtR_3SSYRFmZb7hwG2NU9_YniYEe", le: { n: "EBhRb6NzmCprA7p1TubnPfaDkqDIPbSwuG1sUh7DalWq", 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.EIXxomNqrIh5QOMvmEZSOV33lLnNNQwOKksCTikvZGTU", metadata: { pre: "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG", sn: 2 }, done: false, error: null, response: null } }, done: true, error: null, response: { ced: { v: "ACDC10JSON000602_", d: "EP6oTQDfrpVVFH0CAgpmNIoJmKC_-xYXgH1n6akLBy_g", i: "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG", ri: "EMASG0VnzsqD6M05aisJn_2WpiGtq-R60oTjC7V9db7K", s: "EKA57bKBKxr_kN7iN5i7lMUxpMG-s19dRcmov1iDxz-E", a: { d: "EP9Q1ajBjgHtIGpP5ndWcHBDQL7NbTaE3ll4oQfcsNvk", i: "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb", AID: "", LEI: "875500ELOZEL05BVXV37", personLegalName: "Jane Doe", officialRole: "CEO", dt: "2025-07-18T00:32:13.066000+00:00" }, e: { d: "EJiyyaCGlPPGA3CvqtR_3SSYRFmZb7hwG2NU9_YniYEe", le: { n: "EBhRb6NzmCprA7p1TubnPfaDkqDIPbSwuG1sUh7DalWq", 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: EP6oTQDfrpVVFH0CAgpmNIoJmKC_-xYXgH1n6akLBy_g Granting Credential AID "le" granting credential to AID "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb" via IPEX... Successfully submitted IPEX grant from "le" to "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb". 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 "EFbLEhRyDozmgJ4Brxia5E1umotbIPIcJQ2XbqBwltwW" from AID "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG"... Successfully submitted IPEX admit for grant "EFbLEhRyDozmgJ4Brxia5E1umotbIPIcJQ2XbqBwltwW". Marking notification "0AD3prpxFkWoEKLgwHlcrNN0" as read... Notification "0AD3prpxFkWoEKLgwHlcrNN0" marked as read. Waiting for notification with route "/exn/ipex/admit"... Marking notification "0AAeKH9pb7rd_kzL85OXAWk4" as read... Notification "0AAeKH9pb7rd_kzL85OXAWk4" 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 "EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD"... { name: "credential.EMJh3p0CNQerrykfKNQ7HrXeT5ovyyMBcxH_QFSuNRjp", metadata: { ced: { v: "ACDC10JSON000605_", d: "EMJh3p0CNQerrykfKNQ7HrXeT5ovyyMBcxH_QFSuNRjp", i: "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb", ri: "ECpgAt4SKlKvNL90AE-fkOZ1OH7mvhMP7iCmq_PNUM2r", s: "EBNaNu-M9P5cgrnfl2Fvymy4E_jvxxyjb70PRtiANlJy", a: { d: "EEMrzTpQrMB0gzAx8DWZoAQktvxSM41HTzKURY10pJwV", i: "EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD", LEI: "875500ELOZEL05BVXV37", personLegalName: "Jane Doe", officialRole: "CEO", dt: "2025-07-18T00:32:21.405000+00:00" }, e: { d: "EEE-W1E3oMbgt2tniVNCGz7VBYTfBh2Ebe7rM2ckOEwU", auth: { n: "EP6oTQDfrpVVFH0CAgpmNIoJmKC_-xYXgH1n6akLBy_g", 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.EI3a0iJaXTXyDKCtUVyBGUGQDXjV2pxilrf6h6X2N42B", metadata: { pre: "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb", sn: 3 }, done: false, error: null, response: null } }, done: true, error: null, response: { ced: { v: "ACDC10JSON000605_", d: "EMJh3p0CNQerrykfKNQ7HrXeT5ovyyMBcxH_QFSuNRjp", i: "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb", ri: "ECpgAt4SKlKvNL90AE-fkOZ1OH7mvhMP7iCmq_PNUM2r", s: "EBNaNu-M9P5cgrnfl2Fvymy4E_jvxxyjb70PRtiANlJy", a: { d: "EEMrzTpQrMB0gzAx8DWZoAQktvxSM41HTzKURY10pJwV", i: "EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD", LEI: "875500ELOZEL05BVXV37", personLegalName: "Jane Doe", officialRole: "CEO", dt: "2025-07-18T00:32:21.405000+00:00" }, e: { d: "EEE-W1E3oMbgt2tniVNCGz7VBYTfBh2Ebe7rM2ckOEwU", auth: { n: "EP6oTQDfrpVVFH0CAgpmNIoJmKC_-xYXgH1n6akLBy_g", 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: EMJh3p0CNQerrykfKNQ7HrXeT5ovyyMBcxH_QFSuNRjp Granting Credential AID "qvi" granting credential to AID "EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD" via IPEX... Successfully submitted IPEX grant from "qvi" to "EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD". 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 "ENd52fK--mCcKHvfJ2t3C-4CVMBxj1_MLapx-YazDX_n" from AID "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb"... Successfully submitted IPEX admit for grant "ENd52fK--mCcKHvfJ2t3C-4CVMBxj1_MLapx-YazDX_n". Marking notification "0AByMpKxGMt2RxzvzyV9fHOX" as read... Notification "0AByMpKxGMt2RxzvzyV9fHOX" marked as read. Waiting for notification with route "/exn/ipex/admit"... Marking notification "0ACnUuT14anZO2QGQ_9Ql9jZ" as read... Notification "0ACnUuT14anZO2QGQ_9Ql9jZ" 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 "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb"... { name: "credential.EK6ZSkelhfJR2i1aKVOzMAaTLyJZZVaaNGVtm2qG3Ned", metadata: { ced: { v: "ACDC10JSON000816_", d: "EK6ZSkelhfJR2i1aKVOzMAaTLyJZZVaaNGVtm2qG3Ned", i: "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG", ri: "EMASG0VnzsqD6M05aisJn_2WpiGtq-R60oTjC7V9db7K", s: "EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g", a: { d: "EHHRh2UlhtwD5XAlKqWsqt4WyN576VtsH2EjcbgqIDND", i: "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb", AID: "", LEI: "875500ELOZEL05BVXV37", personLegalName: "John Doe", engagementContextRole: "Managing Director", dt: "2025-07-18T00:32:29.547000+00:00" }, e: { d: "EJiyyaCGlPPGA3CvqtR_3SSYRFmZb7hwG2NU9_YniYEe", le: { n: "EBhRb6NzmCprA7p1TubnPfaDkqDIPbSwuG1sUh7DalWq", 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.EFeNczgi1epWleHR_h0fY0p4WvJBWGJVVQLdie5zuqA3", metadata: { pre: "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG", sn: 3 }, done: false, error: null, response: null } }, done: true, error: null, response: { ced: { v: "ACDC10JSON000816_", d: "EK6ZSkelhfJR2i1aKVOzMAaTLyJZZVaaNGVtm2qG3Ned", i: "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG", ri: "EMASG0VnzsqD6M05aisJn_2WpiGtq-R60oTjC7V9db7K", s: "EH6ekLjSr8V32WyFbGe1zXjTzFs9PkTYmupJ9H65O14g", a: { d: "EHHRh2UlhtwD5XAlKqWsqt4WyN576VtsH2EjcbgqIDND", i: "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb", AID: "", LEI: "875500ELOZEL05BVXV37", personLegalName: "John Doe", engagementContextRole: "Managing Director", dt: "2025-07-18T00:32:29.547000+00:00" }, e: { d: "EJiyyaCGlPPGA3CvqtR_3SSYRFmZb7hwG2NU9_YniYEe", le: { n: "EBhRb6NzmCprA7p1TubnPfaDkqDIPbSwuG1sUh7DalWq", 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: EK6ZSkelhfJR2i1aKVOzMAaTLyJZZVaaNGVtm2qG3Ned Granting Credential AID "le" granting credential to AID "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb" via IPEX... Successfully submitted IPEX grant from "le" to "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb". 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 "ENVYzw6LWYhvTNel4F3a8ukR67Ele0hCww_LN5koyQ1b" from AID "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG"... Successfully submitted IPEX admit for grant "ENVYzw6LWYhvTNel4F3a8ukR67Ele0hCww_LN5koyQ1b". Marking notification "0AA64v8WOwhvfEFzbeavKpRD" as read... Notification "0AA64v8WOwhvfEFzbeavKpRD" marked as read. Waiting for notification with route "/exn/ipex/admit"... Marking notification "0ACSfQaVk3tF4wdtYywLA-OA" as read... Notification "0ACSfQaVk3tF4wdtYywLA-OA" 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 "EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD"... { name: "credential.EPiJg8dr85f7gxsqS5aatchdXd7lUgWBvAzeyt5YQIAe", metadata: { ced: { v: "ACDC10JSON0007dc_", d: "EPiJg8dr85f7gxsqS5aatchdXd7lUgWBvAzeyt5YQIAe", u: "0AAR4PAjL6mV652tH6oElTeK", i: "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG", ri: "EMASG0VnzsqD6M05aisJn_2WpiGtq-R60oTjC7V9db7K", s: "EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw", a: { d: "EF0DpTizv4UASZhTw28_ahP8nT2GQAxpcL1K-q9JV0-L", i: "EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD", LEI: "875500ELOZEL05BVXV37", personLegalName: "John Doe", engagementContextRole: "Managing Director", dt: "2025-07-18T00:32:37.263000+00:00" }, e: { d: "EJiyyaCGlPPGA3CvqtR_3SSYRFmZb7hwG2NU9_YniYEe", le: { n: "EBhRb6NzmCprA7p1TubnPfaDkqDIPbSwuG1sUh7DalWq", 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.EDF-9IqgR4PJY7wbHD-2AXkxnN6v3syzwHV-WNIOMtvK", metadata: { pre: "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG", sn: 4 }, done: false, error: null, response: null } }, done: true, error: null, response: { ced: { v: "ACDC10JSON0007dc_", d: "EPiJg8dr85f7gxsqS5aatchdXd7lUgWBvAzeyt5YQIAe", u: "0AAR4PAjL6mV652tH6oElTeK", i: "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG", ri: "EMASG0VnzsqD6M05aisJn_2WpiGtq-R60oTjC7V9db7K", s: "EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw", a: { d: "EF0DpTizv4UASZhTw28_ahP8nT2GQAxpcL1K-q9JV0-L", i: "EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD", LEI: "875500ELOZEL05BVXV37", personLegalName: "John Doe", engagementContextRole: "Managing Director", dt: "2025-07-18T00:32:37.263000+00:00" }, e: { d: "EJiyyaCGlPPGA3CvqtR_3SSYRFmZb7hwG2NU9_YniYEe", le: { n: "EBhRb6NzmCprA7p1TubnPfaDkqDIPbSwuG1sUh7DalWq", 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: EPiJg8dr85f7gxsqS5aatchdXd7lUgWBvAzeyt5YQIAe Granting Credential AID "le" granting credential to AID "EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD" via IPEX... Successfully submitted IPEX grant from "le" to "EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD". 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 "EE98D8N3Tx99IljSRWK9iYrc8VxvoYEWtdNDCba8IWts" from AID "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG"... Successfully submitted IPEX admit for grant "EE98D8N3Tx99IljSRWK9iYrc8VxvoYEWtdNDCba8IWts". Marking notification "0ACcGOvtuiQwlXY4wKZdcpeG" as read... Notification "0ACcGOvtuiQwlXY4wKZdcpeG" marked as read. Waiting for notification with route "/exn/ipex/admit"... Marking notification "0AAkd7dOY6wd0tqsnBada0do" as read... Notification "0AAkd7dOY6wd0tqsnBada0do" 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 "EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD"... { name: "credential.ECI7JLJXxHfbXa8tIKRpb7qAegNjOyRnGpppvc04Npx5", metadata: { ced: { v: "ACDC10JSON0007e8_", d: "ECI7JLJXxHfbXa8tIKRpb7qAegNjOyRnGpppvc04Npx5", u: "0AAVQUbEDuotWzToMzTaSOdr", i: "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb", ri: "ECpgAt4SKlKvNL90AE-fkOZ1OH7mvhMP7iCmq_PNUM2r", s: "EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw", a: { d: "EOPKWjQIdGxpuaZ0hCZsWIWZM5zPxPPByNw8cV_j4qEn", i: "EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD", LEI: "875500ELOZEL05BVXV37", personLegalName: "John Doe", engagementContextRole: "Managing Director", dt: "2025-07-18T00:32:44.767000+00:00" }, e: { d: "ENI6yPGvSOZliA6OtRWgD6da5JXZk1VpHi8SQWuTEx-p", auth: { n: "EK6ZSkelhfJR2i1aKVOzMAaTLyJZZVaaNGVtm2qG3Ned", 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.EKMeHTsXwxQHzoSesLa-hIwFrrWTtvl3QT0VUywl8lIY", metadata: { pre: "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb", sn: 4 }, done: false, error: null, response: null } }, done: true, error: null, response: { ced: { v: "ACDC10JSON0007e8_", d: "ECI7JLJXxHfbXa8tIKRpb7qAegNjOyRnGpppvc04Npx5", u: "0AAVQUbEDuotWzToMzTaSOdr", i: "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb", ri: "ECpgAt4SKlKvNL90AE-fkOZ1OH7mvhMP7iCmq_PNUM2r", s: "EEy9PkikFcANV1l7EHukCeXqrzT1hNZjGlUk7wuMO5jw", a: { d: "EOPKWjQIdGxpuaZ0hCZsWIWZM5zPxPPByNw8cV_j4qEn", i: "EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD", LEI: "875500ELOZEL05BVXV37", personLegalName: "John Doe", engagementContextRole: "Managing Director", dt: "2025-07-18T00:32:44.767000+00:00" }, e: { d: "ENI6yPGvSOZliA6OtRWgD6da5JXZk1VpHi8SQWuTEx-p", auth: { n: "EK6ZSkelhfJR2i1aKVOzMAaTLyJZZVaaNGVtm2qG3Ned", 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: ECI7JLJXxHfbXa8tIKRpb7qAegNjOyRnGpppvc04Npx5 Granting Credential AID "qvi" granting credential to AID "EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD" via IPEX... Successfully submitted IPEX grant from "qvi" to "EBH5kVgvQouoPqfTTcXPCkTbqeuWs4ECpndQucY6N3UD". 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 "ECJEoAxtCbhlOJmT-a3Sa5axCtsw77GtxCdDM40MNttr" from AID "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb"... Successfully submitted IPEX admit for grant "ECJEoAxtCbhlOJmT-a3Sa5axCtsw77GtxCdDM40MNttr". Marking notification "0ABaltHWBnhZ3XkQGpXrMiA_" as read... Notification "0ABaltHWBnhZ3XkQGpXrMiA_" marked as read. Waiting for notification with route "/exn/ipex/admit"... Marking notification "0ABJHytAegvmC4gT8kc4htzt" as read... Notification "0ABJHytAegvmC4gT8kc4htzt" 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=EKOhpyKyDF4QV7lWP3ZQogr9U5ob2IAy7KrtFuNSio22 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.
    ⚠️ BUG ALERT

    There is a known issue where Sally cannot process credential presentation on the first attempt. To work around this, run the code cell below twice

    For more details on this and other issues, please see the Known Issues Section

    ```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 200 from Sally Fetching Credential Info... { "credential": "EBhRb6NzmCprA7p1TubnPfaDkqDIPbSwuG1sUh7DalWq", "type": "LE", "issuer": "EF9f9EHcW2r42dxOe0nCe73-qmHEJaJJM9YhiC1M-uxb", "holder": "EIAaw14n2zX8zfn6IkFLI6k_Gta26wCWV1B27CE9V6OG", "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.
    # 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: Sally Webhook Fails on First Presentation The `sally` vLEI Reporting Agent may fail to send a webhook notification on the very first credential presentation it receives after being started. **Expected Behavior** Upon receiving and successfully validating a credential presentation, `sally` should immediately make a POST request to its pre-configured webhook URL. **Actual Behavior** On the first attempt to present a credential, sally's logs show multiple errors, and the webhook call is not made. However, on the second and all subsequent presentation attempts for the same or different credentials, `sally` processes the presentation correctly and successfully calls the webhook. **Workaround** If a webhook call is not received after the first presentation, simply perform the presentation again. For more technical details and to track the status of this issue, please refer to: https://github.com/GLEIF-IT/sally/issues/46 ## Issue 3: 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 # Welcome to vLEI Training - 101 This collection of Notebooks is designed to guide you through the foundational concepts of the [Key Event Receipt Infrastructure](https://trustoverip.github.io/tswg-keri-specification/) (KERI) and [Authentic Chained Data Containers](https://trustoverip.github.io/tswg-acdc-specification/) (ACDC) protocols, followed by the workings of the [verifiable Legal Entity Identifier](https://www.gleif.org/en/organizational-identity/introducing-the-verifiable-lei-vlei/introducing-the-vlei-ecosystem-governance-framework) (vLEI) ecosystem. We aim to equip you with the knowledge needed to build applications leveraging this powerful identity technology. After completing this training, you will: - Understand the KERI protocol - Understand the ACDC protocol - Understand the vLEI ecosystem - Have the basis to develop your own vLEI POC ## Prerequisites The training aims to be accessible, but having background knowledge will certainly smooth your learning journey. 1. **Command-Line Interface (CLI) Familiarity:** The training will involve using the KERI Command Line Interface (KLI). Therefore, having prior experience working with a terminal or command prompt (like Bash, Zsh, PowerShell, or Windows CMD) is highly beneficial. 2. **Conceptual Understanding of Digital Identity:** A general awareness of digital identity concepts and some understanding of the limitations of traditional systems will provide useful context for understanding KERI's purpose. 3. **Basic Cryptography Concepts:** Having a basic understanding of what public and private keys are and the general idea behind digital signatures and hash functions will give you a head start. 4. **Python Programming:** The training includes several Python scripts. 5. **TypeScript Programming:** Code snippets from the 102 module notebooks utilize TypeScript code. 6. **Docker Basics:** For setting up and troubleshooting more complex KERI environments or running components like witnesses or agents, a basic understanding of Docker concepts (containers, images, `docker-compose`) will be useful. ## Understanding Your Learning Environment This training series utilizes Jupyter Notebooks to provide an interactive and hands-on learning experience. Jupyter Notebooks allow for a mix of explanatory text, and live, executable code cells, creating a dynamic way to understand complex topics like KERI, ACDCs, and the vLEI ecosystem. ### Notebook Philosophy Each notebook in this series is designed to be largely **stand-alone for the concepts it introduces**, building upon the knowledge from previous notebooks. While conceptual links are strong, the code examples within a specific notebook are generally self-contained or rely on a clearly defined setup at the beginning of that notebook. Crucially, **cells within a single notebook are meant to be executed in sequence from top to bottom.** Variables, states, and environments created in earlier cells are often prerequisites for later cells to function correctly. Running cells out of order, or skipping cells, will likely lead to errors or unexpected behavior. ### Navigating Large Notebooks For longer notebooks, navigating can be made easier using the **Table of Contents (ToC)** feature. In Jupyter Lab, you can find this in the left sidebar. * Look for an icon that resembles a list or a document outline. Clicking this will open a navigable ToC based on the Markdown headings (H1, H2, H3, etc.) in the notebook. * This allows you to quickly jump to specific sections of the notebook, which is especially helpful when reviewing material or looking for particular topics. ### Interacting with Notebook Cells Jupyter Notebooks are composed of different types of cells, primarily: * **Markdown Cells:** These cells contain explanatory text, like the one you are reading now. They are formatted using Markdown syntax, which allows for rich text formatting, images, and links. You do not "run" Markdown cells in the same way as code cells, but they are rendered to display the formatted text. * **Code Cells:** These cells contain executable code. In this training series, you will encounter: * Shell commands (for `kli`): Prefixed with an exclamation mark (`!`), e.g., `!kli status`. * Python code: For scripting, examples, and utility functions. * TypeScript code: In the 102 module notebooks for `signify-ts` examples. **Running Code Cells:** To execute a code cell: 1. Select the cell by clicking on it. 2. Press `Shift + Enter` to run the current cell and automatically select the next cell. 3. Alternatively, you can click the "Run" button (a play icon ▢️) in the toolbar. When a code cell is running, an asterisk (`[*]`) will appear in the brackets to its left. Once execution is complete, a number (e.g., `[1]`) will replace the asterisk, indicating the order of execution. Any output from the code (text, errors, etc.) will be displayed directly below the cell. **Running All Cells:** If you want to run all cells in a notebook from top to bottom, especially after restarting the kernel or opening the notebook fresh, you can use the "Restart Kernel and Run All Cells" option. * In Jupyter Lab, this is found in the "Kernel" menu (`Kernel > Restart Kernel and Run All Cells...`) or as a button in the toolbar (represented by a double play icon ⏩). * This is a convenient way to ensure the entire notebook is executed in the correct order.
    πŸ’‘ TIP
    If you see `In [*]:` next to a cell for a long time, it means the code is still running. Some operations, especially those involving network communication or complex cryptographic processes, might take a few moments to complete.
    ### Managing the Notebook Kernel Each active notebook is connected to a "kernel," which is the computational engine that executes the code in the notebook's cells. * **Restarting the Kernel:** If you encounter persistent errors, or if you want to reset the notebook's state and start fresh (e.g., clear all variables), you can restart the kernel. * In Jupyter Lab, this is done via the "Kernel" menu: `Kernel > Restart Kernel...`. * Restarting the kernel will require you to re-run cells from the beginning to redefine variables and recreate the necessary state (or use "Restart Kernel and Run All Cells"). * **Interrupting the Kernel:** If a cell is taking too long to execute or you suspect it's stuck in an infinite loop, you can interrupt the kernel. * In Jupyter Lab, use `Kernel > Interrupt Kernel`. * You can also use the **Stop button** (A square ⏹️ icon) in the toolbar to interrupt the currently running cell. ### Clearing Output You can clear the output of a single cell or all cells in a notebook: * **Current Cell:** `Edit > Clear Output` (or right-click the cell). * **All Cells:** `Edit > Clear All Outputs`. This can be useful for decluttering the view or before re-running a notebook from scratch. ## Software Versions This material was created and tested to work with: - **[weboftrust/keri:1.2.6](https://github.com/WebOfTrust/keripy/releases/tag/1.2.6)** - **[gleif/keria:0.3.0](https://github.com/GLEIF-IT/keria/releases/tag/0.3.0)** - **[weboftrust/signify-ts:0.3.0-rc1](https://www.npmjs.com/package/signify-ts)** - **[weboftrust/vlei:1.0.0](https://github.com/WebOfTrust/vLEI/releases/tag/1.0.0)**
    🧩 DID YOU KNOW?
    KERI, ACDC, and the vLEI ecosystem offer a strong foundation for secure digital interactions. However, achieving truly strong security requires additional effort. Real-world safety depends on proper implementation and security practices. Even the best technology can be weakened by things like losing control of private keys, or people being tricked into giving away their access (social engineering). Achieving real security is about combining strong technology with sound operational security measures.
    [<- Prev (TOC)](000_Table_of_Contents.ipynb) | [Next (Intro) ->](101_07_Introduction_to-KERI_ACDC_and_vLEI.ipynb) # Foundations: KERI, ACDC, and the vLEI Ecosystem
    🎯 OBJECTIVE
    Provide a high-level overview of the three foundational concepts we'll be covering during this training:
  • The KERI protocol for secure identifiers
  • The ACDC protocol for verifiable credentials
  • The GLEIF vLEI ecosystem, which applies these technologies to organizational identity.

    Consider this a starting point; we'll dive into the details, practical examples, and specific commands in the notebooks that follow.
  • ## The KERI Protocol **KERI** stands for **Key Event Receipt Infrastructure**, invented by [Dr. Samuel Smith](https://keri.one/131-2/). It's a decentralized key management infrastructure (DKMI) that aims to provide a secure and decentralized identity layer for the internet, focusing on establishing trust through cryptographic proof rather than relying solely on centralized authorities. It is a "never trust, always verify" security model with no share secrets, meaning no shared passwords, keys, or other types of cryptographic secrets. This means that KERI is a signed-everything model, meaning every communication between components is signed so that trust in each transmission can be verified by verifying its signature or by verifying the anchoring of data to a key event log (KEL). Core Ideas: * **Self Addressing Identifiers (SAIDs):** [Self addressing identifiers](https://trustoverip.github.io/tswg-keri-specification/#term:said) are a special type of content-addressable identifier where the identifier is based on and embedded within the data it refers to, making it self referential. The embedding happens after computing the digest in a two step digest and embedding process. See the [spec reference](https://trustoverip.github.io/tswg-said-specification/draft-ssmith-said.html). * **Autonomic Identifiers (AIDs):** KERI's foundation is built on [self-certifying identifiers](https://trustoverip.github.io/tswg-keri-specification/#self-certifying-identifier-scid) called AIDs. These identifiers are generated from and cryptographically bound to key pairs controlled by an entity, eliminating the need for a central registration authority for the identifier itself. An AID is a SAID derived from the first event (inception event) in a key event log (KEL). * **Key Event Logs (KELs):** Each AID has an associated KEL, which is a secure, append-only log of signed "key events" (like identifier creation, key rotation, etc.). Each KEL is the log for only one AID. This log provides a verifiable key history, or provenance, of the control over the AID. Anyone can verify the current authoritative keys for an AID by processing its KEL. * **End-Verifiability:** KERI emphasizes that identifier control and key events can be verified by anyone, anywhere, using only the KEL, without trusting intermediaries. * **Witnesses:** For high availability and resilience, the person controlling of keys for an AID (controller) can designate witnesses who receive, verify, and store key events. This both allows the controller to set security thresholds for event signing and also makes the KEL accessible when the controller is offline. * **And more:** KERI has many other advanced features, but we'll focus on the fundamentals in this introduction. ## The ACDC Protocol **ACDC** stands for **Authentic Chained Data Container**. It is KERI's native format for Verifiable Credentials (VCs), designed to work within KERI-based ecosystems. **Core Ideas:** * **Verifiable Credentials:** ACDCs are digital containers for claims or attributes (like a name, role, or authorization) that are issued by one identifier (AID) to another. * **Built on KERI:** ACDCs leverage AIDs for identifying issuers and issues. The validity and status (issued, revoked) of an ACDC are anchored to the issuer's Key Event Log (KEL) through a secondary log called a Transaction Event Log (TEL). * **Schemas & SAIDs:** Each ACDC conforms to a specific Schema, which defines its structure and data types. Both the schema and the ACDC instance itself are identified using SAIDs (Self-Addressing Identifiers), making them tamper-evident. * **Chaining (Edges):** ACDCs can be cryptographically linked together using "edges," forming verifiable chains or graphs of evidence (e.g., an approval credential linking back to the request credential). * **Rules:** ACDCs can optionally include embedded machine-readable rules or legal prose (like Ricardian Contracts). * **IPEX (Issuance and Presentation Exchange):** a [credential exchange protocol](https://trustoverip.github.io/tswg-acdc-specification/#issuance-and-presentation-exchange-ipex) defining a mechanism and workflow for how ACDCs are issued between parties and how they are presented for verification in a securely attributable way. This protocol also defines a workflow for [graduated disclosure](https://trustoverip.github.io/tswg-acdc-specification/#graduated-disclosure), a variant of selective disclosure that allows for progressive, selective unblinding of claims or attributes after an agreement has been negotiated between the discloser (holder) and the receiver (disclosee) of an ACDC. ## The GLEIF vLEI Ecosystem The **verifiable Legal Entity Identifier (vLEI)** is a system pioneered by the Global Legal Entity Identifier Foundation (GLEIF) to create a secure, digitized version of the traditional LEI used for organizational identity. It aims to enable automated authentication and verification of organizations globally. **Core Ideas:** * **Digital Counterpart to LEI:** The vLEI acts as a digitally verifiable representation of an organization's LEI code, enabling automated, machine-readable verification. * **Built on KERI/ACDC:** The vLEI infrastructure is built using the KERI protocol and represents vLEI credentials as ACDCs. This leverages KERI's security and ACDC's verifiable credential format. * **Trust Chain / Ecosystem:** The vLEI system establishes a chain of trust: * **GLEIF (Root of Trust):** GLEIF operates as the root of the ecosystem; its AID and KEL serve as the ultimate anchor for verifying the authority of QVIs. The root of trust uses **identifier delegation** to establish an authorization chain from the Root of Trust to the QVI through delegation chained KELs. * **Qualified vLEI Issuers (QVIs):** GLEIF uses its KERI identity to issue QVI credentials to a trusted network of QVIs. This means that both identifier delegation and credential issuance are used to delegate authority from GLEIF to QVIs for the purpose of allowing QVIs to issue vLEI credentials. * **Organizations:** QVIs are qualified to issue vLEI credentials, which represent the organization's identity, to legal entities. * **Organizational Role:** An organization holding a vLEI can then issue specific **vLEI Role Credentials** to individuals representing the organization in official or functional capacities (e.g., CEO, authorized signatory, supplier). These role credentials cryptographically bind the person's identity in that role to the organization's vLEI. * **Official Organizational Role (OOR) Credential:** A person representing a legal entity may be issued an OOR credential that indicates their official role in an organization. The rules for OOR credential issuance must follow the ISO 5009 Official Organization Role standard for official role names. * **Engagement Context Role (ECR) Credential:** An ECR credential indicates a person performs a given role for a company-defined context. It is a more permissive credential type where the name of the role is legal-entity specific. * **QVI Workflow:** The workflow centrally involves the QVIs. GLEIF qualifies these issuers. A QVI interacts with an organization to verify its identity information (linked to its traditional LEI) and then uses its verifiable delegated authority from GLEIF to issue the organization its primary vLEI credential. This QVI issuance step is crucial for establishing the organization's verifiable digital identity within the ecosystem. * **Organization Workflow:** Once an organization receives its vLEI credential from a QVI, it can issue credentials including OORs and ECRs. The vLEI ecosystem uses KERI and ACDC to extend the existing LEI system into the digital realm, creating a globally verifiable system for organizational identity that incorporates the roles individuals hold within an organization organizations, all anchored back to GLEIF as the root of trust.
    πŸ“ SUMMARY
    KERI provides the secure identifier layer, ACDC provides the credential format on top of KERI, and vLEI is a specific application of both for organizational identity.
    [<- Prev (Welcome)](101_05_Welcome_to_vLEI_Training_-_101.ipynb) | [Next (KERI Command Line Interface - KLI) ->](101_10_KERI_Command_Line_Interface.ipynb) # Understanding the KERI Command Line Interface (KLI)
    🎯 OBJECTIVE
    Introduce the KERI Command Line Interface (KLI) and demonstrate some of its basic utility commands.
    ## Using KLI in Notebooks Throughout these notebooks, you will interact with the KERI protocol using the **KLI**. The KLI is the standard text-based tool for managing identifiers and infrastructure directly from your computer's terminal. Since you are working within Jupyter notebooks, the KLI commands are written with an exclamation mark prefix (`!`). This tells the notebook environment to run the command in the underlying system shell, rather than as Python code. So, you'll frequently see commands structured like this: `!kli [options]` **What can you do with KLI?** The KLI provides a wide range of functionalities. Key capabilities include: - **Identifier management**: Management and creation of keystores and identifiers - **Utility functions**: Functions to facilitate KERI-related operations for debugging and troubleshooting. - **Credential management**: Creation of credentials - **Comunication operations**: Establishing connections between AIDs - **IPEX actions**: To issue and present credentials - **Run witness**: Start a witness process in order to receipt key events - **Others**: The KLI provides commands for most of the features available in the KERI and ACDC protocol implementations.
    ℹ️ NOTE
    There are UI based methods to manage Identifiers, known as wallets, but for the purpose of this training, the KLI offers a good compromise between ease of use and visibility of technical details.
    ## Overview of Basic Utilities Let's explore some helpful commands available in the **KERI Command Line Interface (KLI)**. This isn't a complete list of every command, but it covers some essential utilities that you'll find useful as you work with KERI. **KERI library version** ```python !kli version ``` Library version: 1.2.8 **Generate a salt**: Create a new random salt (or seed) in the fully-qualified [CESR](https://trustoverip.github.io/tswg-cesr-specification/) format. A salt is a random value used as an input when generating cryptographic key pairs to help ensure their uniqueness and security. What it means to be fully qualified is that the bytes in the cryptographic salt are ordered according to the CESR protocol. This ordering will be explained in a later training when CESR is introduced and explained. For now just think of CESR as a custom file format for KERI and ACDC data. ```python # This will output a qualified base64 string representing the salt !kli salt ``` 0AAkG1BmB7xnXBxo2Aasxrq9 **Generate a passcode**: The passcode is used to encrypt your keystore, providing an additional layer of protection. ```python # This will output a random string suitable for use as an encryption passcode !kli passcode generate ``` tbV7F3IPkvU7hHY1bgVsl **Print a timestamp**: Timestamps are typically used in operations involving multiple signers with what are called multi-signature (or "multisig") groups. ```python !kli time ``` 2025-09-12T04:05:52.717998+00:00 **Display help menu** ```python !kli -h ``` usage: kli [-h] command ... options: -h, --help show this help message and exit subcommands: command aid Print the AID for a given alias challenge clean Cleans and migrates a database and keystore contacts decrypt Decrypt arbitrary data for AIDs with Ed25519 p ... delegate did ends escrow event Print an event from an AID, or specific values ... export Export key events in CESR stream format incept Initialize a prefix init Create a database and keystore interact Create and publish an interaction event introduce Send an rpy /introduce message to recipient wi ... ipex kevers Poll events at controller for prefix list List existing identifiers local location mailbox migrate multisig nonce Print a new random nonce notifications oobi passcode query Request KEL from Witness rename Change the alias for a local identifier rollback Revert an unpublished interaction event at the ... rotate Rotate keys saidify Saidify a JSON file. salt Print a new random passcode sign Sign an arbitrary string ssh status View status of a local AID time Print a new time vc verify Verify signature(s) on arbitrary data version Print version of KLI watcher witness Additional commands will be introduced as they are used in upcoming trainings. [<- Prev (Intro)](101_07_Introduction_to-KERI_ACDC_and_vLEI.ipynb) | [Next (Controllers and Identifiers) ->](101_15_Controllers_and_Identifiers.ipynb) # KERI Core: Controllers, Identifiers, and Key Event Logs
    🎯 OBJECTIVE
    Explain the fundamental KERI concepts of Autonomic Identifiers (AIDs), the Controller entity, and the Key Event Log (KEL).
    Before we dive into creating identifiers and doing operations with the KLI, let's understand two fundamental concepts: **Identifiers** and the **Controller**. ## Autonomic Identifiers (AIDs) Identifiers are a generic term; they exist in many forms, but here we are concerned with digital identifiers. In a general sense, an identifier is a name, label, or sequence of characters used to uniquely identify something within a given context. Identifiers are useful to assign claims to something or to locate a resource. Common examples of identifiers are domain names, email addresses, ID numbers, and so on. KERI identifiers are called **Autonomic Identifiers (AIDs)**. They have properties that give them additional capabilities compared to traditional digital identifiers. Their most important attribute is to maintain a stable identifier over time while their controlling keys may be rotated to keep the identifier secure. There are many different properties of AIDs: - **Universally Unique:** Like standard UUIDs, AIDs are designed to be globally unique without needing a central issuing authority, thanks to their cryptographic foundation. Β  - **Provide asymmetric cryptography features:** Beyond being an identifier, AIDs provide signing and verification capabilities due to being built on public and private key pairs. - **Cryptographically Bound Control:** AIDs are bound to a set of cryptographic key pairs at time of creation, called the **inception event**, and later key pairs from a **rotation event**; this binding forms the basis of their security and allows the holder of the private key(s) to control the identifier and prove that control through digital signatures and a key event log (KEL). - - **Control Over Time:** AIDs are designed for persistent control. The identifier's control history and current authoritative keys are maintained in a verifiable **key event log (KEL)**, allowing anyone to determine the current authoritative keys and verify the control history. This enables keys to be rotated (changed) securely over time without abandoning the identifier itself, even if old keys are compromised. - **Self-Managed:** Unlike traditional identifiers (like usernames or domain names) that rely on central administrators or registries, an AID is managed directly by its owner(s) β€” known as the Controller β€” through cryptographic means (specifically, their private keys). This makes AIDs maximally decentralized, directly controlled by end-users. - **Self-Certifying:** An AID inherently proves its own authenticity. Its validity stems directly from its cryptographic link to its controlling keys, established at its creation, not from an external authority vouching for it. - **Authenticates & Authorizes:** The cryptographic nature of an AID allows its Controller to directly prove their control (authenticate) and grant permissions (authorize actions or access related to the AID) without needing a third-party identity system. - **Multi-Signature Control (Multisig):** An AID does not have to be controlled by only one Controller. KERI supports configurations requiring multiple identifiers, using key pairs held by one or more Controllers, to cooperatively authorize actions. This can involve needing a specific number of signatures (e.g., 3 out of 5) or advanced weighted threshold multi-signature schemes. Β  - **Secure Key Rotation (Pre-rotation):** When keys controlling an AID need to be changed (rotated), KERI uses a highly secure [pre-rotation](https://trustoverip.github.io/tswg-keri-specification/#key-rotationpre-rotation) method. In the inception event and each rotation event, a secure commitment is made to the next set of rotation keys that hides the set of actual next public keys by using digests of each next public key. This means the private-public keypair(s) for the next rotation remains unexposed and secure until they are actually needed, protecting the rotation process itself from attack. Β  - **Identifier Delegation:** A Controller of one AID can securely grant specific, often limited or revokable, authority as a delegator to another AID, the delegate. This is an important capability for scaling signing operations by using many delegated identifiers in parallel. Don't worry if these features raise many questions right now. We will explain the "how" behind them gradually in the sections to come. ## The Controller Role In KERI, the Controller is the entity that holds the private cryptographic key(s) associated with an Autonomic Identifier and is therefore responsible for managing it. This possession of the private key(s) is the source of its authority and control over the AID. Β  So, a Controller is an entity managing their identifiers and the key pairs for those identifiers. Some controller scenarios include: - Personal identity - An individual managing their own digital identity. - Organizational identity - An organization managing its official identifier. - Agentic identity - An autonomous piece of software or device managing its own identifier. - Delegated agentic identity - An autonomous piece of software acting on behalf of a person or organization. Β  - Multisignature identity - A group managing a shared identifier via multi-signature schemes. Participants could be people, organizations, or AI agents. The most important aspect is a Controller has direct access to the private keys the AID is derived from. While the Controller holds authority over the AID, it relies on software to operate and maintain it. In this training, you will first be using the KLI as the Controller’s tool for interacting with and managing AIDs. Later trainings will include using the Signify and KERIA tooling to interact with AIDs. ## Key Event Logs (KELs) - Never Trust, Always Verify The Controller's authority more than a trusted assertion, it is proven using cryptography through a verification process. Remember, KERI is a "never trust, always verify" protocol. No matter what statements a Controller makes, those cannot be relied upon unless they can be cryptographically verified. KERI is a "signed everything" architecture with no shared secrets. This means no bearer tokens like JWT and OAuth have. Instead, KERI uses cryptographic signatures to create trust. The basis of this trust comes from Controllers signing statements with their private key pairs. This means Controllers possess the private keys associated with their AID. They use these keys to sign messages and authorize actions. The association between key pairs and an AID is initially formed by what is called the **inception event**, the first event in a **Key Event Log (KEL)**. Every significant action taken by a Controller regarding their AID, like creating the identifier (inception), changing its keys (rotation), or other interactions, is recorded as a **Key Event** in the KEL. These Key Events are stored sequentially in a **Key Event Log (KEL)**. Think of the KEL as the official history book for an AID. Like a blockchain, a KEL is a hash-chained data structure. ### Key Event Log diagram ```mermaid graph RL %% Define event nodes ICP["Inception πŸ”‘"] ROT1["Rotation 1 πŸ”‘"] ROT2["Rotation 2 πŸ”‘"] IXN1["Interaction 1 βš“"] %% Define backward chaining ROT1 --> ICP ROT2 --> ROT1 IXN1 --> ROT2 %% Optional node styling for UML appearance classDef eventBox fill:#f5f5f5,stroke:#000,stroke-width:1px,rx:5px,ry:5px,font-size:14px; class ICP,ROT1,ROT2,IXN1, eventBox; ``` Here are some details about the KEL * It starts with the AID's "birth certificate" – the **Inception Event**. * Every subsequent authorized change (like a key rotation) is added as a new entry, cryptographically linked to the previous one. Each new event is signed by the keys referred to in the last rotation event, or from the inception event if no rotations have occurred yet. * Anyone can potentially view the KEL to verify the AID's history and current state, but only the Controller(s) can add new, valid events to it. * There may be multiple copies of a KEL; they can be distributed across a network of witnesses, a concept we will dive deeper into later. ## Advanced Control Mechanisms Control in KERI can be quite nuanced including single signature, multiple signature (multisig), and delegation in any given AID. While the Controller ultimately holds authority, they can sometimes grant specific permissions to others through delegation. Furthermore, the Controller responsibility may be shared across multiple controlling parties in a multisig AID. * **Signing vs. Rotation Authority**: A Controller might keep the power to change the AID's keys (rotation authority) but allow another entity (a "custodian") to perform more routine actions like signing messages (signing authority). * **Delegation**: A Controller can grant some level of authority to a completely separate Delegated Identifier. This allows for creating scalable signing infrastructure with delegation hierarchies that can model complex organizational or authority structures. We'll explore these advanced concepts like delegation and multisig configurations in later sections. # Types of Autonomic Identifiers ## Transferable AID A transferable AID may rotate keys and thus may have inception, rotation, and interaction events in its key event log. Most controllers that are not witnesses will use transferable AIDs. Any AID that issues credentials will be a transferable AID. - Example transferable AID: `EIkO4CUmYXukX4auGU9yaFoQaIicfVZkazQ0A3IO5biT` - Notice the 'E' at the start. ## Non-transferable AID A non-transferable AID cannot rotate keys and only ever has one event, the inception event, in its key event log. Use cases for non-transferable AIDs include witnesses, IoT devices, ephemeral identifiers, or anywhere that signing capabilities are needed where rotation capabilities are not. You can visually see the difference between a non-transferable AID and a transferable AID because a non-transferable AID starts with the "B" character as shown here: - `BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha` - Notice the 'B' at the start.
    πŸ“ SUMMARY

    Fundamental KERI concepts:

    • Autonomic Identifiers (AIDs): These are KERI's unique, self-managing digital identifiers. Unlike traditional IDs, they are cryptographically bound to key pairs from creation, are self-certifying (requiring no central authority), and support features like secure key rotation (pre-rotation), multi-signature control, and delegation.
    • Controller: The entity (person, organization, software), or entities in the case of multisig, holding the private key(s) for an AID, giving it the authority to manage the identifier and authorize actions.
    • Key Event Log (KEL): The secure, append-only, hash-chained data structure serving as a verifiable key history for an AID. It records all significant actions (like creation and key rotations) signed by the Controller, allowing anyone to track the identifier's control provenance. A KEL may also store interaction events for anchoring arbitrary data to a KEL, sort of like anchoring data to a blockchain. We will explore this deeply in a future lesson.

    In essence, Controllers use their private keys to manage AIDs, and all authoritative actions are recorded in the KEL.

    [<- Prev (Controllers and Identifiers)](101_10_KERI_Command_Line_Interface.ipynb) | [Next (Working with Keystores and AIDs with the KLI) ->](101_20_Working_with_Keystores_and_AIDs_via_KLI.ipynb) # KLI Operations: Managing Keystores and Identifiers
    🎯 OBJECTIVE
    Demonstrate how to create a KERI keystore and then manage identifiers within it using the kli init, kli incept, and kli list commands.
    ## Initializing Keystores Before you can create identifiers or perform many other actions with KLI, you need a keystore. The keystore is an encrypted data store that holds the keys for your identifiers. To initialize a keystore, you give it a name, protect it with a passcode, and provide a salt for generating the keys. The command to do this is `kli init`. Here's an example:
    πŸ’‘ TIP
  • If you run clear_keri(), the keystore directories are deleted.
  • This function is provided as a utility to clean your data and re-run the notebooks.
  • It will be called at the beginning of each notebook.
  • ```python # Imports and Utility functions from scripts.utils import clear_keri clear_keri() ``` Proceeding with deletion of '/usr/local/var/keri/' without confirmation. ⚠️ Path not found: /usr/local/var/keri/. Nothing to remove. ```python # Choose a name for your keystore keystore_name="my-first-key-store" # Use a strong, randomly generated passcode (using a predefined one here, but can be created with 'kli passcode generate') keystore_passcode="xSLg286d4iWiRg2mzGYca" # Use a random salt (using a predefined one here, but can be created with 'kli salt') keystore_salt="0ABeuT2dErMrqFE5Dmrnc2Bq" !kli init --name {keystore_name} \ --passcode {keystore_passcode} \ --salt {keystore_salt} ``` KERI Keystore created at: /usr/local/var/keri/ks/my-first-key-store KERI Database created at: /usr/local/var/keri/db/my-first-key-store KERI Credential Store created at: /usr/local/var/keri/reg/my-first-key-store aeid: BD-1udeJaXFzKbSUFb6nhmndaLlMj-pdlNvNoN562h3z The command sets up the necessary file structures for your keystore, so once executed, it's ready for you to create and manage Identifiers within it. ![](images/empty-keystore.png)
    ℹ️ NOTE
    • In the example, predefined --passcode and --salt values are used for convenience, but randomly generated values can be obtained using the kli passcode generate and kli salt
    • As mentioned earlier, the passcode is used for encryption of the keystore.
    • The salt value is used as input (along with other context including the keystore's aeid) to deterministically generate all key-pairs belonging to an identifier. In these examples, you'll see many public keys and derivations of those (such as an AID's prefix), that are dependent on the salt value.
    • You can initialize multiple keystores as long as they have different names.
    ## Creating Identifiers (Inception) Now that your keystore is set, you can create your first identifier (AID) within it using the `kli incept` command. You'll need to provide: - `--name` and `--passcode`: Think of it as the keystore access credentials `keystore_name` and `keystore_passcode` - `--alias`: It will be difficult to recall an AID by its value. A human-readable `alias` is assigned using this parameter - `--icount` and `--isith`: the number of signing keys and the signing threshold, respectively. - Other parameters such as `--ncount`, `--nsith`, and `--toad` will be explained later. Executing `kli incept` will create the AID and output the prefix. This also means that the command will add the first event to the AID KEL, the inception event. Proceed and create your first AID: ```python # Choose a human-readable alias for your identifier within this keystore aid_alias = "my-first-aid" # Create (incept) the identifier !kli incept --name {keystore_name} \ --passcode {keystore_passcode} \ --alias {aid_alias} \ --icount 1 \ --isith 1 \ --ncount 0 \ --nsith 0 \ --toad 0 ``` Prefix BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC Public key 1: BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC ![](images/incepted-keystore.png) ## Understanding Prefixes The `kli incept` command generated an AID, which is represented by a unique string, e.g., `BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC`, known as the Prefix. While closely related, they represent different aspects of the identifier: - AID: This is the formal concept of the self-governing identifier, representing the entity and its control. An entity may have multiple AIDs. - Prefix: This is the practical, usable string representation of the AID. It's derived directly from the AID's initial cryptographic keys and is constructed by combining: - A Derivation Code: Indicates the cryptographic suite (key type, signature algorithm, hashing algorithm) used. - The Encoded Public Key: A string derived from the public portions of the initially generated key pairs associated with the AID. **Prefix Self-Certification:** KERI AIDs are [self-certifying](https://trustoverip.github.io/tswg-keri-specification/#self-certifying-identifier-scid) in the sense that an AID does not rely on a trusted entity and instead relies only on its public keys to provide verifiability for signed statements made by the controller of an AID. This self-certifying quality holds throughout the AID's lifecycle, from inception to all future events, such as rotations. This works because: 1. The identifier's prefix is derived from the set of public keys that are included in the inception event. The prefix is the self addressing identifier (SAID), a kind of digest, of the inception event. This provides a strong cryptographic binding between the AID prefix and the keys used to generate the inception event. 2. As validated rotation events occur, the AID's KEL is appended, which changes its effective Key-State. Key-State is a set of data that is time-dependent and includes the valid public signing keys and related data at a given point in time. The Key-State at a given point in time is derived from the KEL. 3. Given any signed statement made by the AID controller, the set of public keys and related data from the Key-State as of the time of that signed statement is sufficient to verify its authenticity. Because of this relationship between keypairs, the inception event, and the key event log, anyone who has the prefix and the KEL can cryptographically verify signatures created by a given AID with the matching private keys (i.e., by the AID's controller) from any given point in the history of a KEL. This verifiability establishes authenticity for all actions taken by an AID without needing to check with outside authorities or registries, meaning they are self-certifying. ### Security precaution for live transactions **Keep in mind, as a security precaution**, signature verification with a prefix and a KEL is most securely done with the most recent keys that are currently authorized for the AID, as in the latest key-state (i.e., set of keys given the inception and all rotations). Key rotation changes the authorized keys, requiring reference to the AID's KEL for up-to-date verification. Historical signatures may still be verified, yet to ensure proper security during a live transaction the latest controlling keypairs (pubic portions) should always be used for signature verification. This means signatures from old keypairs, during a live transaction, should always be rejected when verifying signatures of an in-progress transaction. Such an approach is appropriate because there is no way to know if an attacker has compromised old keypairs and is using old keys to sign the new transaction events. To adopt the highest security posture then usage of the latest keypairs according to the KEL should **always** be required.
    πŸ“ SUMMARY
  • The AID is the secure, self-managed identifier
  • The prefix is the actual text string you use to represent that AID, whose structure makes the AID's self-certifying property work
  • The alias (my-first-aid in our example) is just a local nickname within your keystore to easily refer to the prefix
  • The terms AID, identifier, prefix, and alias tend to be used interchangeably
  • ℹ️ NOTE
    As you may have figured out, most of the kli commands require a keystore. Assume from now on that --name and --passcode refer to the keystore access.
    ## Displaying Identifier Status You can check the status of the identifier you just created using `kli status` and its `alias`. This command will show details about the AID's current state, including its Alias, prefix, sequence number, public keys, and additional information. More details on what all this data means will be explained later ```python # Check the status of the AID using its alias !kli status --name {keystore_name} \ --passcode {keystore_passcode} \ --alias {aid_alias} ``` Alias: my-first-aid Identifier: BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC Seq No: 0 Witnesses: Count: 0 Receipts: 0 Threshold: 0 Public Keys: 1. BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC ## Displaying Key Event Logs (KELs) You can use `kli status` with the `--verbose` parameter to show the key event log. ```python !kli status --name {keystore_name} \ --passcode {keystore_passcode} \ --alias {aid_alias} \ --verbose ``` Alias: my-first-aid Identifier: BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC Seq No: 0 Witnesses: Count: 0 Receipts: 0 Threshold: 0 Public Keys: 1. BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC Witnesses: { "v": "KERI10JSON0000fd_", "t": "icp", "d": "EG23dnLAUA4ywPcu2qbokplb2cb1XlIOw24iIKYtR3v4", "i": "BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC", "s": "0", "kt": "1", "k": [ "BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC" ], "nt": "0", "n": [], "bt": "0", "b": [], "c": [], "a": [] } Here are some descriptions of the KEL fields (see the [spec](https://trustoverip.github.io/tswg-keri-specification/#keri-data-structures-and-labels)): - `v`: Version String - `t`: Message type (`icp` means inception) - `i`: AID Prefix that created the event ("issuer" of the event) - `s`: sequence number of the event, always zero for the inception event since it is the first event - `kt`: Keys Signing Threshold (the `isith` value used in `kli inception`) - `k`: List of public keys that are Signing Keys (You get as many keys as defined by the `icount` value used in `kli inception`) - `nt`: Next Signing Threshold (rotation signing threshold), zero in this case. This will be explored in an upcoming lesson. - `n`: List of public key **digests** that are rotation keys authorized to perform rotations. Since there are no rotation keys specified here then this identifier may never rotate and may be considered to have rotated to "null" on its first event, meaning it can only ever be used for signing. - `bt`: Backer (witness) Threshold - the number of backer (witness) receipts the event must have in order to be considered accepted by the controller and valid. - `b`: Backer (witness) list - the AID prefixes of the backers (witnesses) that are authorized by the controller to generate witness receipts for this event and any after it, until changed by a rotation event. - `c`: configuration traits - not used here - `a`: anchors (seals) - list of field maps used to anchor data in a key event
    πŸ“š REFERENCE
    To see the full details of the key event fields, refer to KERI Data Structures and Labels
    ## Listing Identifiers in a Keystore You can also list all the identifiers managed within this keystore. To illustrate this, let's create an additional Identifier ```python !kli incept --name {keystore_name} \ --passcode {keystore_passcode} \ --alias "my-second-aid" \ --icount 1 \ --isith 1 \ --ncount 0 \ --nsith 0 \ --toad 0 ``` Prefix BBuVNJvbJD2WNduQ0JUGRVGb6uKYrF5bO5T4gdGt_ezO Public key 1: BBuVNJvbJD2WNduQ0JUGRVGb6uKYrF5bO5T4gdGt_ezO Now use `kli list` to list all the identifiers managed by the keystore ```python # List all Identifiers in the keystore !kli list --name {keystore_name} --passcode {keystore_passcode} ``` my-second-aid (BBuVNJvbJD2WNduQ0JUGRVGb6uKYrF5bO5T4gdGt_ezO) my-first-aid (BHt9Kw8oUgfB2kiyoj65B2VE5fZLr87S5MJP3l4JeRwC) ![](images/two-aids.png)
    πŸ“ SUMMARY

    The basics of managing KERI identifiers using the KLI:

    • Keystore Creation: A keystore, essential for managing identifiers, is created using kli init, requiring a name, passcode, and salt
    • Identifier Inception: New identifiers (AIDs) are created within a named keystore using kli incept, which also starts their Key Event Log (KEL)
    • Key Event Log (KEL): The KEL tracks an AID's history with fields like version (v), event type (t), identifier prefix (i), signing threshold (kt), and keys (k)
    • Displaying identifiers:kli status displays an AID information and the KEL
    • Listing Identifiers: The kli list command displays all identifiers managed within a specific keystore
    [<- Prev (Controllers and Identifiers)](101_15_Controllers_and_Identifiers.ipynb) | [Next (Signatures) ->](101_25_Signatures.ipynb) # Digital Signatures in KERI
    🎯 OBJECTIVE
    Explain digital signatures, how to verify a digital signature using the KLI verify command, and understand how tampering affects signature validity.
    ## Fundamentals of Digital Signatures Having explored KERI Identifiers (AIDs) and their management, we now focus on digital signatures. This section explains what digital signatures are, their crucial properties, and how they operate within KERI. A digital signature is a cryptographic mechanism used to provide assurance about the authenticity and integrity of digital data. It serves a similar purpose to a handwritten signature but offers significantly stronger guarantees through cryptography. The process involves two stages: 1. **Signing:** (see SIGNING PROCESS diagram below) * The signer (e.g., an AID Controller) begins with the information they want to sign. * They then create a condensed, fixed-length representation of that information β€” called a digest β€” by applying a hash function. * A note on terminology: While the term "hash" is often used to refer to both the function and its output, in this text we will use β€œhash function” to refer to the algorithm and β€œdigest” to refer to its output. * Next, the signer uses their unique private signing key to apply a digital signature algorithm to the digest. This process produces a digital signature β€” a cryptographic proof that the signer authorized the original data. * Only someone with access to the private key can generate a valid signature for a given digest. * The generated signature is typically attached to the original information. In the case of KERI this signature is encoded in the [Composable Event Streaming Representation](https://trustoverip.github.io/tswg-cesr-specification/) (CESR) encoding format. ```mermaid graph TD subgraph "COLOR LEGEND" L1["πŸ“Š Input/Output Data"] L2["βš™οΈ Algorithms/Functions"] L4["πŸ” Private Key"] L5["πŸ”‘ Public Key"] end style L1 fill:#e3f2fd style L2 fill:#f3e5f5 style L4 fill:#ffcdd2 style L5 fill:#bbdefb ``` ```mermaid graph TD subgraph "SIGNING PROCESS" A["Original Message: 'Transfer $1000 to Alice'"] --> B["Hash Function: e.g., SHA-256"] B --> C["Message Digest: '0x3b7e72...'"] C --> D["Signing Algorithm"] E["Private Key πŸ”"] --> D D --> F["Digital Signature"] F --> G["Signature + Message"] A -.-> G end G --> H["πŸ“€ Transmitted over network"] style A fill:#e3f2fd style B fill:#f3e5f5 style C fill:#e3f2fd style D fill:#f3e5f5 style E fill:#ffcdd2 style F fill:#e3f2fd style G fill:#e3f2fd style H fill:#f5f5f5 ``` 2. **Verification:** * Anyone receiving the information and signature can verify its validity using the signer's corresponding public key. * The verifier applies a verification algorithm using the original information, the signature, and the corresponding public key from the correct point in history of a KEL. * This algorithm is the complement of the signing process. It uses the public key to mathematically validate that the signature corresponds to the digest of the raw information. The verification algorithm applies mathematical operations (which vary by signature scheme) to confirm the signature was created with the corresponding private key for the given digest. If the mathematical verification succeeds, the signature is valid; otherwise, it fails. * **Outcome:** * **Valid Signature:** If the signature verification succeeds, the verifier has high confidence in the information's authenticity, integrity, and non-repudiability and can trust the data and its originator. * **Invalid Signature:** If the signature fails verification the information may have been tampered with, the signature might be corrupt, or the legitimate holder of the private key didn't generate it. Thus the verifier should not trust the data. * Successful verification confirms: * **Authenticity:** The information originated from the owner of the key pair. * **Integrity:** The information has not been altered since it was signed. * **non-repudiability**: The signer cannot successfully deny signing the information. Because generating the signature requires the private key (which should be kept secret by the owner), a valid signature serves as strong evidence of the signer's action. ```mermaid graph TD subgraph "VERIFICATION PROCESS" H["πŸ“₯ Received Message + Signature"] --> I["Separate Components"] I --> J["Original Message: 'Transfer $1000 to Alice'"] I --> K["Received Signature"] J --> L["Hash Function: e.g., SHA-256"] L --> M["Computed Digest: '0x3b7e72...'"] M --> N["Verification Algorithm"] K --> N O["Public Key πŸ”‘"] --> N N --> P{Valid?} P -->|Yes| Q["βœ… Authentic, Unmodified, Non-repudiable"] P -->|No| R["❌ Untrusted, Possibly tampered, or wrong key"] end style H fill:#e3f2fd style I fill:#f3e5f5 style J fill:#e3f2fd style K fill:#e3f2fd style L fill:#f3e5f5 style M fill:#e3f2fd style N fill:#f3e5f5 style O fill:#bbdefb style P fill:#f5f5f5 style Q fill:#c8e6c9 style R fill:#ffcdd2 ``` ## Verification Process in KERI In KERI, digital signatures are fundamental for establishing trust and verifying the authenticity of Key Events and other interactions associated with an AID. They cryptographically link actions and data back to the identifier's controlling keys. While the verification algorithm is standard, the key challenge for a Verifier is obtaining the correct public key(s) that were authoritative for the AID when the information was signed. The Verifier must perform these steps: 1. **Identify the Authoritative Public Key(s):** * For an AID's inception event, the AID prefix is derived from the initial public key(s) (leveraging KERI's self-certifying nature). * For subsequent events (like rotations or interactions), the Verifier must consult the AID's Key Event Log to get the most up to date controlling key pair(s). The KEL provides the history of key changes, allowing the Verifier to determine which public key(s) were valid at the specific point in time the event or message was signed. 2. **Perform Cryptographic Verification:** * Once the correct public key(s) are identified, the Verifier uses them, along with the received data and signature, in the standard cryptographic verification algorithm (as described earlier). This reliance on the KEL to track key state over time is crucial for maintaining the security of interactions with KERI identifiers long after their initial creation.
    ℹ️ NOTE
    There's a subtle difference between a Verifier (who checks cryptographic correctness according to KERI rules) and a Validator (who might perform broader checks, including business logic, and broader trust policies in addition to verification). In KERI discussions, "Verifier" typically emphasizes the cryptographic checks.
    ## KLI Examples: Signing and Verifying Let's see how signing and verification work using the KLI commands. ### Initial Setup First, create a keystore and an identifier. ```python # Imports and Utility functions from scripts.utils import clear_keri clear_keri() keystore_name="signature-keystore" passcode="xSLg286d4iWiRg2mzGYca" salt="0ABeuT2dErMrqFE5Dmrnc2Bq" aid_alias = "aid-signature" !kli init --name {keystore_name} \ --passcode {passcode} \ --salt {salt} !kli incept --name {keystore_name} \ --passcode {passcode} \ --alias {aid_alias} \ --icount 1 \ --isith 1 \ --ncount 0 \ --nsith 0 \ --toad 0 ``` 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/signature-keystore KERI Database created at: /usr/local/var/keri/db/signature-keystore KERI Credential Store created at: /usr/local/var/keri/reg/signature-keystore aeid: BD-1udeJaXFzKbSUFb6nhmndaLlMj-pdlNvNoN562h3z Prefix BCtRkWLNdWNRvB8L5gYMaLkanJQWi8wGbmmAtEw9XSWw Public key 1: BCtRkWLNdWNRvB8L5gYMaLkanJQWi8wGbmmAtEw9XSWw ### Signing Data Now, sign a simple text message using the private key associated with the `aid-signature` identifier. To do so use the command `kli sign` presented below: ```python !kli sign --name {keystore_name} \ --passcode {passcode} \ --alias {aid_alias} \ --text "hello world" ``` 1. AABjrlljacVpT8kDsvzv3qCVR1iiwJ-XPaAiKDURCH_vdrkgJgLK4i9h2Qv-xxmT2UxCSif0C-Ovvx-xp2vVDJUB The output is the digital signature generated for the text "hello world" using the private key of the AID. This digital signature is encoded in text format with the CESR encoding protocol, the core cryptographic primitive, text, and binary encoding protocol used in the KERI and ACDC protocols. ### Verifying a Valid Signature You can now use the `kli verify` command to check if the signature is valid for the given message and identifier (prefix). The relevant parameters here are: - `--prefix`: The prefix of the signer - `--text`: original text - `--signature`: signature to verify ```python !kli verify --name {keystore_name} \ --passcode {passcode} \ --alias {aid_alias} \ --prefix BCtRkWLNdWNRvB8L5gYMaLkanJQWi8wGbmmAtEw9XSWw \ --text "hello world" \ --signature AABjrlljacVpT8kDsvzv3qCVR1iiwJ-XPaAiKDURCH_vdrkgJgLK4i9h2Qv-xxmT2UxCSif0C-Ovvx-xp2vVDJUB ``` Signature 1 is valid. The command confirms the signature is valid. It used the public key associated with the prefix to verify the signature against the provided text. ### Impact of Tampering What happens if the signature is altered even slightly? The next command has the last character of the signature modified from "B" to "C" which will cause verification to fail. Try to verify again. ```python !kli verify --name {keystore_name} \ --passcode {passcode} \ --alias {aid_alias} \ --prefix BCtRkWLNdWNRvB8L5gYMaLkanJQWi8wGbmmAtEw9XSWw \ --text "hello world" \ --signature AABjrlljacVpT8kDsvzv3qCVR1iiwJ-XPaAiKDURCH_vdrkgJgLK4i9h2Qv-xxmT2UxCSif0C-Ovvx-xp2vVDJUC # Tampered last character ``` ERR: Signature 1 is invalid. As expected, the verification fails. Even a tiny change invalidates the signature, demonstrating the integrity protection it provides.
    πŸ“ SUMMARY
    • Digital Signature Process: Data is signed by first creating a condensed representation (a digest) using a hash function, and then applying a signing algorithm to that digest using a private key. The result is a digital signature.
    • Verification: To verify a signature, the recipient uses the signer's public key and the signature to validate the digest. They also compute the digest of the received data independently. If the two digests match, the signature is valid.
    • Core Guarantees: A valid digital signature confirms authenticity (the message came from the key owner), integrity (the message wasn't altered), and non-repudiability (the signer cannot deny their action).
    • KERI's Key Management: In KERI, the crucial step for a verifier is finding the correct public key that was authoritative at the time of signing. This is accomplished by consulting the identifier's Key Event Log (KEL), which provides the secure, verifiable history of key changes.
    • Practical Demonstration: The kli sign command generates a signature, while kli verify checks it. Even a minor alteration to the signature or the original data will cause the verification to fail, demonstrating the cryptographic security of the process.
    [<- Prev (Working with Keystores and AIDs via KLI)](101_20_Working_with_Keystores_and_AIDs_via_KLI.ipynb) | [Next (Rotation) ->](101_30_Key_Rotation.ipynb) # Key Rotation and Pre-rotation
    🎯 OBJECTIVE
    Understand the importance of key rotation, learn about the pre-rotation mechanism, and see how to execute and verify a rotation using KLI commands.
    ## Importance of Key Rotation Key rotation in a scalable identity system while the identifier remains stable is the hard problem from cryptography and distributed systems that KERI solves. The need to rotate keys guided the entire design of KERI and deeply impacted the vLEI system architecture. This is because securing identity and data involves more than just signing data; robust long-term security for an identity and any data it signs relies on key rotation. The ability to rotate keys is a fundamental security practice that involves changing over time the cryptographic keys associated with an identifier. Rotating keys is not just about changing them arbitrarily; it's a crucial practice for several reasons: - **Security Hygiene and Limiting Exposure:** Keys used frequently are more exposed to potential compromise (e.g., residing in memory). Regularly rotating to new keys limits the time window an attacker has if they manage to steal a current key - **Cryptographic Agility:** Cryptographic algorithms evolve. Vulnerabilities are found in older ones, and stronger new ones emerge (like post-quantum algorithms). Key rotation allows an identifier to smoothly transition to updated cryptography without changing the identifier itself - **Recovery and Delegation:** You might need to recover control of an identifier if the current keys are lost or compromised, or delegate authority to another entity. Both scenarios typically involve establishing new keys, which is achieved through rotation events ## Understanding Establishment Events Before diving into key rotation, it's helpful to explain Establishment Events. Not all events recorded in a KEL are the same. Some events specifically define or change the set of cryptographic keys that are authorized to control an identifier (AID) at a particular point in time. These crucial events are called Establishment Events. The two primary types are: Β  - **Inception Event (icp):** The very first event that creates the AID and establishes its initial controlling keys - **Rotation Event (rot):** An event that changes the controlling keys from the set established by the previous Establishment Event to a new set These Establishment Events form the backbone of an AID's security history, allowing anyone to verify which keys had control at what time. Other event types exist (like interaction events), but they rely on the authority defined by the latest Establishment Event. Interaction events rely on the signing authority of the keys referenced in the latest establishment event. ## The Pre-Rotation Mechanism KERI utilizes a strategy called pre-rotation, which decouples the act of key rotation from the preparation for it. With pre-rotation, the cryptographic commitment (a digest of the public keys) for the next key set is embedded within the current key establishment event. This means the next keys can be generated and secured in advance, separate from the currently active operational keys. This pre-commitment acts as a safeguard, as the active private key doesn't grant an attacker the ability to perform the next rotation, as they won't have the corresponding pre-committed private key.
    ℹ️ NOTE
    A potential question arises: "If the next keys are kept in the same place as the active operational keys, doesn't that defeat the purpose?" Pre-rotation enables stronger security by decoupling preparation from rotation, but realizing this benefit depends on sound operational practices. Specifically, the pre-committed keys must be stored more securely than the active ones. KERI provides the mechanism; effective key management brings it to life.
    ## Performing Key Rotation with KLI Next, you will complete a key rotation example. Start by setting up a keystore and an identifier. ```python # Imports and Utility functions from scripts.utils import clear_keri clear_keri() keystore_name="rotation-keystore" keystore_passcode="xSLg286d4iWiRg2mzGYca" salt="0ABeuT2dErMrqFE5Dmrnc2Bq" # Alias for non-transferable AID aid_alias_non_transferable = "aid-non-transferable" # Initialize the keystore !kli init --name {keystore_name} --passcode {keystore_passcode} --salt {salt} # Incept the AID !kli incept --name {keystore_name} \ --passcode {keystore_passcode} \ --alias {aid_alias_non_transferable} \ --icount 1 \ --isith 1 \ --ncount 1 \ --nsith 1 \ --toad 0 ``` 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/rotation-keystore KERI Database created at: /usr/local/var/keri/db/rotation-keystore KERI Credential Store created at: /usr/local/var/keri/reg/rotation-keystore aeid: BD-1udeJaXFzKbSUFb6nhmndaLlMj-pdlNvNoN562h3z Prefix BEG5uWt6xB94bIkdGUCjYcBf_ryDgPa7t1GUtVc7lerw Public key 1: BEG5uWt6xB94bIkdGUCjYcBf_ryDgPa7t1GUtVc7lerw Now, attempt to rotate the keys for this AID, using the command `kli rotate`. You will see an error message ```python !kli rotate --name {keystore_name} --alias {aid_alias_non_transferable} --passcode {keystore_passcode} ``` ERR: Attempt to rotate nontransferable pre=BEG5uWt6xB94bIkdGUCjYcBf_ryDgPa7t1GUtVc7lerw. The error message says we tried to rotate a nontransferable prefix. What does this mean? ### Transferable vs. Non-Transferable AIDs Not all KERI identifiers are designed to have their keys rotated. By default, `kli incept` creates a non-transferable identifier. Here is the difference: **Non-Transferable AID:** - Key rotation is not possible. Think of it as a fixed set of keys for an identifier. - Its control is permanently bound to the initial set of keys established at inception. - The prefix is derived from these initial keys. - As a special case, when only a single key pair was used to create a non-transferable AID the public key is directly derivable from the AID prefix itself. - This is useful for use cases where you want to avoid sending KELs of non-transferable AIDs and instead infer the one-event KEL and associated public key from the AID. **Transferable AID:** - Key rotation is possible. - Its control can be transferred (rotated) to new sets of keys over time. - It uses the pre-rotation mechanism, committing to the next set of keys in each rotation event. - The prefix is derived from the initial keys. Although authoritative keys will change upon each rotation the prefix will remain the same. This allows the identifier to remain stable even as its underlying controlling keys change. How does KERI know the difference? The difference lies in the parameters set during the AID's inception event. Let's look at the inception event data for the non-transferable AID we just created: ```python !kli status --name {keystore_name} --alias {aid_alias_non_transferable} --passcode {keystore_passcode} --verbose ``` Alias: aid-non-transferable Identifier: BEG5uWt6xB94bIkdGUCjYcBf_ryDgPa7t1GUtVc7lerw Seq No: 0 Witnesses: Count: 0 Receipts: 0 Threshold: 0 Public Keys: 1. BEG5uWt6xB94bIkdGUCjYcBf_ryDgPa7t1GUtVc7lerw Witnesses: { "v": "KERI10JSON0000fd_", "t": "icp", "d": "EC8pCWrNEdrLD64K1Z7qlYQp7mp6Dq7n30Ze6ElP49pO", "i": "BEG5uWt6xB94bIkdGUCjYcBf_ryDgPa7t1GUtVc7lerw", "s": "0", "kt": "1", "k": [ "BEG5uWt6xB94bIkdGUCjYcBf_ryDgPa7t1GUtVc7lerw" ], "nt": "0", "n": [], "bt": "0", "b": [], "c": [], "a": [] } Look closely at the JSON output at the end (representing the inception event). You'll find these key fields: - `"nt": "0"`: The threshold required to authorize the next key set is zero. - `"n": []`: The list of digests for the next public keys is empty. These two fields mark the AID as non-transferable. No commitment to future keys was made. ### Incepting and Rotating a Transferable Identifier To enable key rotation, we need to explicitly create a transferable AID using the `--transferable` option during inception and using `--ncount` and `--nsith` equal to 1 (or greater). This tells KLI to: - Generate not just the initial keys, but also the next set of keys (pre-rotated keys). - Set the appropriate nt (Next Key Signing Threshold, defined by `nsith`) in the inception event. - Include the digests of the next public keys in the n field of the inception event. Now create a transferable AID: ```python # Alias for our transferable AID aid_alias_transferable = "aid-transferable" # Create the identifier WITH the --transferable flag !kli incept --name {keystore_name} \ --passcode {keystore_passcode} \ --alias {aid_alias_transferable} \ --icount 1 \ --isith 1 \ --ncount 1 \ --nsith 1 \ --toad 0 \ --transferable ``` Prefix EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM Public key 1: DOdymqdtGJzeoRRSL9C8Suni6ebPaSqQfuEUM_JFkPQx Now, check its status and inception event: ```python !kli status --name {keystore_name} \ --passcode {keystore_passcode} \ --alias {aid_alias_transferable} \ --verbose ``` Alias: aid-transferable Identifier: EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM Seq No: 0 Witnesses: Count: 0 Receipts: 0 Threshold: 0 Public Keys: 1. DOdymqdtGJzeoRRSL9C8Suni6ebPaSqQfuEUM_JFkPQx Witnesses: { "v": "KERI10JSON00012b_", "t": "icp", "d": "EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM", "i": "EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM", "s": "0", "kt": "1", "k": [ "DOdymqdtGJzeoRRSL9C8Suni6ebPaSqQfuEUM_JFkPQx" ], "nt": "1", "n": [ "EO95Pwm8WYG_dIS2-H6LGoXmzOEEnbRljeIjy-Hd7aVx" ], "bt": "0", "b": [], "c": [], "a": [] } Compare the JSON output for this transferable AID's inception event with the previous one. You'll notice key differences: - `"nt": "1"` the next Key Signing Threshold is now 1 - `"n": ["EO95Pwm8WYG_dIS2-H6LGoXmzOEEnbRljeIjy-Hd7aVx"]` The presence of a key digest means that this AID is transferable and has pre-rotated keys ready. ### Performing the Rotation With the commitment to the next keys in place, we can now successfully rotate the key of the transferable AID. ```python !kli rotate --name {keystore_name} \ --passcode {keystore_passcode} \ --alias {aid_alias_transferable} ``` Prefix EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM New Sequence No. 1 Public key 1: DOkM4enfZoc7w8oVdkXzRaVoCdz8f9aAm2u4kA5CHNcQ ### Examining the Rotation (rot) Event The kli rotate command performed the key rotation by creating and signing a new establishment event of type `rot`. Let's examine the state of the AID after the rotation: ```python !kli status --name {keystore_name} \ --passcode {keystore_passcode} \ --alias {aid_alias_transferable} \ --verbose ``` Alias: aid-transferable Identifier: EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM Seq No: 1 Witnesses: Count: 0 Receipts: 0 Threshold: 0 Public Keys: 1. DOkM4enfZoc7w8oVdkXzRaVoCdz8f9aAm2u4kA5CHNcQ Witnesses: { "v": "KERI10JSON00012b_", "t": "icp", "d": "EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM", "i": "EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM", "s": "0", "kt": "1", "k": [ "DOdymqdtGJzeoRRSL9C8Suni6ebPaSqQfuEUM_JFkPQx" ], "nt": "1", "n": [ "EO95Pwm8WYG_dIS2-H6LGoXmzOEEnbRljeIjy-Hd7aVx" ], "bt": "0", "b": [], "c": [], "a": [] } { "v": "KERI10JSON000160_", "t": "rot", "d": "EMZIjwx8mBQpTbKa4q-daoxu0Rv5oX-KR0Q3JbQOJG3Z", "i": "EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM", "s": "1", "p": "EAv3ajpSbn807a-HSPuDZm0PFzr6jn58m306dibjrxwM", "kt": "1", "k": [ "DOkM4enfZoc7w8oVdkXzRaVoCdz8f9aAm2u4kA5CHNcQ" ], "nt": "1", "n": [ "EJ9DtlVWW6TKPU0AcXBhx3YYDR5FuF9zXqJQqmqJngU8" ], "bt": "0", "br": [], "ba": [], "a": [] } Observe the following changes in the output: - **Event Type (t):** The latest event shows `"t": "rot"`, indicating it's a rotation event. - **Digest said (d):** This is the digest of the event block. - **Sequence Number (s):** The `s` value in the latest event has incremented (from "0" to "1"). Each rotation event increases the sequence number. - **Current Keys (k):** The public key(s) listed in the `k` field of the latest event have changed. They are revealed as public keys instead of the digest previously listed in the `n` field of the inception event. The previously committed pre-rotated keys are now the active signing keys. - **Next Keys Signing Threshold (nt):** Is 1, as defined by the `--nsith` parameter during inception - **New Next Keys (n):** The `n` field in the rotation event contains a new key digest. The rotation process automatically generated the next set of keys for the next potential rotation and committed them. - **Prefix (i):** has not changed. **Understanding the rot Event** - A `rot` event is an Establishment Event. Like the inception (`icp`) event, it defines the authoritative key state of an AID at a specific point in its history (sequence number). - Its primary function is to change the key state. It transitions control from the keys established in the previous establishment event to the keys that were pre-rotated (committed to via the n field) in that previous event. - It simultaneously establishes the commitment (n field and nt threshold) for the next rotation cycle. - This chaining of events (icp -> rot -> rot -> ...) forms the Key Event Log, and the ability to verify this log using receipts from witnesses is a fundamental concept within KERI. You have now successfully rotated the keys for a transferable KERI identifier!
    πŸ“ SUMMARY

    Key rotation is essential for security hygiene, cryptographic agility, and enabling recovery or delegation. KERI uses a "pre-rotation" strategy where the commitment (digest) for the next set of keys is included in the current key establishment event (`icp` or `rot`). This secures the rotation process even if the currently active key is compromised.

    Performing a rotation (kli rotate) creates a rot event, increments the sequence number, activates the previously pre-rotated keys (revealing them in the k field), and commits to a new set of keys (digest in the n field), all while keeping the AID prefix unchanged. This chained process forms part of the Key Event Log (KEL).
    [<- Prev (Signatures)](101_25_Signatures.ipynb) | [Next (Modes, OOBIs, and Witnesses) ->](101_35_Modes_oobis_and_witnesses.ipynb) # KERI Infrastructure: Modes, OOBIs, and Witnesses
    🎯 OBJECTIVE
    Explain KERI's Direct and Indirect modes and the key components enabling Indirect Mode: Out-of-Band Introductions (OOBIs) for discovery, Mailboxes for asynchronous communication, Witnesses for availability and consistency, and the Threshold of Accountable Duplicity (TOAD) for defining signing thresholds.
    ## Operational Modes: Direct and Indirect KERI provides a secure way to manage identifiers and track control using verifiable logs of key events (KEL). How these logs are shared and verified between the controller and someone verifying that identifier depends on one of the two operational modes: Direct and Indirect. ### Direct Mode Direct Mode is a controller-to-controller communication approach, similar to a direct conversation, or like making an HTTP request from a client to a server. In this mode the source controller shares their Key Event Log directly with a destination controller through an HTTP or TCP request. The destination controller acts as a validator by verifying the KEL events and their signatures to ensure integrity. The destination controller can choose to establish trust based solely on verifying the signatures of the source controller on its KEL. This is a lower security posture than relying on a watcher network, yet may be an appropriate choice for a use case. It is also a simple way to start using KERI and allows quick bootstrapping of nodes in a system because validators directly receive and verifies the KEL. This mode is an option for interactions where both parties can connect directly, even if only occasionally, and need to be online to exchange new events or updates.Β 
    🧩 DID YOU KNOW?

    Future Note: Watcher Networks for Direct Mode Verification Thresholds

    While watchers are not yet widely used in the KERI ecosystem landscape, using a watcher network to set a verification threshold is one way to increase the security of a direct mode installation. A watcher or watcher network may be used by the validating controller to compare the KEL being received from the source controller with the view of the KEL that the watcher network has. This is similar to how verifier nodes in distributed consensus systems, like a blockchain, will verify block history with multiple nodes prior to accepting a new block.
    #### Example of Direct Mode The vLEI Reporting API component called [sally](https://github.com/GLEIF-IT/sally) is a direct mode validator component that receives credential presentations in the vLEI ecosystem. It receives KELs, ACDCs (credentials) directly from a presenter, verifies them, and validates them. #### Direct Mode Wrap up Although we haven't done any interaction so far, all the things we have done until this point fit within the direct mode approach. ### Indirect Mode Indirect Mode is the asyncronous approach leveraging mailboxes for communication and witnesses for highly-available KELs, similar to using a public bulletin board instead of direct messaging. It’s for scenarios where the controller may be sometimes offline or needs to serve many validators at once. Rather than relying on direct communication, it introduces infrastructure to both allow a controller to receive messages while offline, the mailbox, and to make the KEL reliably accessible from witnesses. Verifiability extends beyond the controller’s signature to signed event receipts produced by witnesses, called witness receipts. This additional verification capability relies on a network of Witnesses, chosen by the controller, that verify, return signed receipts of, and store key events. When combined with the two factor authentication (2FA) capability then witnesses increase the security of an AID. This mode is ideal for public identifiers used from mobile devices and web browsers, one-to-many interactions, or any situation where the controller can’t be constantly online. #### Indirect Mode Wrap Up Most elements of the KERI ecosystem use indirect mode. Unless you know you need direct mode then you should be using indirect mode as your default. ## OOBIs: Discovery Mechanism When an AID controller is operating in either mode, you need a way to tell others where they can find information about it, like its Key Event Log (KEL) or the schema of an ACDC. This is where Out-of-Band Introductions (OOBIs) come in. They function as an address of the way to communicate with a controller or to retrieve a resource. **What is an OOBI?** An OOBI is a **discovery mechanism** used in KERI used to discover controllers or resources. Its primary uses are to link a specific KERI AID to a network location (a URL or URI) where information about that identifier can potentially be found and also to declare the location a resource is hosted such as a JSON Schema document for an ACDC or a CESR stream for a well-known credential. ### Example OOBI The simplest form of an OOBI pairs a SAID, either an AID or the SAID of a document, with a URL. For example: `("http://8.8.5.6:8080/oobi", "EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM", "controller")` This OOBI suggests that controller information related to the AID `EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM` might be available at the service endpoint `http://8.8.5.6:8080/oobi`. The URL representation may be one of any of the following: Blind OOBI (no AID at the end) interpreted as a controller OOBI: - `http://8.8.5.6:8080/oobi` Controller OOBI with no role: - `http://8.8.5.6:8080/oobi/EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM` Controller OOBI with the specific role at the end: - (`http://8.8.5.6:8080/oobi/EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM/controller` ### Kinds of OOBIs There are four similar kinds of OOBIs: controller OOBIs, witness OOBIs, agent OOBIs, and data OOBIs. For controller OOBIs there are three variants: the blind OOBI, the no-role OOBI, and the full OOBI. #### Controller OOBI A controller OOBI is a service endpoint that a controller uses to advertise where its KEL may be retrieved from and where it may receive data. This is typically used by a witness or a direct mode agent. When witnesses are declared in an inception event they will typically have had their controller OOBI resolved. Examples: - Blind OOBI: `http://8.8.5.6:8080/oobi` - AID and no role: `http://8.8.5.6:8080/oobi/EaU6JR2nmwyZ-i0d8JZAoTNZH3ULvYAfSVPzhzS6b5CM` - AID and role: `http://10.0.0.1:9823/oobi/ECLwKe5b33BaV20x7HZWYi_KUXgY91S41fRL2uCaf4WQ/controller` #### Witness OOBI A witness OOBI is a service endpoint authorized and used by a controller to designate a witness as a mailbox for a given controller. It means that the witness runs a mailbox that receives messages on behalf of a controller so that the controller may poll for and receive messages when it comes back online. They look like this: - `http://10.0.0.1:5645/oobi/EA69Z5sR2kr-05QmZ7v3VuMq8MdhVupve3caHXbhom0D/witness/BM35JN8XeJSEfpxopjn5jr7tAHCE5749f0OobhMLCorE` This OOBI means that the controller with AID `EA69Z5sR2kr-05QmZ7v3VuMq8MdhVupve3caHXbhom0D` is using the witness with AID `BM35JN8XeJSEfpxopjn5jr7tAHCE5749f0OobhMLCorE` as its mailbox. #### Agent OOBIs An Agent OOBI, used in the KERIA multitenant agent server, is similar to a witness OOBI in that it is a service endpoint authorized and used by a controller to designate an agent as a mailbox for a controller. Where an Agent OOBI differs from a witness OOBI is that an agent OOBI also indicates which specific agent was authorized to act as an agent for a given Signify Controller. It looks like this: - `http://keria2:3902/oobi/ECls3BaUOAtZNO3Ejb4zCv-fybh_hk3iNQMZJVdItr5W/agent/EAueTIcNo9FYqBvtT2QSH-zKFW3TMJGrxEETuIyW2CLF` #### Data OOBIs A data OOBI shows a location to resolve what is typically either a JSON file or a CESR stream, though may be any resource identified by a self-addressing identifier (SAID). Data OOBIs are usually used for ACDC credential schemas, which are JSON files, or CESR streams for well-known ACDC credentials in order to speed up credential verification by hosting common parts of a verification chain in well-known locations. For example, the QVI JSON schema identified with the SAID `EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao` is made available at the following URL on the `10.0.0.1` host. - `http://10.0.0.1:7723/oobi/EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao` ### Role of the Service at an OOBI Endpoint What an OOBI means is that a controller has designated and cryptographically authorized a particular service endpoint (web URL) as the location that controller will receive requests at whether for OOBI resolution, key state requests, or for direct CESR stream transmissions. **Key Points** - OOBIs Facilitate Discovery (Out-of-Band): They may use existing internet infrastructure (web servers, QR codes, etc.) to share potential (url, aid) links. This happens outside of KERI's core trust guarantees. - OOBIs Themselves Are NOT Trusted: Receiving an OOBI does not guarantee the URL-AID link is valid or that the data at the URL is legitimate. - Trust Requires KERI Verification (In-Band): After using an OOBI URL to retrieve data (like a Key Event Log), you must use KERI's standard cryptographic verification methods (checking signatures, verifying event history) to establish trust. In short, OOBIs help you find potential information; verification ensures you can trust it. ## Mailboxes Mailboxes are a simple store and forward mechanism where one controller receives messages on behalf of another. As the primary enabler of indirect mode, mailboxes are the always online presence that continues to receive messages for a controller while that controller is offline or unavailable.
    ℹ️ NOTE
    Currently, the KERIpy implementation of witnesses also provides mailboxes. When a transferable identifier declares a witness in the inception event, that witness will also be used as a mailbox for the controller to receive messages from other controllers.
    Similarly, KERIA agents also serve as mailboxes for Signify Controllers.
    To receive messages from mailboxes a controller polls all of its witness mailboxes. Polling all of the mailboxes is currently necessary because when messages are sent from a source controller to a destination controller then one witness is selected at random from the list of witnesses that the source controller has for the destination controller. The message is not sent to every mailbox for the destination. Thus, every mailbox must be polled in order to discover new messages.
    ℹ️ NOTE
    When the work to support separate mailboxes is completed and supported in the KERIpy reference implementation, then a controller may declare and use only one mailbox. This will simplify mailbox management for controllers that use more than one witness since there will be then only one mailbox, and it will be deployed separately from the witness.
    ## Role of Witnesses Witnesses are entities designated by the controller within their AID key event log, acting much like trusted notaries. Their role is to receive key events directly from the controller, verify the controller’s signature, and check that each event aligns with the event history they have recorded for that AID. Once a witness confirms an event is valid and encounters it for the first time, it generates a **receipt** by signing the event (Witnesses also have their own AID). The witness then stores both the original event and its receipt, alongside receipts from other witnesses, in a local copy of the KEL known as the **Key Event Receipt Log (KERL)**. Witnesses play a critical role in ensuring the system’s reliability and integrity. They provide availability by forming a distributed service that validators can query to access the KEL of a given prefix, even if the controller itself is unavailable. Additionally, they help ensure consistency: since honest witnesses only sign the first valid version of an event at a given sequence number they observe, it becomes significantly harder for a controller to present conflicting log versions (**duplicity**). It's important to note that witnesses are software components. For the system to improve security and availability, the witness should be deployed independently, ideally operated by different entities, on different infrastructure, from both the controller and each other. ## TOAD: Ensuring Accountability A key challenge in maintaining the integrity of an identifier's history is preventing the controller from presenting conflicting versions of events. This situation, known as **duplicity**, occurs if a controller improperly signs two or more different key events purporting to be at the same sequence number in their Key Event Log (KEL) – for example, signing two different rotation events both claiming to be sequence number 3. Such conflicting statements undermine trust in the identifier's true state and control. Reasons for duplicity may be due to malicious intent or operational errors. KERI addresses this partly through the behavior of witnesses, which only sign the first valid event they see per sequence number, and partially through watchers which keep a duplicate copy of a KEL for a given controller so they may detect when a malicious controller tries to change history by changing a key event at a given sequence number that has already occurred. KERI assigns *accountability* for an event, and thus any potential duplicity (change of history), based on a signing threshold of witnesses for a given event, called the **Threshold of Accountable Duplicity (TOAD)**. This signing threshold quantifies the level of agreement needed to assign accountability to a controller for a given event, and thus any potential duplicity. The TOAD is specified in the inception event for an AID and can be changed in each rotation event. We have seen this parameter before when calling `kli incept`. The `toad` value represents the minimum number of unique witness receipts the controller considers sufficient to accept accountability for a key event. By gathering receipts that meet or exceed this controller-defined threshold (`toad`), validators gain assurance that the event history they are watching is the one the controller stands behind and is broadly agreed upon by the witness network. Crucially, while the `toad` defines the controller's threshold for their accountability, a validator may independently establish its own, often higher, threshold watchers that must agree on the history of a KEL to accept an event as fully validated according to its trust policy. These two threshold mechanisms, the TOAD for a signing threshold and a watcher threshold, allowing for distinct controller accountability and validator trust levels, are key to KERI's robust security model and fault tolerance, helping distinguish between minor issues and significant, actionable inconsistencies.
    πŸ“ SUMMARY
    KERI provides two operational modes for sharing Key Event Logs (KELs).
    • Direct Mode: The synchronous, controller-to-controller connection approach for sharing KELs, suitable for when both parties are online.
    • Indirect Mode: An asynchronous approach using key infrastructure, designed for high availability and for controllers that may be offline.
    Key Infrastructure for Indirect Mode:
    • Witnesses: Designated AIDs that enhance reliability and help prevent duplicity by receiving, receipting, and storing a controller's key events in a Key Event Receipt Log (KERL).
    • Mailboxes: A store-and-forward service, often coupled with a witness, that accepts messages on behalf of an offline controller.
    • OOBIs (Out-of-Band Introductions): An untrusted discovery mechanism that links an AID to a network URL. OOBIs help locate KELs and other resources, which must then be cryptographically verified.
    Accountability and Trust:
    • TOAD (Threshold of Accountable Duplicity): A controller-set threshold defining the minimum number of witness receipts required to hold the controller accountable for an event.
    • Validator Trust Policy: A validator can enforce its own, separate trust policy, potentially requiring a higher threshold of verification (e.g., from watchers) than the controller's TOAD.
    [<- Prev (Key Rotation)](101_30_Key_Rotation.ipynb) | [Next (Witnesses) ->](101_40_Witnesses.ipynb) # KLI Operations: Configuring AID Witnesses
    🎯 OBJECTIVE
    Demonstrate how to configure witnesses and the Threshold of Accountable Duplicity (TOAD) in a configuration file and use it to create an AID.
    ## Verifying the Demo Witness Network Now that you understand Witnesses and oobis, let's see some practical usage. Within the deployment of these notebooks, we have included a demo witness network. It is composed of three witnesses: - `http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha` - `http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM` - `http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX` (These witnesses are predefined (**[wan.json](config/witness-demo-docker/wan.json), [wes.json](config/witness-demo-docker/wes.json), [wil.json](config/witness-demo-docker/wil.json)**); that's why we know the prefixes beforehand.) To verify the witness network is working, let's query the KEL of one of them using its oobi and `curl`.
    ℹ️ NOTE
    You can include request parameters on the end of an OOBI and when it is resolved they will be added as contact information for the controller whose OOBI is being resolved, like the ?name=Wan&tag=witness section in the OOBI.
    "http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness"
    This will add the following properties to the contact data for the OOBI:
    • name as "Wan"
    • tag as "witness"
    This is a useful technique for enriching a contact in your contact database with human-friendly attributes.
    ```python !curl -s http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha ``` {"v":"KERI10JSON0000fd_","t":"icp","d":"EIkO4CUmYXukX4auGU9yaFoQaIicfVZkazQ0A3IO5biT","i":"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","s":"0","kt":"1","k":["BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha"],"nt":"0","n":[],"bt":"0","b":[],"c":[],"a":[]}-VAn-AABAAAMlb78gUo1_gPDwxbXyERk2sW8B0mtiNuACutAygnY78PHYUjbPj1fSY1jyid8fl4-TXgLXPnDmeoUs1UO-H0A-EAB0AAAAAAAAAAAAAAAAAAAAAAA1AAG2025-09-12T03c57c20d758008p00c00{"v":"KERI10JSON0000fd_","t":"rpy","d":"EHkrUtl8Nt7nZjJ8mApuG80us9E_td3oa4V-oW2clB2K","dt":"2024-12-31T14:06:30.123456+00:00","r":"/loc/scheme","a":{"eid":"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","scheme":"http","url":"http://witness-demo:5642/"}}-VAi-CABBBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha0BDkVOk5p25Rhim3LkhYXmDNNiUcZkgCp_BWvEB45q6f_pKJBYYlpUABpci5DMzBNXlz4RvK8ImKVc_cH-0D8Q8D{"v":"KERI10JSON0000fb_","t":"rpy","d":"EDSjg0HilC3L4I_eI53C3_6lW9I6pPbR4SWGgoOmDhMb","dt":"2024-12-31T14:06:30.123456+00:00","r":"/loc/scheme","a":{"eid":"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","scheme":"tcp","url":"tcp://witness-demo:5632/"}}-VAi-CABBBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha0BDLG3-HNp-kclaNamqIRI46rNcAhpIEQBDON2HO28r9zO-6S53_w7AA_Q4Weg4eAjvTGiXiNExhO86elrIEd74F{"v":"KERI10JSON000116_","t":"rpy","d":"EBBDzl8D5gFgFkVXaB-XNQlCem-4y5JywPcueWAMRfCp","dt":"2024-12-31T14:06:30.123456+00:00","r":"/end/role/add","a":{"cid":"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","role":"controller","eid":"BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha"}}-VAi-CABBBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha0BDt7alD1tA9x_9vVMKxY1Ne113qJ-xDdCyThnAh6_c13Rsrb9WW5HlKyQxyW5DVXWJjQ65yzME5kCLBiJWYBKEL The command should have returned a KEL; you should be able to recognize part of it. It starts with `{"v": "KERI10JSON0000fd_", "t": "icp"...`. If so, the witness network is up and running. You will see that the response contains JSON and a cryptic text format that looks like `-VAn-AABAAAMl`. This is a CESR string, something we will get into in a later training. ## Keystore Initialization with Witness Configuration Let's continue with the example. As usual, we need to create a keystore, but this time we are going to do something different. We are going to use a configuration file to provide the OOBIs of the witnesses to the keystore. The content of the configuration file can be seen here: **[Keystore configuration file](config/keri/cf/keystore_init_config.json)** ```python # Imports and Utility functions from scripts.utils import clear_keri clear_keri() keystore_name="tests-keystore" keystore_passcode="xSLg286d4iWiRg2mzGYca" salt="0ABeuT2dErMrqFE5Dmrnc2Bq" !kli init --name {keystore_name} --passcode {keystore_passcode} --salt {salt} \ --config-dir ./config \ --config-file keystore_init_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/tests-keystore KERI Database created at: /usr/local/var/keri/db/tests-keystore KERI Credential Store created at: /usr/local/var/keri/reg/tests-keystore aeid: BD-1udeJaXFzKbSUFb6nhmndaLlMj-pdlNvNoN562h3z 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 ## Listing keystore contacts As you can see, the initialization has loaded the oobis. You can also check the loaded witness information by calling the `kli contact list` command ```python !kli contacts list --name {keystore_name} --passcode {keystore_passcode} ``` { "id": "BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha", "alias": "Wan", "oobi": "http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness", "challenges": [], "wellKnowns": [] } { "id": "BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX", "alias": "Wil", "oobi": "http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wil&tag=witness", "challenges": [], "wellKnowns": [] } { "id": "BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM", "alias": "Wes", "oobi": "http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wes&tag=witness", "challenges": [], "wellKnowns": [] } ## Incepting an AID with Witness Configuration Next, you can incept a new AID. Use a configuration file again. The content of the configuration file (**[aid configuration file](config/aid_inception_config.json)**) can be seen here: ```json { "transferable": true, "wits": ["BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha","BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM"], "toad": 1, "icount": 1, "ncount": 1, "isith": "1", "nsith": "1" } ``` Notable highlights in this configuration are the inclusion of the witnesses' prefixes and the `toad` set to 1 Here is the `incept` command: ```python aid_alias_transferable = "aid-transferable" !kli incept --name {keystore_name} --alias {aid_alias_transferable} --passcode {keystore_passcode} \ --file ./config/aid_inception_config.json ``` Waiting for witness receipts... Prefix EJq-DYl9EQVlY1lShETUWLQuDEcVdRkWXfkkGBNDugjZ Public key 1: DOdymqdtGJzeoRRSL9C8Suni6ebPaSqQfuEUM_JFkPQx Check the status of the AID using `kli status` ```python !kli status --name {keystore_name} --alias {aid_alias_transferable} --passcode {keystore_passcode} --verbose ``` Alias: aid-transferable Identifier: EJq-DYl9EQVlY1lShETUWLQuDEcVdRkWXfkkGBNDugjZ Seq No: 0 Witnesses: Count: 2 Receipts: 2 Threshold: 1 Public Keys: 1. DOdymqdtGJzeoRRSL9C8Suni6ebPaSqQfuEUM_JFkPQx Witnesses: 1. BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha 2. BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM { "v": "KERI10JSON000188_", "t": "icp", "d": "EJq-DYl9EQVlY1lShETUWLQuDEcVdRkWXfkkGBNDugjZ", "i": "EJq-DYl9EQVlY1lShETUWLQuDEcVdRkWXfkkGBNDugjZ", "s": "0", "kt": "1", "k": [ "DOdymqdtGJzeoRRSL9C8Suni6ebPaSqQfuEUM_JFkPQx" ], "nt": "1", "n": [ "EO95Pwm8WYG_dIS2-H6LGoXmzOEEnbRljeIjy-Hd7aVx" ], "bt": "1", "b": [ "BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha", "BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM" ], "c": [], "a": [] } In this status, you will find a few new things: - The witnesses section has a count of 2 and mentions two receipts and a threshold of 1 - The KEL has the `b` field populated with the witnesses' prefixes - The `bt` threshold is set to 1 (toad) Guidance for setting the proper threshold, "toad," for the number of witnesses may be found in the KERI Algorithm for Witness Agreement ([KAWA](https://trustoverip.github.io/tswg-keri-specification/#keris-algorithm-for-witness-agreement-kawa)). Generally this means having roughly between 2/3 and 3/4 of witness nodes available, though the equation and table shown below give a precise definition of how to calculate TOAD. In the below chart N = the number of witnessess and M = the number that TOAD should be set to in order to have a strong guarantee of witness agreement even in the face of faulty witnesses. ![image.png](101_40_Witnesses_files/f6dc13ce-4878-4e73-9ca3-ed81773283a7.png)
    πŸ“ SUMMARY
    We used the demo witness network to provide receipts for the inception event of an identifier.
    Witnesses are specified during AID inception using a configuration file by listing their AID prefixes in the wits property of the inception configuration and by setting the Threshold of Accountable Duplicity toad property to a non-zero number. The kli incept command utilizes this file to create the AID, embedding the witness information into the inception event. Checking the AID status with kli status ... --verbose reveals the witness prefixes in the b field, the TOAD in the bt field, and any received witness receipts.
    The table listed above may be used as guidance for setting the TOAD to ensure a strong guarantee of witness agreement.
    [<- Prev (Modes, OOBIs, and Witnesses)](101_35_Modes_oobis_and_witnesses.ipynb) | [Next (Connecting Controllers) ->](101_45_Connecting_controllers.ipynb) # KLI Operations: Connecting Controllers
    🎯 OBJECTIVE
    Explain how to establish a secure, mutually authenticated connection between two KERI controllers using Out-of-Band Introductions (OOBIs) and challenge/response protocol to enhance trust.
    ## Initial Controller Setup So far, we have only done basic operations with AIDs in an isolated way. That has limited use in practical applications; after all, establishing identity verification only becomes meaningful when interacting with others. In KERI, this interaction starts with controllers needing to discover and securely connect with each other. In our context, this means we need to establish connections between controllers. We've already seen a similar process when pairing transferable AIDs with witnesses. Now, let's explore how two controllers (a and b) can connect using Out-of-Band Introductions (OOBIs) and enhance trust with **challenge/response**. ### Keystore Initialization For the example, you need to use two different keystores called `keystore-a` and `keystore-b`, both initialized using the `keystore_init_config.json` configuration. This means they will both load the same initial set of three witness contacts, providing witness endpoints where each controller's KEL (and thus key state) can be published and retrieved when identifiers are created using inception later. ```python # Imports and Utility functions from scripts.utils import clear_keri clear_keri() keystore_a_name="keystore_a" keystore_a_passcode="xSLg286d4iWiRg2mzGYca" salt_a="0ABeuT2dErMrqFE5Dmrnc2Bq" !kli init --name {keystore_a_name} --passcode {keystore_a_passcode} --salt {salt_a} \ --config-dir ./config \ --config-file keystore_init_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/keystore_a KERI Database created at: /usr/local/var/keri/db/keystore_a KERI Credential Store created at: /usr/local/var/keri/reg/keystore_a aeid: BD-1udeJaXFzKbSUFb6nhmndaLlMj-pdlNvNoN562h3z 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 ```python keystore_b_name="keystore_b" keystore_b_passcode="LLF1NYii5L7jTMvw4gDar" salt_b="0ADzG7sbUyw-MYIoUyQe5wxB" !kli init --name {keystore_b_name} --passcode {keystore_b_passcode} --salt {salt_b} \ --config-dir ./config \ --config-file keystore_init_config.json ``` KERI Keystore created at: /usr/local/var/keri/ks/keystore_b KERI Database created at: /usr/local/var/keri/db/keystore_b KERI Credential Store created at: /usr/local/var/keri/reg/keystore_b aeid: BPJYwdaLcdcbB6pTpRal-IhbV_Vb8bD6vq_qiMFojHNG 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 ### Identifier Inception Now, you need AIDs to represent the controllers. Create one transferable AID in each keystore, aliased `aid_a` and `aid_b` respectively. Use the aid_inception_config.json file, which specifies the initial set of witnesses for both AIDs. (While they share witnesses here, controllers could use different witness sets). ```python aid_a = "aid_a" !kli incept --name {keystore_a_name} \ --passcode {keystore_a_passcode} \ --alias {aid_a} \ --file ./config/aid_inception_config.json ``` Waiting for witness receipts... Prefix EML-Hx1ivj6CSkPTM80xCqFmabG9l9ZrVxPe9omW2cWl Public key 1: DDiMxDbmRMjC0mDSkzlwEbYveGozxRXXIsFUo3ixQaU4 ```python aid_b = "aid_b" !kli incept --name {keystore_b_name} \ --passcode {keystore_b_passcode} \ --alias {aid_b} \ --file ./config/aid_inception_config.json ``` Waiting for witness receipts... Prefix EAJR7SlFds3hQpH8kj8HySFRdhW6DcC7m9KdELNJIUma Public key 1: DHEa1ktRvZUjdRitkgJ5u3tNjitiw9Ba0cgz-fMhTS4c ## OOBI Exchange for Discovery With your AIDs established, you need a way for them to find each other. Remember, each witness, in the current implementation, uses each of its witnesses both as a KEL publication mechanism and as a mailbox to receive messages on behalf of the controller. To tell other controllers where to find this witness mailbox, the local controller must provide a way to connect to the witness and the mailbox. This is where Out-of-Band Introductions (OOBIs) come in. You have used OOBIs before; to recapitulate, an OOBI is a specialized URL associated with an AID and how to reach one of its endpoints (like a witness or mailbox). ### Generating OOBI URLs Use the `kli oobi generate` command to create OOBIs for your AIDs. Specify which AID (`--alias`) within which keystore (`--name`) should generate the OOBI, and importantly, the role associated with the endpoint included in the OOBI URL. Here, `--role witness` means the OOBI URL will point to one of the AID's designated witnesses, providing an indirect way to fetch the AID's KEL. This role also, as of the current implementation, also includes the witness acting as a mailbox. There is a separate `--role mailbox` that may be used yet is not covered in this particular training. Use `--role witness` for now. You will see a separate OOBI generated for each witness. ```python !kli oobi generate --name {keystore_a_name} \ --passcode {keystore_a_passcode} \ --alias {aid_a} \ --role witness ``` http://witness-demo:5642/oobi/EML-Hx1ivj6CSkPTM80xCqFmabG9l9ZrVxPe9omW2cWl/witness http://witness-demo:5643/oobi/EML-Hx1ivj6CSkPTM80xCqFmabG9l9ZrVxPe9omW2cWl/witness ```python !kli oobi generate --name {keystore_b_name} \ --passcode {keystore_b_passcode} \ --alias {aid_b} \ --role witness ``` http://witness-demo:5642/oobi/EAJR7SlFds3hQpH8kj8HySFRdhW6DcC7m9KdELNJIUma/witness http://witness-demo:5643/oobi/EAJR7SlFds3hQpH8kj8HySFRdhW6DcC7m9KdELNJIUma/witness Note that the command returns multiple OOBIs, one for each witness endpoint configured for the AID. Any of these can be used to initiate contact. For simplicity, we'll capture the first OOBI URL generated for each AID into the variables `oobi_a` and `oobi_b`. ```python # Imports and Utility functions from scripts.utils import exec command_a = f"kli oobi generate --name {keystore_a_name} --alias {aid_a} --passcode {keystore_a_passcode} --role witness" oobi_a = exec(command_a) print(f"OOBI A: {oobi_a}") command_b = f"kli oobi generate --name {keystore_b_name} --alias {aid_b} --passcode {keystore_b_passcode} --role witness" oobi_b = exec(command_b) print(f"OOBI B: {oobi_b}") ``` OOBI A: http://witness-demo:5642/oobi/EML-Hx1ivj6CSkPTM80xCqFmabG9l9ZrVxPe9omW2cWl/witness OOBI B: http://witness-demo:5642/oobi/EAJR7SlFds3hQpH8kj8HySFRdhW6DcC7m9KdELNJIUma/witness ### Resolving OOBI URLs Now that `aid_a` and `aid_b` each have an OOBI, they need to resolve them. The `kli oobi resolve` command handles this. What happens when an OOBI is resolved? That depends on the type of OOBI. An OOBI resolution for HTTP OOBIs performs an HTTP GET request on the URL. Resolving controller or witness OOBIs returns the key event log for the AID specified in the OOBI URL. For example, when `keystore_a` resolves `oobi_b`, its uses the URL to contact the specified witness. The witness provides the KEL for `aid_b`. `keystore_a` then verifies the entire KEL cryptographically, ensuring its integrity and confirming the public keys associated with `aid_b`. A human-readable alias `--oobi-alias` is assigned for easy reference later. The same process happens when `keystore_b` resolves `oobi_a`. ```python !kli oobi resolve --name {keystore_a_name} \ --passcode {keystore_a_passcode} \ --oobi-alias {aid_b} \ --oobi {oobi_b} ``` http://witness-demo:5642/oobi/EAJR7SlFds3hQpH8kj8HySFRdhW6DcC7m9KdELNJIUma/witness resolved ```python !kli oobi resolve --name {keystore_b_name} \ --passcode {keystore_b_passcode} \ --oobi-alias {aid_a} \ --oobi {oobi_a} ``` http://witness-demo:5642/oobi/EML-Hx1ivj6CSkPTM80xCqFmabG9l9ZrVxPe9omW2cWl/witness resolved ### Listing contacts After successful resolution, the other AID appears in the keystore's contact list. You can verify this using `kli contacts list`. You'll see the newly resolved AID alongside the witnesses loaded during the keystore initialization. This confirms that the keystore now knows the other AID's identifier prefix and has verified its KEL. ```python !kli contacts list --name {keystore_a_name} \ --passcode {keystore_a_passcode} ``` { "id": "BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha", "alias": "Wan", "oobi": "http://witness-demo:5642/oobi/BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha/controller?name=Wan&tag=witness", "challenges": [], "wellKnowns": [] } { "id": "BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX", "alias": "Wil", "oobi": "http://witness-demo:5644/oobi/BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX/controller?name=Wil&tag=witness", "challenges": [], "wellKnowns": [] } { "id": "BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM", "alias": "Wes", "oobi": "http://witness-demo:5643/oobi/BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM/controller?name=Wes&tag=witness", "challenges": [], "wellKnowns": [] } { "id": "EAJR7SlFds3hQpH8kj8HySFRdhW6DcC7m9KdELNJIUma", "alias": "aid_b", "oobi": "http://witness-demo:5642/oobi/EAJR7SlFds3hQpH8kj8HySFRdhW6DcC7m9KdELNJIUma/witness", "challenges": [], "wellKnowns": [] } ## Authenticating Control with Challenge-Response Resolving an OOBI and verifying the KEL is a crucial first step. It confirms that the AID exists and that its key state history is cryptographically sound. However, it doesn't definitively prove that the entity you just connected with over the network is the legitimate controller you intend to interact with. You've verified the identifier, but not necessarily the authenticity of the current operator at the other end of the connection. Network connections can be vulnerable to Man-in-the-Middle (MITM) attacks or other deceptions. This is where the challenge-response mechanism becomes essential. It provides a way to verify that the controller on the other side genuinely possesses the private keys corresponding to the public keys in the KEL you just verified. This adds a critical layer of authentication on top of the OOBI discovery process. This is how it works: One party (the challenger, say `aid_b`) generates a random challenge phrase. The challenger sends this phrase to the other party (`aid_a`) through an Out-of-Band (OOB) channel. This means using a communication method different from the KERI network connection (e.g., a video call chat, phone call, secure email) to prevent an attacker on the KERI network channel from intercepting or modifying the challenge. Using the same channel for both the challenge words and the response defeats the purpose of protecting against MITM attacks because MITM attacks occur "in-band" on a given channel so you must use a separate, "out-of-band" communication channel, such as a video chat, to exchange the challenge phrase. The challenged party (`aid_a`) receives the phrase and uses their current private key to sign it. `aid_a` then sends the original phrase and the resulting signature back to `aid_b` over the KERI connection, typically using general internet infrastructure. Next, `aid_b` verifies two things: - that the returned phrase matches the one originally sent, and - that the signature correctly verifies against the current signing public key associated with `aid_a` in its verified KEL. If the verification succeeds, `aid_b` now has strong assurance that they are communicating with the entity that truly controls `aid_a`'s private keys. This process is typically done mutually, with `aid_a` also challenging `aid_b` to gain strong confidence in the controller of `aid_b`'s keys. You can generate the challenge phrases using `kli challenge generate`. The code below will store them in variables for later use in the commands. ```python print("Example challenge phrase:") !kli challenge generate --out string print("\nChallenge phrases A and B:\n") phrase_a = exec("kli challenge generate --out string") print(f"Challenge Phrase A: {phrase_a}") phrase_b = exec("kli challenge generate --out string") print(f"Challenge Phrase B: {phrase_b}") ``` Example challenge phrase: wasp hire similar despair sausage deposit replace fame devote doll mosquito churn Challenge phrases A and B: Challenge Phrase A: boil remain myself vendor impact frog render monkey salad valve flower oxygen Challenge Phrase B: all autumn copper leg midnight police legend labor keep salon frown innocent Now, simulate the OOB exchange: `aid_b` sends `phrase_b` to `aid_a`, and `aid_a` sends `phrase_a` to `aid_b`. Each party then uses `kli challenge respond` to sign the phrase they received and `kli challenge verify` to check the response from the other party. ```python print(phrase_a) !kli challenge respond --name {keystore_b_name} \ --passcode {keystore_b_passcode} \ --alias {aid_b} \ --words "{phrase_a}" \ --recipient {aid_a} ``` boil remain myself vendor impact frog render monkey salad valve flower oxygen ```python !kli challenge verify --name {keystore_a_name} \ --passcode {keystore_a_passcode} \ --alias {aid_a} \ --words "{phrase_a}" \ --signer {aid_b} ``` Checking mailboxes for any challenge responses. . Signer aid_b successfully responded to challenge words: '['boil', 'remain', 'myself', 'vendor', 'impact', 'frog', 'render', 'monkey', 'salad', 'valve', 'flower', 'oxygen']' ```python print(phrase_b) !kli challenge respond --name {keystore_a_name} \ --passcode {keystore_a_passcode} \ --alias {aid_a} \ --words "{phrase_b}" \ --recipient {aid_b} ``` all autumn copper leg midnight police legend labor keep salon frown innocent ```python !kli challenge verify --name {keystore_b_name} \ --passcode {keystore_b_passcode} \ --alias {aid_b} \ --words "{phrase_b}" \ --signer {aid_a} ``` Checking mailboxes for any challenge responses. . Signer aid_a successfully responded to challenge words: '['all', 'autumn', 'copper', 'leg', 'midnight', 'police', 'legend', 'labor', 'keep', 'salon', 'frown', 'innocent']' Successful verification on both sides mutually establishes cryptographically strong authenticated control of the identifiers on both sides of the interaction. This significantly increases the trust level between the two controllers far beyond the verifiability granted by sharing key histories (KELs) during the initial connection through mutual OOBI resolution. After the challenge response and verification process each party knows they are interacting with the legitimate key holders for each respective AID.
    πŸ“ SUMMARY
    After initial discovery (often via OOBIs), KERI controllers can enhance trust by verifying active control of private keys using a challenge-response protocol. This involves each controller generating a unique challenge phrase (kli challenge generate). One controller (aid_a) then responds to the other's challenge (phrase_b) by signing it (kli challenge respond), and the second controller (aid_b) verifies this response (kli challenge verify). This process is repeated reciprocally. Successful verification by both parties confirms they are interacting with the legitimate key holders for each AID.
    [<- Prev (Witnesses)](101_40_Witnesses.ipynb) | [Next (Delegated AIDs) ->](101_47_Delegated_AIDs.ipynb) # KLI Operations: Creating and Managing Delegated AIDs
    🎯 OBJECTIVE
    Understand the concept of delegated AIDs, where one Autonomic Identifier (AID), the delegator, grants specific authority to another AID, the delegate. This is an illustration of [cooperative delegation](https://trustoverip.github.io/tswg-keri-specification/#cooperative-delegation), meaning both parties work together to form a strongly cryptographically bound delegation relationship between the delegator and the delegate. This notebook demonstrates how to create and manage delegated AIDs using the KERI Command Line Interface (KLI), covering:
    • The two-step cooperative process of delegated inception.
    • Performing delegated key rotation.
    • Examining the Key Event Logs (KELs) of both the delegator and the delegate to understand how the delegation is anchored and verified.
    • ## Introduction to Delegated AIDs In KERI, delegation is a powerful mechanism that allows one AID (the delegator) to authorize another AID (the delegate) to perform certain actions such as signing. This is achieved through a cooperative cryptographic process where both parties participate in establishing the relationship. The primary purpose of delegation in KERI is to allow **scaling of signing authority** by setting up **delegation hierarchies**. The delegation process is as follows: #### Delegation Process Diagram ![image.png](101_47_Delegated_AIDs_files/8abf7d25-5405-4b88-b9ab-0033ce884834.png) #### Process Steps The process steps illustrated in the Delegation Process Diagram above are: 1. Create the Delegator AID. You did this earlier in the training. 2. Create the Proxy AID. 3. Create the Delegate AID and specify the Proxy AID as the communication proxy using the `--proxy` flag to `kli incept`. This process waits for completion until the Delegator AID approves the delegation. It can time out and would need to be restarted. 4. The Proxy AID signs the delegation request from the Delegate AID. 5. The Proxy AID sends the delegation request to the Delegator AID. 6. The Delegator AID receives the signed delegation request, along with the Proxy AID's KEL, verifies the signature on the delegation request, and then chooses to approve or deny the delegation. - In the case of an approval the Delegator AID creates an anchor (seal) and adds that anchor to it's KEL using an interaction event which signifies the Delegator's approval of the delegation. The delegator then sends its KEL containing this anchor, and witness receipts for the interaction event, to the Delegate. 7. The Delegate AID completes delegation by receiving from the Delegator AID a current copy of the delegator's KEL which now includes the anchoring delegation approval seal from the delegator combined with any witness receipts for the delegator's interaction event including this seal. #### Result of Delegation Once the delegation process shown above is complete you will end up with a simple delegation chain that looks like the following ```mermaid graph BT %% Define event nodes DELEGATOR["Delegator πŸ”‘"] DELEGATE["Delegate πŸ”‘"] %% Define backward chaining DELEGATE -- delegated from --> DELEGATOR %% UML appearance classDef eventBox fill:#f5f5f5,stroke:#000,stroke-width:1px,rx:5px,ry:5px,font-size:14px; class DELEGATOR,DELEGATE, eventBox; ``` #### Aspects of KERI Delegation The strong cryptographic binding between delegator and delegate allow the delegator to fractionalize, or split up, their signing authority into multiple smaller units that allow for the straightforward scaling of signing authority. Placing an object called an "anchor" in the delegator's key event log is used to signify delegation approval. This delegation anchor is also known as a "seal." Major aspects of KERI delegation include: - Cooperative Establishment: The creation (inception) and subsequent management (e.g., rotation) of a delegated AID requires coordinated, cooperative actions from both the delegate (initiating the request) and the delegator (confirming and anchoring the event). - Cryptographic Binding: The delegated AID's prefix is a self-addressing identifier (SAID) derived from its own delegated inception event. This inception event, in turn, includes the delegator's AID, creating a strong cryptographic link between the delegator and the delegate. - Anchoring: The delegator anchors the delegation to it's KEL by including a "delegated event seal" in one of its own key events. This seal contains the delegate's AID, the sequence number of the delegated event, and a digest of that delegated event. - An anchor of this form is used for both delegated inception when an identifier is set up and also for delegated rotations when keys of the delegate are changed. The delegator must approve of a delegated rotation in order for a delegated identifier to rotate keys. The anchor in the KEL of the delegator functions as that approval. - Delegated Authority: The delegator typically retains ultimate establishment control authority, while the delegate might be authorized for specific non-establishment events such as signing (interaction events) or further, limited delegations. - Hierarchical Structures: Delegation can be applied recursively, enabling the creation of complex hierarchical key management structures. #### Hierarchical Delegation Diagram ![image.png](101_47_Delegated_AIDs_files/1492ddae-7901-43e5-840c-51ef06c2ebef.png) This notebook will walk through the KLI commands to perform delegated inception and delegated rotation, illustrating how these concepts are put into practice. ## Initial Setup ### Step 1: Create Keystores and Delegator AID First, we'll set up the necessary keystores and a primary AID for the delegator. We will also initialize a keystore for the delegate. For brevity and simplicity in this notebook, passcodes for keystores are omitted using the `--nopasscode` flag. In a production deployment you would generally want to use the `--passcode ` argument to secure a keystore. The `keystore_init_config.json` file is used to pre-configure the keystores with witness information. ```python # Imports and Utility functions from scripts.utils import exec, exec_bg, clear_keri from scripts.utils import pr_continue, pr_title, pr_message clear_keri() pr_title("Initializing keystores") # Delegate Keystore delegate_keystore="delegate_keystore" delegate_salt= exec("kli salt") !kli init --name {delegate_keystore} \ --nopasscode \ --salt {delegate_salt} \ --config-dir ./config \ --config-file keystore_init_config.json # Delegator Keystore delegator_keystore="delegator_keystore" delegator_salt=exec("kli salt") !kli init --name {delegator_keystore} \ --nopasscode \ --salt {delegator_salt} \ --config-dir ./config \ --config-file keystore_init_config.json pr_title("Incepting delegator AID") # Delegator AID delegator_alias = "delegator_alias" !kli incept --name {delegator_keystore} \ --alias {delegator_alias} \ --icount 1 \ --isith 1 \ --ncount 1 \ --nsith 1 \ --wits BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha \ --wits BLskRTInXnMxWaGqcpSyMgo0nYbalW99cGZESrz3zapM \ --wits BIKKuvBwpmDVA4Ds-EpL5bt9OqPzWPja2LigFYZN2YfX \ --toad 2 \ --transferable pr_title("Generating OOBIs") # OOBI Exchange # Delegator generates an OOBI for its AID delegator_oobi = exec(f"kli oobi generate --name {delegator_keystore} --alias {delegator_alias} --role witness") print("Delegator OOBI: " + delegator_oobi) pr_title("Resolving OOBIs") # Delegate's keystore resolves the Delegator's OOBI !kli oobi resolve --name {delegate_keystore} \ --oobi-alias {delegator_alias} \ --oobi {delegator_oobi} 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/delegate_keystore KERI Database created at: /usr/local/var/keri/db/delegate_keystore KERI Credential Store created at: /usr/local/var/keri/reg/delegate_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/delegator_keystore KERI Database created at: /usr/local/var/keri/db/delegator_keystore KERI Credential Store created at: /usr/local/var/keri/reg/delegator_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 delegator AID Waiting for witness receipts... Prefix EHDW4TgdyYTkUwxtZlIt03poPBA4Ouk5w4LJ6MTJJRLB Public key 1: DG7EhH42hjxj77O-InfYucbj7AacdEbZKnMw2qhKrarD Generating OOBIs Delegator OOBI: http://witness-demo:5642/oobi/EHDW4TgdyYTkUwxtZlIt03poPBA4Ouk5w4LJ6MTJJRLB/witness Resolving OOBIs http://witness-demo:5642/oobi/EHDW4TgdyYTkUwxtZlIt03poPBA4Ouk5w4LJ6MTJJRLB/witness resolved You can continue βœ… ## Creating Delegated Identifiers Delegation is a multi-step process involving both the entity wishing to become a delegate and the entity granting the delegation (the delegator). The above delegation process diagram is repeated for convenience. ![image.png](101_47_Delegated_AIDs_files/6da4065c-a5cf-4d45-893b-4dca0b51073e.png) ### Step 2: Delegate Incepts Proxy The delegate needs an AID that can initiate the delegation request. This "proxy" AID is a regular AID within the delegate's keystore. It will be used to facilitate communication between the in-process delegate and the delegator until the delegator confirms the delegation and the process is complete. ```python # Delegate proxy pr_title("Incepting delegate proxy AID") # This AID is in the delegate's keystore and is used to initiate the delegation request. delegate_proxy_alias = "delegate_proxy_alias" !kli incept --name delegate_keystore \ --alias delegate_proxy_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() ``` Incepting delegate proxy AID Waiting for witness receipts... Prefix ECIiDbnDdnPSDqCY4eewKF_6-ZcizO9fkIX6AMYmnr5z Public key 1: DM864_xepY76KkPFfrOASZcGZ4kywBsiteRn2Gm55X8w You can continue βœ… ### Steps 3, 4, and 5: Delegate requests delegated AID Inception, Proxy signs and forwards request During the setup process the delegate uses a proxy AID to send a signed delegation request to the delegator. #### A Proxy is Required for Secure Communications The use of the proxy as shown in steps 3-5 below is necessary because the delegated identifier is not fully set up and thus cannot be used to sign anything until the delegator approves the delegation. This means the delegate cannot sign its own delegation request. So, to request the inception of a new, delegated AID a separate non-delegated AID is used as a one-time, temporary communication proxy. This proxy signs the delegation request so that it can be verifiable by the delegator. ![image.png](101_47_Delegated_AIDs_files/cb546982-d3f2-4633-89c5-b5953c049e08.png) #### kli incept for the delegate When using `kli incept` during delegation the following arguments are needed: - `--name` and `--alias`: Define the keystore and the alias for the new delegated AID being created. - `--delpre`: Specifies the prefix of the AID that will be delegating authority. - `--proxy`: Specifies the alias of the AID within the `delegate_keystore` that is making the request and will serve as the communication proxy between the delegator and the delegate (`delegate_alias`). The `kli incept --delpre` command will initiate the process and then wait for the delegator to confirm. We run this in the background (`exec_bg`) because it will pause. ```python pr_title("Incepting delegated AID") delegator_pre = exec(f"kli aid --name {delegator_keystore} --alias {delegator_alias}") pr_message("Delegator prefix: " + delegator_pre) delegate_alias = "delegate_alias" # Incept delegate. Note --delpre and --proxy parameters # The command runs in the background since it waits for the delegator's confirmation # exec_bg (execute in background) does that. Output is sent to a log file. # This pattern of exec_bg is repeated throughout the notebook command = f""" kli incept --name {delegate_keystore} \ --alias {delegate_alias} \ --icount 1 \ --isith 1 \ --ncount 1 \ --nsith 1 \ --wits BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha \ --toad 1 \ --transferable \ --delpre {delegator_pre} \ --proxy {delegate_proxy_alias} > ./logs/delegate_incept.log """ exec_bg(command) pr_continue() ``` Incepting delegated AID Delegator prefix: EHDW4TgdyYTkUwxtZlIt03poPBA4Ouk5w4LJ6MTJJRLB Command kli incept --name delegate_keystore --alias delegate_alias --icount 1 --isith 1 --ncount 1 --nsith 1 --wits BBilc4-L3tFUnfM_wJr4S4OJanAv_VmF_dJNN6vkf2Ha --toad 1 --transferable --delpre EHDW4TgdyYTkUwxtZlIt03poPBA4Ouk5w4LJ6MTJJRLB --proxy delegate_proxy_alias > ./logs/delegate_incept.log started with PID: 914 You can continue βœ… ### Step 6: Delegator confirms delegation The delegator now needs to confirm the delegation request. The `kli delegate confirm` command checks for pending delegation requests for the specified delegator AID and, if `--auto` is used, automatically approves them. This action creates an interaction event in the delegator's KEL that anchors the delegate's inception event as a "seal" which functions as a proof of delegation approval. ```python # Delegator confirmation pr_title("Confirming delegation") command = f""" kli delegate confirm --name {delegator_keystore} \ --alias {delegator_alias} \ --interact \ --auto """ output = exec(command, True) pr_message(output) pr_continue() ``` Confirming delegation ['Delegagtor Prefix EHDW4TgdyYTkUwxtZlIt03poPBA4Ouk5w4LJ6MTJJRLB', 'Delegate EESIOsSAKBrCvozIIAKcj87hQvntj_wcWiTHuu7AZPI7 inception Anchored at Seq. No. 1', 'Delegate EESIOsSAKBrCvozIIAKcj87hQvntj_wcWiTHuu7AZPI7 inception event committed.'] You can continue βœ… Now, let's examine the status of the newly created delegated AID. ```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: 0 Delegated Identifier Delegator: EHDW4TgdyYTkUwxtZlIt03poPBA4Ouk5w4LJ6MTJJRLB βœ” Anchored Witnesses: Count: 1 Receipts: 1 Threshold: 1 Public Keys: 1. DB5PKs2yTLWkgoaboz2rR0g_im9FBkQF2g8VWcjt6oUP 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" } Key observations from the delegate's status: - `Delegated Identifier`: This line confirms it's a delegated AID. - `Delegator: βœ” 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. ![image.png](101_48_Multisignature_Identifiers_files/453bb096-35eb-4de3-baa0-7b51b6eae48f.png) ## 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. ![image.png](101_48_Multisignature_Identifiers_files/5dee8dbd-8aa0-4f8f-ac64-ea676e4f182a.png) 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. ![image.png](101_48_Multisignature_Identifiers_files/a4a49bf5-780a-4cc7-b61b-32f36bc5deb3.png) ### 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. ![image.png](101_50_ACDC_files/dae13352-a6c2-4f74-91a7-bf94467d5da9.png) ### 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:
      1. Setup: Issuer and Holder established identities (AIDs) and connected via OOBI resolution.
      2. Registry: Issuer created a credential registry (managed via a TEL) to track credential status.
      3. 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.
      4. Creation: Issuer created the specific ACDC instance using kli vc create, providing data conforming to the schema and linking it to the registry.
      5. 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).
      6. 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&reg=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:
      1. Prerequisites: We started with a Holder possessing an issued credential from an Issuer (established via the recap section).
      2. Verifier Setup: A Verifier established its KERI identity (AID).
      3. Connectivity: The Holder and Verifier exchanged and resolved OOBIs. The Verifier also resolved the credential's schema OOBI to enable validation.
      4. 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).
      5. 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. ![ACDC Edge Diagram](./images/acdc-graph-edges.png) 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. ![I2I Operator](./images/I2I-operator.png) 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. ![Not Issuer to Issuee Operator Diagram](./images/NI2I-Operator.png) 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:
      1. Initial Setup: Keystores, AIDs (ACME, Employee, Sub-contractor), and credential registries (for ACME and Employee) were initialized. OOBI connections were established between relevant parties.
      2. 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.
      3. 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).
      4. 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.
      5. 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.
      1. Setup: Three participants (Training Provider, Company, Employee) were initialized with keystores, AIDs, and credential registries for the issuers. OOBI connections were established between them.
      2. 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.
      3. 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.
      4. 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.
      5. 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:
    1. Initialize the library with await ready().
    2. 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.
    3. 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.
    4. 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.
    5. 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:
    1. 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).
    2. 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.
    3. 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.
    4. Operations can be listed with client.operations().list() and deleted with client.operations().delete(operationName).
    5. 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):
    1. Initial Setup: Each client was initialized, booted its KERIA agent, connected, and created an Autonomic Identifier(aidA for Alfred, aidB for Betty).
    2. 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().
    3. 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.
    4. 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:

    1. IPEX Grant: The issuer or holder using an IPEX Grant to share the credential with a verifier.
    2. 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.
    3. 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 Verification Chain ### 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. GLEIF Root of Trust and Delegated Identifier 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. ![vLEI Chain of Verifiable Authority](./images/chain-of-authority.png) 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. ![vLEI Legal Entity Credential](./images/le-credential.png) ### 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. ![vLEI Verification Chain](./images/vlei-verification-chain.png) ## 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