DockerとHyperledger Indy/AriesでSSIシステムのローカル開発環境を構築する

こんにちは。 GMOグローバルサイン・ホールディングスCTO室で分散型IDのR&Dをしている神沼(@t_kanuma)です。 この記事では、1年前にポストした以下の記事のAWS環境に近いものをローカルPCに構築します。 以下の記事では述べなかった開発環境の構築方法を詳細に述べます。

IndyとAriesとは何か、またその関係やAriesのアーキテクチャなどについては上記の記事に加えてこちらも合わせてご参照ください。

環境概要図


各要素はそれぞれコンテナを表します。

1. Docker Desktopのインストール

Docker Desktopをインストールします。

2. Indy Ledger(Blockchain)の構築

Hyperledger Indyノード4つで構成される開発用Ledgerを構築します。SSIをリードするカナダの自治体、ブリティッシュコロンビア州がGitHubで公開しているvon-networkリポジトリを利用します。

GitHubリポジトリのClone

まずは(2021/12/23時点での)最新リリースをクローンします。

git clone https://github.com/bcgov/von-network -b v1.7.2 --depth 1 
cd von-network

von-networkディレクトリ直下にmanageというDocker Imageをビルドしたり、そのイメージからコンテナを起動するシェルスクリプトが用意されています。

Docker Imageのビルド

以下のコマンドでイメージをビルドします。

./manage build

スクリプトを覗くと、同ディレクトリのDockerfileでビルドしていることが分かります。
このDockerfileはベースイメージにbcgovimages/von-image:node-1.12-4を使用しています。

ビルドが完了したら、CLIまたはDocker DesktopのUIからvon-network-baseというイメージが出来ていることが確認できます。

Ledgerの起動

次に以下のコマンドで5つのコンテナを起動します。

./manage start --wait

お手元のdocker-composeのバージョンと設定によっては、ここでエラーになってしまいコンテナが起動しないかもしれません。

docker-compose version

でバージョンがv2.0.0以上になっているとmanageスクリプト(↓に抜粋)はdocker-composeコマンド(L27)ではなく、最新のdocker composeコマンドを実行します(L32)。それはv2.0.0以上ではdocker-composeはdocker composeとして実行されますが、L27の通りではパラメータの指定の仕方でエラーのなるからです。しかしそのエラーを回避するためのL32のパラメータの指定が間違っているようで、これがコンテナを起動できない原因のようです。(スクリプトのバグ?)

 27 dockerCompose="docker-compose --log-level ERROR"
 28 dockerComposeVersion=$(docker-compose version --short | sed 's~v~~;s~-.*~~')
 29 dockerComposeVersion=${dockerComposeVersion%.*}
 30 if [[ $(awk "BEGIN {print (${dockerComposeVersion} >= 2.0) ? "true" : "false" }") == "true" ]]; then
 31   # Use the new syntax when version 2.0.0 or greater is detected.
 32   dockerCompose="docker --log-level error compose"
 33 fi

回避策としては、Docker Desktopの設定画面で以下の赤枠のチェックボックスをオフにします。これでdocker-composeがv1になり、スクリプトを変更しなくても実行できます。

startサブコマンドの–waitパラメータは、コンテナを起動後してLedger内のノードが同期して利用可能な状態になったらコマンドが返ってくるパラメータです。

manageスクリプトを覗くとstartコマンドでは同ディレクトリのdocker-compose.ymlを参照していることを確認できます。docker-compose.ymlでは、8つのサービス(コンテナ)が定義されています。8つのサービス全てがベースイメージに先程ビルドしたvon-network-baseを使用しています。startコマンドではこの8つから、web-server、node1、node2、node3、node4の5つのコンテナを起動しています。node1〜4はそれぞれがブロックチェーンを構成するIndyノードです。web-serverはこのブロックチェーンのトランザクションや状態を確認できるWeb画面です。

コンソールで正常にコマンドが完了したこと、またCLIまたはUIからで5つのコンテナが正常に起動したのを確認できたら、http://localhost:9000 からWeb画面にアクセスできます。

備考: その他サブコマンド

# 各コンテナで出力するlogをtailしたい場合は、以下のコマンドです。
./manage logs

# ネットワーク(各コンテナ)をストップしたい場合は、以下のコマンドです。
# このコマンドの実行では現在のLedgerのデータを削除することなく、ネットワークを停止できます。
./manage stop

# 各コンテナをストップして破棄までしたい場合は、以下のコマンドです。  
# 前述したdocker-compose.ymlを見ると各コンテナに対しNamed Volumeを作成し、
# ホストに永続化していることが確認できますが、ここのコマンドでは明示的に永続化したデータまでを削除しています。
./manage down

ここまでで、開発用のIndy Ledgerを構築できました。

3. IssuerのPublic DIDの作成

前述のWeb画面にてLedger上にIssuerのPublic DIDを登録します。赤枠の部分にDIDの公開鍵ペアの種となる任意のseed(と後で分別しやすいよう適当なalias)を入力します。

また画面経由でなく、curlやAPIクライアントツールでも可能です。

curl -X POST "http://localhost:9000/register"
-d '{
  "seed": "gmogshd0000000000000000000000000",
  "role": "ENDORSER", 
  "alias": "GMOGSHD"
  }'

登録すると、このようにPublic DIDがLedgerに登録されます。
Web画面のLedger StateのDomain種別のTransaction画面から確認できます。

4. Issuer Agentの構築

Ariesフレームワークの1種であるACA-Pyを使います。

GitHubリポジトリのClone

まずは(2021/12/23時点での)最新バージョンをクローンします。

git clone https://github.com/hyperledger/aries-cloudagent-python.git -b 0.7.2 --depth 1 
cd aries-cloudagent-python

備考: ACA-Py側で用意している起動手順

ACA-Pyリポジトリに手を加えずに起動するには、用意されているscriptsディレクトリ配下のrun_dockerスクリプトを使います。

このスクリプトを覗いてみると、scriptsディレクトリと同階層のdockerディレクトリ内のDockerfile.runでイメージをビルドしているのがわかります。

run_dockerでのACA-Py起動の記述は以下の通りです。
provisionとstartというモード(サブコマンド)があります。(違いは後述します。)

./run_docker provision --foo=bar --baz=qux
./run_docker start  --foo=bar --baz=qux

パラメータは以下で確認できます。
startモードのパラメータは100個ほどあります。(多い…)
パラメータの精査というのが少なからず重要になってくると思います。

./run_docker provision --help
./run_docker start --help

本稿では、run_dockerスクリプトを使用しません。
いくつかやり方があると思いますが、今回は既存のコードを変更することなく、新しくファイル(docker-compose)を追加して起動する方法にしました。

備考: provisionとstart

結論から言えば、本番環境では初回起動はprovisionモードで、それ以降はstartモードで起動するのが望ましいです。

本番での運用を考えた場合、DIDの秘密鍵や他エージェントとの接続情報を保持する暗号化されたWalletを作るタイミングは初回起動の1度のみです。それ以降のACA-Pyコンテナの再起動や再配置、冗長化や負荷分散のためのスケールアウトのタイミングでは既に存在するWalletを参照することになるでしょう。

Walletの作成ではPublic DID公開鍵ペアの種となるseed(任意のランダム値)が必要になります。provisionモードではパラメータとしてこのシークレットな値を必要とします。何度も実行するかもしれないstartモードではこの値は必要ありません。代わりにWallet名とWalletキーにより繋ぐべきWalletを特定します。つまりseedを使うタイミングを1度に限定できるわけです。

ただ本稿はローカルの開発環境であるため、provisionモードを使用せずにstartモードだけでやります。startモードにはauto-provisionというパラメータがあり、これを指定することでWalletがなければ作る、あれば使うという振る舞いをします。(その代わり、seedは毎度パラメータに含まれます。)

またWalletを保管できるDBには2種類あります。ACA-Pyプロセスに組み込まれているSQLite、もしくは別プロセスとして存在させるPostgreSQLです。本稿ではPostgreSQLを使います。

docker-compose.ymlと.envファイルの作成

aries-cloudagent-python/dockerディレクトリ配下にissuerディレクトリを作成して、その中にdocker-compose.ymlと.envファイルを作成します。設定内容は参考情報としてご参照ください。

# docker-compose.yml

version: '3.8'
services:
  acapy:
    build:
      context: ../..
      dockerfile: ./docker/Dockerfile.run
    depends_on:
      - db
    ports:
      - "${DIDCOMM_PORT}:${DIDCOMM_PORT}"
      - "${ADMIN_API_PORT}:${ADMIN_API_PORT}"
    # entrypointをオーバーライドする
    entrypoint: /bin/bash
    # commandを付与する
    command: [
      "-c",
      "aca-py \ 
      start \
      --label ${LABEL} \
      --endpoint http://${IP_ADDRESS}:${DIDCOMM_PORT} \
      --inbound-transport http '0.0.0.0' ${DIDCOMM_PORT} \
      --outbound-transport http \
      --admin '0.0.0.0' ${ADMIN_API_PORT} \
      --admin-insecure-mode \ 
      --auto-ping-connection \
      --auto-respond-messages \
      --auto-accept-invites \
      --auto-accept-requests \
      --preserve-exchange-records \
      --wallet-type indy \
      --wallet-name ${WALLET_NAME} \
      --wallet-key ${WALLET_KEY} \
      --wallet-storage-type postgres_storage \
      --wallet-storage-config '{\"url\":\"db:${DB_PORT}\",\"wallet_scheme\":\"DatabasePerWallet\"}' \
      --wallet-storage-creds '{\"account\":\"${DB_ACCOUNT_NAME}\",\"password\":\"${DB_ACCOUNT_PASSWORD}\",\"admin_account\":\"${DB_ACCOUNT_NAME}\",\"admin_password\":\"${DB_ACCOUNT_PASSWORD}\"}' \
      --seed ${WALLET_SEED} \
      --auto-provision \
      --genesis-url ${LEDGER_URL}/genesis \
      --log-file /home/indy/logs/acapy.log \
      --log-level ${LOG_LEVEL}",
    ]
    networks:
      dev-nw:
        ipv4_address: ${IP_ADDRESS}
      von_von:
  db:
    image: postgres:latest
    environment:
      POSTGRES_USER: ${DB_ACCOUNT_NAME}
      POSTGRES_PASSWORD: ${DB_ACCOUNT_PASSWORD}
    expose:
      - "${DB_PORT}:${DB_PORT}"
    networks:
      - dev-nw
networks:
  # Indy Ledgerのnetwork
  von_von:
    external: true
  # aca-pyとpostgres用のnetwork
  dev-nw:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: ${AGENT_SUBNET}

(各パラメータはenvironmentに(例えば–endpointであれば、ACAPY_ENDPONTのように)記述することもできるようです。そうした方がキレイなのですが、–inbound-transportのように複数の値を受け付けるパラメータに対し、色々試したもののenvironmentでの定義ではエラーになってしまい、上記の記述になっています。)

# .env

# Docker周り
COMPOSE_PROJECT_NAME=issuer-dev

# aca-py周り
LABEL=issuer-dev
# アドレスとポートの値は例です
AGENT_SUBNET=172.19.0.0/16
IP_ADDRESS=172.19.0.3
DIDCOMM_PORT=8030
ADMIN_API_PORT=8031
# aca-pyコンテナを再配置/再起動した際にwalletにつなぎに行くために必要になる。
WALLET_NAME=issuer-wallet-dev
WALLET_KEY=thisissecret
# Ledgerにpublic didを登録する際に使ったseed
WALLET_SEED=gmogshd0000000000000000000000000
# 172.18.0.1はLedgerのGateway IP
LEDGER_URL=http://172.18.0.1:9000
LOG_LEVEL=debug

# Postgres周り
DB_ACCOUNT_NAME=postgres
DB_ACCOUNT_PASSWORD=foobarbaz
DB_PORT=5432

ACA-PyとPostgres、両コンテナの起動

docker compose up -d

ACA-Pyのログレベルをdebugにしておくと、Indy Ledgerに繋ぎに行っていること、Walletを作成していることが確認できます。また一度起動したACA-Pyコンテナをstopし、再度起動するとWalletを取得し直していることも見て取れます。

Swagger UI確認 / DB確認

ACA-Pyを正常に起動後、上記の設定では以下のURLにアクセスするとAdmin APIのSwagger UIを表示できます。
http://0.0.0.0:8031/api/doc

Swagger UIからWalletの情報を見てみます。

上記でIndy Ledgerに登録したDID、VerKey(公開鍵)と値が同じであることが確認できました。

またPostgresの中も確認してみます。

パラメーターで指定したWallet NameでDBが出来ていることが確認できました。
またそのDBの中にはACA-Pyが作ったテーブルが4つ存在することも確認できます。

ここまでで、ACA-PyとそのWalletを保管するPostgreSQLの起動ができました。

5. ここから…

詳細は割愛しますが、開発を進めるにはさらに以下のタスクがあります。

WebhookとなるEvent Handlerの開発

ACA-Pyが他エージェントとやりとりを進める度にこのWebhookにイベントとしてHTTPリクエストが飛んできます。ここはいわばREST APIやイベントキューを用意してあげればいいわけで、設計と実装は自由な部分です。WebhookのURLが決まったら前述のstartコマンドのパラメータに、例えば以下のように追記します。(詳細はACA-Pyのこのドキュメントをご参照ください。)

--webhook-url <Webhook URL>

Issuerの残りのDID DocのLedgerへの登録

IssuerがVerifiable Credentialを発行するには、上記で登録したPublic DID、ACA-Py起動時に作成されるATTRIBレコードに加えて、それらに紐づく2つのトランザクションレコード(SCHEMA、およびCRED_DEF)の登録が必要です。Admin APIにこれらを登録するためのEndpointがそれぞれに用意されています。

やりとりするHolder Agentの構築

Issuerの目的はVerifiable Credentialを発行することですから、相手となるHolderがいないと意味がありません。Holderエージェントは本来モバイルデバイス上に存在するもので、Aries Mobile Agent React NativeAries Mobile Agent Xamarinの開発が進められています(これらは開発者から見ると参照実装的な位置付けとして捉えることができると考えています)。ただし、とりあえずIssuerを動かすことを目的とするならば、ACA-Pyを使い擬似的なHolderを作ることで対応できます。(当記事の構築方式に倣った場合の具体的な手法としては、Issuerと同じ要領でHolderディレクトリを作成して、その中にHolder用のdocker-compose.yml、.envファイルを作成します。)

Issuerとの違いは、(Indyの世界では)HolderはPublic DIDを作らないという点です。そのため、startモードのパラメーターにseedは不要です。その代わりにwallet-local-didを追加します。この指定により、Holderはトランザクションを読み込むだけの主体としてLedgerにアクセスできるようになります。(Verifierにおいても同様です。)

ここまでくれば…

あとはAgent間のやりとり(Credentialの発行やProofの検証)のフローに沿って、例えばEvent Handlerで拾ったイベントを処理してアプリ側DBを更新して、ユーザにメールなどで通知をする。そしてUIからユーザにAdmin APIの次のステップの呼び出しを促しことができます。もしくは、拾ったイベントをEvent Router(例えばAWSであればStep FunctionsやSQS、SNS)に渡して、別の処理機構でイベントを取り出し、Admin APIの次のステップを呼び出すこともできます。こうして、一連のフローを進めていくことができます。(フローの進め方の参考として、ACA-PyのDemo用のコードがわかりやすいと思います。)

6. 終わりに

ACA-Pyには1年前と比較して、クラウド上のIssuer/Verifierエージェントとモバイルデバイス上のHolderエージェントの中間に入り、IPを持たないHolderへ/からのDIDCommメッセージをルーティングするMediatorモード、IssuerとIndyノードの間に入ってIssuerの書き込みを代理するEndorserモード、1つのACA-Pyプロセスで複数のAgentを扱うMultitenantモード、これらの新しいモードで動作させるパラメータが追加されています。

今後、この辺りを発信できればと思っています。
どなたかのお役に立てたならば幸いです。