【Hyperledger Indy/Aries】AFJでReact Native製Holder Agentを作りVCモデルを回す(前編)

SSI

こんにちは。GMOグローバルサイン・ホールディングスCTO室で分散型IDの研究開発をしている神沼@t_kanumaです。

これまでのHyperledger Indy/Ariesに関する拙著では、Holder AgentにCloud Agent専用のACA-Py(Aries Cloud Agent -Python)を使い疑似的に構築してきました。しかし物理世界で運転免許証や社員証、保険証を自分の財布やバッグの中で持つように、Holder Agentは本来、Edge AgentとしてVC(Verifiable Credential)を含むWalletをモバイルデバイス内に持ちます。

前編と後編からなるこの記事では、Hyperledger Ariesの実装の1種でモバイルアプリを開発可能なAFJ(Aries Framework JavaScript)を基盤に使い、独自のReact Native製Holder Agentを作成する様子をお届けします。またそのHolderをMediator、Issuer、Verifierと繋げて、VC発行や失効、Proof検証といったVCモデルにおけるイベントを一通り実行した流れを記載します。

今回の前編ではAFJの把握を目的として、まずNode.js環境で擬似的なHolder Agentを作り動かます。後編ではそれをベースにReact Nativeアプリを作り動かします。

前提

Hyperledger Indy/Ariesとは何か、またAriesのアーキテクチャなどについては以下の拙著をご参照ください。

また以下の拙著の内容を前提にしています。ご了承ください。

AFJとACA-Py

処理方式における差異

AFJとACA-Pyとの大きな違いは、開発者が実装するControllerの在り方です。

ACA-Pyでは機能をREST API形式で提供し、開発者はそれを呼び出すControllerを別プロセスで作ります。一方AFJでは、一般的なWebフレームワークのそれと同じで、Node.jsの1つのパッケージとして提供され、Controllerは同じプロセスで動作します。

相互運用性

AFJもACA-PyもAries RFC群を実装するソフトウェアですが、お互いに同じRFCを実装していないと通信できません。

以下、両者がサポートするRFCの一覧を記載しているページです。

ここで上記のページから、両者が後述する動作確認で利用する主なRFCを実装しているかどうかを確認します。以下、◯は両者サポート済みということで問題ない意味です。△はドキュメントの解釈によっては上手くいかない可能性が残る部分です。

RFC名  AFJ as Holder ACA-Py as Issuer, Verifier, Mediator
Connection Protocol 1.0
DID Exchange Protocol 1.0
Out-Of-Band Protocol 1.1
Issue Credential Protocol 1.0
Presentation Proof Protocol 1.0
Mediator Coordination Protocol
Indy: Credential Revocation

以下、△とした部分について述べます。

MediationについてACA-Pyでは以下の注釈付きのサポートでしたが、動作させた結果、機能することを確認済みです。(後述します。)

Only pre-AIP 2.0 version. Must be updated to use did:key for full AIP 2.0 support.

RevocationについてAFJでは以下の機能が実装中である記述がありますが、これはVerifierとしては使えないということだと考えます。実際、Holderとして動作させた時にVC失効の一連の流れが機能することを確認済みです。(後述します。)

Receiving and Verifying revocable Indy Credentials.

以下、その他備考です。

  • Issuer Credential ProtocolおよびPresentation Protocolの最新バージョンは2.0ですが、当記事では今までの記事の通り1.0を使います。(今後このバージョン間の特徴的な差を記事にできたらと思っています。)
  • Aries Framwork GoやAries Framework .NETを含めてフレームワーク間でどれだけ相互運用性が担保されているかのテスト結果が公開されています。AFJとACA-Py間では相互運用性テストを78%パスしていると見て取れます。

動作環境

ローカルのPC環境にて下図の通り環境を構築します。ポイントを列挙します。

  • 各要素、すなわち4つのIndy Node、Issuer、Holder、Verifier、そしてTail Serverは全てDockerコンテナです。
  • DIDComm通信のトランスポートプロトコルについて、MediatorからHolder方向での通信のみWebSocketを使います。他はHTTPです。
  • IssuerとVerifierについて、Webhookは設けず筆者がSwagger UIからAdmin APIを叩く形でやりとりを進めます。
  • HolderのWalletのソフトウェアは、AFJが内包しているSQLiteです。
  • 各OSSのバージョンは以下の通りです。

準備: Issuerに関するLedger上のTransaction

以下のIssuerに関するTransactionについては、あらかじめLedgerに作成しておきます。

  • NYM – Public DIDとそれに紐づく公開鍵を持つ。一般的にDID Documentと呼ばれるモノに対し、それを構成する情報の一部を持つ。
  • ATTRIB – Attribute。Public DIDを所有するAgentのEndpointなどを持つ。一般的にDID Documentと呼ばれるモノに対し、それを構成する情報の一部を持つ。
  • SCHEMA – VCのClaim定義を持つ。
  • CRED_DEF – Credential Defiintion。Schemaの各Claimに対する公開鍵などを持つ。
  • REVOC_REG_DEF – Revocation Registry Definition。 Credential Definitionに紐づきVCの失効管理を行うレジストリを定義する。

作成方法などの詳細は前述の拙著(b)、(c)、(e)をご参照ください。

Holder Agentの開発

メインパートです。

プロジェクト構造

シンプルにnpmもしくはyarnで作ったNode.jsプロジェクトのルートディレクトリに、Controllerとして実装したindex.tsを配置しました。

以下、package.jsonです。

{
  "name": "afj-client",
  "version": "1.0.0",
  "scripts": {
    "start": "tsc && node dist/index.js",
    "lint": "yarn eslint ."
  },
  "dependencies": {
    "@aries-framework/core": "^0.2.0",
    "@aries-framework/node": "^0.2.0"
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^5.32.0",
    "@typescript-eslint/parser": "^5.32.0",
    "eslint": "^8.21.0",
    "eslint-config-prettier": "^8.5.0",
    "typescript": "^4.7.4"
  }
}

Dockerコンテナ周りの実装

Holder Agentはコンテナとして起動させます。ACA-Pyやvon-network、Tail Serverもコンテナであり、そうした方がやり易いからです。プロジェクトのルートディレクトリにDockerfileとdocker-compose.ymlを作ります。

以下、Dockerfileです。AFJが依存するindy-sdk、またindy-sdkが依存する各種ソフトウェアをインストールしています。この実装については、AFJの開発ガイドにおけるLinux環境へのインストレーションページを参考にしました。

FROM archlinux:base-devel as base

# install node.js, npm, and yarn
RUN pacman --noconfirm -Syu
RUN pacman --noconfirm -S nodejs npm
RUN npm install --global yarn

# install libs used by indy-sdk
RUN pacman --noconfirm -S libsodium
RUN pacman --noconfirm -S zeromq

# install git, curl, and cargo
RUN pacman --noconfirm -S git
RUN pacman --noconfirm -S curl
RUN pacman --noconfirm -S cargo

# install rust and indy-sdk
RUN curl --proto '=https' --tlsv1.2 -sSfy https://sh.rustup.rs | sh

RUN git clone https://github.com/hyperledger/indy-sdk
WORKDIR indy-sdk/libindy
RUN cargo build --release
RUN sudo mv target/release/libindy.so /usr/lib/libindy.so

# setup a user
RUN useradd --create-home holder
WORKDIR /home/holder

# install AFJ
COPY package.json .
COPY yarn.lock .
COPY tsconfig.json .

RUN chown -R holder ./
USER holder

RUN yarn install

FROM base as final

# invoke an agent.
COPY index.ts .
ENTRYPOINT ["npm", "start"]

以下、上記Dockerfileを参照するdocker-compose.ymlです。Issuer、Verifier、Tail Server、およびvon-networkと通信可能になるようネットワークの設定をしています。(既に作成済みのネットワークに含めるようにしています。)

version: "3.8"
services:
  holder:
    build:
      context: ./
      dockerfile: ./Dockerfile
    ports:
      - 8090:8090
    networks:
      agent:
        ipv4_address: 172.19.0.9
      von_von:
networks:
  von_von:
    external: true
  agent:
    external: true

Holder Controllerの実装

全てindex.ts内のコードですが、見やすくするため分けて記載します。以下の順で記載します。

  1. メインとなるFunction(Main Functionとする)
  2. Main Functionから切り出したAgent初期化のためのFunction
  3. Main Functionから切り出した各イベント(Connection、Mediation、VC発行、VC失効、Proof提示検証)におけるEvent Listener群

1. Main Function

import fetch from "node-fetch";
import { agentDependencies, HttpInboundTransport } from "@aries-framework/node";
import {
  Agent,
  WsOutboundTransport,
  HttpOutboundTransport,
  ConsoleLogger,
  LogLevel,
  ConnectionEventTypes,
  ConnectionStateChangedEvent,
  RoutingEventTypes,
  MediationState,
  MediationStateChangedEvent,
  DidExchangeState,
  CredentialEventTypes,
  CredentialStateChangedEvent,
  CredentialState,
  ProofEventTypes,
  ProofState,
  ProofStateChangedEvent,
} from "@aries-framework/core";

// omit.... 下記のNo2,3のコードがここに入ります。

(async () => {
  try {
    console.log("initializing a holder agent...");
    const holder = await initializeHolder();

    // Ledgerへのアクセス疎通確認
    // Vtw2qgmuMVy3rk2ipw7VxtはLedgerに登録済みのIssuerのPublic DID
    await holder.ledger.getPublicDid("Vtw2qgmuMVy3rk2ipw7Vxt");

    // Event Listener群の登録
    console.log("registering event listeners...");
    registerConnectionListener(holder);
    registerMediationListener(holder);
    registerVCIssuingListener(holder);
    registerProofPresentListener(holder);
    registerRevocNotificationListener(holder);

    // 対Mediator
    // [Aries RFC 0160: Connection Protocol]でのコネクション生成
    // ログに出したInvitationをコピペしてMediator(ACA-Py)にreceiveさせる。
    const mediator = await holder.mediationRecipient.findDefaultMediator();
    if (mediator === null) {
      console.log("Invitation For Mediator.");
      console.log(
        JSON.stringify(
          (await holder.oob.createLegacyInvitation()).invitation.toJSON()
        )
      );
    } else {
      console.log(
        `a mediator has already been setup: ${JSON.stringify(mediator)}`
      );
    }

    // [Aries RFC 0434: Out-of-Band Protocol 1.1 / Aries RFC 0023: DID Exchange 1.0]でのコネクション生成
    // 本来はこちらの方が望ましいが、ACA-PyがAIP2.0での
    // did:peerに対応していないため(現在実装中の模様)、エラーが発生しコネクションを張れない。
    // https://github.com/hyperledger/aries-cloudagent-python/blob/main/SupportedRFCs.md#did-methods
    // console.log(
    //   JSON.stringify(
    //     (await holder.oob.createInvitation()).outOfBandInvitation.toJSON()
    //   )
    // );
  } catch (e) {
    console.error(e);
  }
})();

2 Agentの初期化のためのFunction

const getGenesisTransaction = async (url: string) => {
  const response = await fetch(url);
  return await response.text();
};

const initializeHolder = async () => {
  const genesisTransactionsOfVon = await getGenesisTransaction(
    "http://172.18.0.1:9000/genesis"
  );
  console.log(genesisTransactionsOfVon);

  const config = {
    label: "nodejs-holder-agent",
    walletConfig: {
      id: "nodejs-holder-wallet",
      key: "testkey0000000000000000000000000",
    },
    connectToIndyLedgersOnStartup: true,
    indyLedgers: [
      {
        id: "local-von-network",
        isProduction: false,
        genesisTransactions: genesisTransactionsOfVon,
      },
    ],
    // ここはReact Nativeでは不要。今回はNode.jsを常駐化させる目的のため。
    endpoints: ["http://172.19.0.9:8090"],
    autoAcceptConnections: true,
    useLegacyDidSovPrefix: true,
    logger: new ConsoleLogger(LogLevel.test),
  };

  const holder = new Agent(config, agentDependencies);
  // Mediatorからのメッセージ受信のため。
  holder.registerOutboundTransport(new WsOutboundTransport());
  // Issuer, Verifierへのメッセージ送信のため。またMediatorとのWS接続の前のConnection生成のため。
  holder.registerOutboundTransport(new HttpOutboundTransport());
  // ここはReact Nativeでは不要。今回はNode.jsを常駐化させる目的のため。
  // これを用意しないとすぐにExitしてしまう。
  holder.registerInboundTransport(new HttpInboundTransport({ port: 8090 }));

  await holder.initialize();
  return holder;
};

3. Event Listener群

(1) Connection
const registerConnectionListener = (holder: Agent) => {
  holder.events.on<ConnectionStateChangedEvent>(
    ConnectionEventTypes.ConnectionStateChanged,
    async ({ payload }) => {
      const connectionRecord = payload.connectionRecord;
      console.log(connectionRecord);
      if (connectionRecord.state === DidExchangeState.Completed) {
        console.log(`Making a connection has been completed.`);
        console.log(`Their Label : ${connectionRecord.theirLabel}`);
        console.log(
          `Connection ID: ${connectionRecord.id}, My DID: ${connectionRecord.did}, Their DID: ${connectionRecord.theirDid}`
        );

        if (connectionRecord.theirLabel === "revoke-mediator") {
          // 相手AgentがMediatorであった場合

          // MediatorにMediationしてもらう依頼と許可取得
          // 許可がGrantされるとMediationのEvent Listenerが起動する。
          const mediationRecordOnRequesting =
            await holder.mediationRecipient.requestAndAwaitGrant(
              connectionRecord
            );

          // Default Meditorとして設定
          const mediationRecordOnSettingAsDefault =
            await holder.mediationRecipient.setDefaultMediator(
              mediationRecordOnRequesting
            );
          console.log(mediationRecordOnSettingAsDefault);

          // WebSocketでの接続の確立
          // AFJは大まかに2つのmediation方式をサポートする。1つはPolling方式、もう1つはWebSocketによるPush方式。
         // ACA-Pyは後者のみサポートする。AFJはそれを検知して接続を試みる。
          await holder.mediationRecipient.initialize();

          // Mediatorを介したIssuerとの接続のためのInvitatition生成
          // [Aries RFC 0160: Connection Protocol]でのコネクション生成
          // ログに出したInvitationをコピペでIssuer(ACA-Py)にreceiveさせる。
          console.log("Invitation for an issuer.");
          console.log(
            JSON.stringify(
              (await holder.oob.createLegacyInvitation()).invitation.toJSON()
            )
          );

          console.log("Invitation for a verifier.");
          console.log(
            JSON.stringify(
              (await holder.oob.createLegacyInvitation()).invitation.toJSON()
            )
          );

          // Mediatorを介したIssuerとの接続のためのInvitatition生成
          // [Aries RFC 0434: Out-of-Band Protocol 1.1 / Aries RFC 0023: DID Exchange 1.0]でのコネクション生成
          // ログに出したInvitationをコピペでIssuer(ACA-Py)にreceiveさせる。
          // ACA-PyがAIP2.0でのdid:peerに対応していないため(現在実装中の模様)、エラーが発生してコネクションを張れない。
          // console.log(
          //   JSON.stringify(
          //     (await holder.oob.createInvitation()).outOfBandInvitation.toJSON()
          //   )
          // );
        }
      }
    }
  );
};
(2) Mediation
const registerMediationListener = (holder: Agent) => {
  holder.events.on<MediationStateChangedEvent>(
    RoutingEventTypes.MediationStateChanged,
    async ({ payload }) => {
      const mediationRecord = payload.mediationRecord;
      if (mediationRecord.state === MediationState.Granted) {
        console.log("Permission for mediation has been granted.");
      }
    }
  );
};
(3) VC発行
const registerVCIssuingListener = (holder: Agent) => {
  holder.events.on<CredentialStateChangedEvent>(
    CredentialEventTypes.CredentialStateChanged,
    async ({ payload }) => {
      const credentialRecord = payload.credentialRecord;
      // VC発行のプロトコルにおける状態ごとの対応
      switch (credentialRecord.state) {
        case CredentialState.OfferReceived: {
          // 発行オファーを受理した際に動く。
          console.log(`Received a credential offer.`);
          console.log(
            `Credential Exchange ID: ${credentialRecord.id}, Connection ID: ${credentialRecord.connectionId}`
          );
          await holder.credentials.acceptOffer({
            credentialRecordId: credentialRecord.id,
          });
          break;
        }
        case CredentialState.RequestSent: {
          // 発行オファーを受理後、発行要求を投げた後に動く。
          console.log(`Sent a credential request.`);
          break;
        }
        case CredentialState.CredentialReceived: {
          // IssuerがCredentialを発行したときに動く。
          console.log(`Storing a credential...`);
          await holder.credentials.acceptCredential({
            credentialRecordId: credentialRecord.id,
          });
          break;
        }
        case CredentialState.Done: {
          // Credentialを受理した後に動く。
          console.log(`a credential has been accepted.`);
          const storedCredentialRecord = await holder.credentials.findById(
            credentialRecord.id
          );
          console.log(
            `stored credential: ${JSON.stringify(storedCredentialRecord)}`
          );
          break;
        }
      }
    }
  );
};
(4) Proof検証
const registerProofPresentListener = (holder: Agent) => {
  holder.events.on<ProofStateChangedEvent>(
    ProofEventTypes.ProofStateChanged,
    async ({ payload }) => {
      const proofRecord = payload.proofRecord;
      switch (proofRecord.state) {
        case ProofState.RequestReceived: {
          console.log(
            `Received a proof request: ${JSON.stringify(proofRecord)}`
          );

          const presentationId = proofRecord.id;
          // Proof proposeとrequestの条件に見合うVC(Claim)群の取得
          // Proofの各Attirubuteに対し複数Claimが紐づく場合もある。
          const retrievedCreds =
            await holder.proofs.getRequestedCredentialsForProofRequest(
              presentationId,
              {
                // Holderからproposeをしていて、それを抽出条件に含めるか含めないか。
                filterByPresentationPreview: false,
                // Verifierが指定した時刻において未失効のVCのみを抽出するかしないか。
                // 失効済みであっても抽出してVerifierで検証結果が否になることを確認するためfalseを設定する。
                // trueで候補が失効済みで抽出されない場合は、↓のautoSelect...でエラーが発生する。(つまりProof提示しない)
                filterByNonRevocationRequirements: false,
              }
            );

          // Proofの各Attributeに当てはめるClaimを1つに絞る。その工程を自動でやってもらう。
          // 自動選択のロジックはメソッド先を参照。
          // 手動でRetrievedCredentialsからRequstedCredentialsを作ることももちろんできる。
          // 今回の場合は発行済みVCが1つのため絞られるということはない。
          const requestedCreds =
            await holder.proofs.autoSelectCredentialsForProofRequest(
              retrievedCreds
            );
          console.log(
            `Requested Credential: ${JSON.stringify(requestedCreds)}`
          );

          // Proofの提示
          await holder.proofs.acceptRequest(presentationId, requestedCreds);
          break;
        }
        case ProofState.PresentationSent: {
          // VerifierにProofを提示した後に動く。
          console.log(`a proof was presented.`);
          break;
        }
        case ProofState.Done: {
          // VerifierがProofを検証してackを送信する。それを受信した際に動く。
          console.log(
            `Proof presentation is done: ${JSON.stringify(proofRecord)}`
          );
          break;
        }
      }
    }
  );
};
(5) VC失効
const registerRevocNotificationListener = (holder: Agent) => {
  holder.events.on(
    CredentialEventTypes.RevocationNotificationReceived,
    async ({ payload }) => {
      console.log(
        `Handling a revocation notification event.: ${JSON.stringify(payload)}`
      );
    }
  );
};

細かいことはコード上にコメントで記述しました。
ここからは、上記のコードを紐付けながら、VCモデルにおけるイベントを一通り行った流れを記載します。

動作確認

確認事項

大まかには、以下の2点を確認しました。

  1. 未失効のVCから成るProofの有効性の検証に成功すること(有効なProofであることの確認)
  2. 失効済みのVCから成るProofの有効性の検証に失敗すること(無効なProofであることの確認)

結果と課題

上記の事項については上手くいくことを確認できました。ただ、細かい点で1つ課題が残りました。

現象として、MediatorからHolderへの通信はWebSocketにしたかったのですが、ここがHTTPになってしまいました。上記のConnectionに関するEvent ListenerのL32の処理において、確かにWebSocketでの通信を確立したという旨をHolderのログから確認したのですが、実際のMediatorのログではHTTPでメッセージをフォワードしていました。

原因として影響がありそうなのは、(その部分のACA-Pyのコードを深掘りできていませんが、)Holderの初期化部分でNode.jsプロセスを常駐化させるために、本来は不要なHTTPのEndpointを設け、それを利用してWebSocketの前にHTTPによるコネクションを確立したことです。それによりMediator側でWebSocketよりHTTPの通信の方が優先された可能性があると考えます。

React Nativeアプリにする際にはこの設定は不要になるため、課題は一旦後編に持ち越すことにします。

行った流れ

コロンの左部分は右部分の処理のトリガーとなる主体を表しています。

(1) Holder以外の各種コンテナの起動

  1. 筆者: von-networkを起動する。
  2. 筆者: Tail Serverを起動する。
  3. 筆者: ACA-Py(Issuer, Verifier, Mediator)を起動する。

詳細は前述の拙著(c)、(d)、(f)をご参照ください。

Mediator ACA-Pyについて補足すると、環境図のようにHolderとはWebSocketで通信させたく、ACA-PyのTransportに対する起動パラメーターの設定を以下のようにしています。

--inbound-transport http '0.0.0.0' <port> \
--inbound-transport ws '0.0.0.0' <port> \
--endpoint http://<ip>:<port> ws://<ip>:<port> \
--outbound-transport http \
--outbound-transport ws \

Issuer ACA-Pyについて補足すると、IssuerからVCを失効させた際にHolderにその旨のメッセージを通知させたいため、以下の起動パラメータを追加しています。

--notify-revocation

(2) Holderの起動とMediatorとの接続

  1. 筆者: Holderを起動する。
  2. Main Function: 初期化処理とEvent Listener群の登録処理後、Mediatorに対するInvitationを作成しロギングする。(このInvitationでは、Holder、Mediator双方ともEndpointはWebSocketではなくHTTPのモノを使う。)
  3. 筆者: ロギングされたInvitationをコピーして、MediatorのAdmin APIをコールする形でそれをMediatorに受容させる。HolderとMediator間でコネクションが確立される。
  4. Event Listener(Connection) L20: Mediatorに自身のmediationを行ってくれるよう依頼し許可をもらう。(Mediatorは起動パラメータの設定のより依頼を自動許可する。)Event Listener(Mediation)が起動する。
  5. Event Listener(Connection) L32: Mediatorとの間でmediationを開始する。つまりはIssuer/VerifierからのメッセージをフォワードするためのWebSocket通信を確立する。(ただし前述の課題の通り、実際のフォワーディングはHTTPで行われる。)
  6. Event Listener(Connection) L40, L47: IssuerとVerifierとの接続のためのInvitationをロギングする。(このInvitationのHolderのEndpointはMediatorのそれが指定されており、以降、IssuerおよびVerifierからHolderへの通信はMediatorを経由する。)

Mediationの詳細については、 拙著(d)をご参照ください

(3) Issuerとの接続とVC発行

  1. 筆者: ロギングされたInvitationをコピーして、IssuerのAdmin APIをコールする形でそれをIssuerに受容させる。HolderとIssuer間でコネクションが確立される。
  2. 筆者: IssuerのAdmin APIを叩き、HolderにVC発行オファーを送信する。(Schemaを含めてオファーの内容は拙著(f)のモノと同様です。)
  3. Event Listener(VC発行) L14: オファーを受理して、VC発行リクエストをIssuerに送信する。
  4. 筆者: IssuerのAdmin APIを叩き、HolderにVCを発行する。
  5. Event Listener(VC発行) L27: 発行されたVCを受理する。

Issuer(ACA-Py)のAdmin API操作の詳細は、拙著(f)をご参照ください。

(4) Verifierとの接続とProof検証

  1. 筆者: ロギングされたInvitationをコピーして、VerifierのAdmin APIをコールする形でそれをVerifierに受容させる。HolderとVerifier間でコネクションが確立される。
  2. 筆者: VerifierのAdmin APIを叩き、HolderにProofリクエストを送信する。(Proofリクエストの内容は拙著(f)のモノと同じです。non_revokedはVC発行後の時間にしています。)
  3. Event Listener(Proof検証) L7~43: リクエストの条件に合致するVC群を抜き出し1つに絞り、VerifierにProofを提示する。
  4. 筆者: VerifierのAdmin APIを叩き、提示されたProofを検証する。(Proofが有効である結果を確認する。)
  5. Event Listener(Proof検証) L49~55: 検証した旨のACKを受け取る。

Verifier(ACA-Py)のAdmin API操作の詳細は、拙著(f)をご参照ください。

(5) VC失効とProof再検証

  1. 筆者: IssuerのAdmin APIを叩き、発行済みのVCを失効させる。Issuerは失効したことのメッセージをHolderに送信する。
  2. Event Listener(VC失効): Issuerから特定のVCが失効したというメッセージを受信する。
  3. 筆者: von-network上で失効によりRevocation Registry Entryの新たなTransactionが生成されていることを確認する。
  4. Proof再検証の流れは上記(4)と同様。ただ今回は失効したことによりProofが無効である検証結果が出る。

Verifier(ACA-Py)のAdmin API操作の詳細は、拙著(f)をご参照ください。

行った流れとその結果は以上です。

おわりに

前編となる当記事では、(前述した課題が残ったものの)Node.js環境でAFJを利用して疑似的なHolder Agentを作り、VCモデルを一通り回すことができました。後編に向けたAFJの把握という目的を達成できたと思います。

後編では同じようにAFJを利用しながら、今度はReact NativeアプリとしてHolder Agentを作り動かす様子をお届けしたいと思います。

どなたかのお役に立てたならば幸いです。