【Hyperledger Aries】Aries Cloud Agent – PythonにおけるMediation機能を試す

SSI

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

この記事ではHyperledger Ariesの実装の1種であるACA-Py(Aries Cloud Agent – Python)が持つMediation機能を試します。

前提

Hyperledger AriesおよびACA-Pyについての詳細は省きますが、端的にいうとACA-PyはDockerコンテナ上のPythonプロセスです。詳細は拙著ではありますが、以下の記事をご参照ください。

また当記事ではローカルに環境を構築します。
以下の拙著の内容を前提にしていますので軽く目を通した上でお進みください。

利用するOSSのバージョンはこの拙著と同様です。

  • ACA-Py: 0.7.3
  • von-network: 1.7.2

Mediation

なぜMediatorは必要なのか? その1

Indy/Ariesの世界では、Issuer、HolderおよびVerifierはP2Pで通信をします。IssuerとVerifierが企業の場合、クラウドにエージェントを配備するのが一般的な形だと考えますが、この場合インターネット上でアクセス可能なIP(言い換えればエンドポイント)を持てます。しかし、一般的に個人であるHolderが使うモバイルアプリ型のエージェントはエンドポイントを持てません。これではIssuer/Verifier -> Holder方向へのDIDCommメッセージ送信ができません。双方向で通信するにはWebSocketを使えますが、その状態に持っていくためにHolderエージェントはオンライン復帰時にDIDCommのConnectionを持つ全てのIssuer/VerifierとWebSocketを張る事態なってしまいます。しかし実際、全てのIssuer/Veriferと常時でメッセージを送受信するケースは考えにくく、その状態に持っていくのはかなりの無駄が生まれ非効率です。そこでMediatorの出番です。

Mediatorはクラウド側に配備されエンドポイントを持ち、Issuer/VerifierからHolderへの通信の中継を行えるエージェントです。ポイントとして、Holderがオフライン状態で通信不可の際はフォワードするメッセージをキューイングし、定期的にHolderに再送信を試みる仕組みを持ちます。Mediatorを介すことでIssuer/VerifierはHolderのオンライン状態に依存せずに自分の仕事に集中することができるわけです。Holderもオンライン復帰時に関係する全てのIssuer/Verifierではなく、MediatorのみWebSocketを張れば良いわけです。

ACA-PyはMediatorとして起動することができます。ACA-Pyによる1Mediatorは複数のコネクションを持つことが可能で、つまりは複数のHolderに対応することができます。

(なおHolderからIssuer/Verifierへの通信についてですが、Issuer/Verifierは通常エンドポイントを持つため、Holderからは直接アクセスするのが基本系です。)

なぜMediatorは必要なのか? その2

W3CのDecentralized Identifiers (DIDs) v1.0の10.6 Service Privacyに、Mediatorが登場します。
それによれば、複数のVerifierが結託して起こるHolderの名寄せ(correlation)を防ぐ、つまりはHolderのプライバシー保護の手段の1つとして、Mediatorが有効だとあります。具体的には、HolderのDID Document内のService Endpointがいつも固定だと、それがHolderを特定するキーになり得るが、多数が利用するMediatorのEndpointを通すことで、それを防げるという理屈です。(ただしこれはHolderがEndpointを持てるCloud Agentの場合の話だと考えます。)

備考: Pull型でのメッセージ取得

ACA-PyはDIDComm配下のトランスポートとしてHTTPとWebSocketを実装しており、Mediationのおいてはどちらかを使い、Holderに対しPush型(Implicit Pickup)でメッセージをフォワードするのがデフォルト実装です。これに加えて、以下のプラグインを使うことでHolderからPull型(Explicit Pickup)でメッセージを取得する手法も取れる模様です。

参考情報1

Mediationやそれに付随する、より深い情報については以下をご参照ください。

  1. [ACA-Py公式Doc] Mediatorが介在する場合のコネクション生成からメッセージングまでのシーケンス図
    • 下記No.3のプロトコルに則っています。
  2. [Aries RFC] 0046: Mediators and Relays
    • Mediationのコンセプトの説明です。
  3. [Aries RFC] 0211: Mediator Coordination Protocol
    • コンセプトを具体化したMediationのプロトコル仕様です。
  4. [Aries RFC] 0160: Connection Protocol
    • コネクション生成のプロトコル仕様です。
  5. [Aries RFC]0019: Encryption Envelope
    • DIDComm v1メッセージの具体的な構造について述べています。

参考情報2

筆者がDIF Japan Monthly Syncで登壇した際にMediatorについて述べています。以下、資料です。

検証

検証事項

  1. 任意の役割を持つACA-Pyを2つ(Alice、Bobとします)と、Alice専用のMediator(ACA-Py)を1つ建てます。中間にMediatorを配置して、AliceとBobの間でメッセージの送受信ができることを確認します。
  2. Aliceがオフラインになった時に、MediatorがBobからのメッセージをキューイングし、定期的に再送信を試みることを確認します。またAliceがオンラインに復帰した際にキューイングされていたメッセージが送信されることを確認します。

自身のMediatorが存在するAliceはいわばHolder的な位置付けであり、一方BobはIssuer/Verifier的な位置付けと言えると思います。

ここから前述の参考情報No.1メッセージフロー図に従って進めます。

それでは、検証を始めます。

1. メッセージフロー図の”Arrange for Mediation with the Mediator”部分

(1) Mediator(ACA-Py)の起動

起動の実装方式については、前述の拙著(c)に倣います。
docker-compose.ymlにおける起動パラメータの差異は以下の通りです。

  • “seed”を外し、代わりに”wallet-local-did”を追加します。MediatorはIssuerと違いIndy Ledger上にPublic DIDを持たないことが理由です。
  • “open-mediation”を追加します。

(2) Alice(ACA-Py)の起動

割愛します。詳細は前述の拙著(c)をご参照ください。
上記(1)のdocker-compose.yml上に、新しいServiceとしてこのACA-Pyを追加するか、もしくは別のdocker-compose.ymlを作るなどで対応できます。上記(1)のコンテナと同じネットワークを指定します。

(3) Alice – Mediator間でのConnectionの生成

AliceとMediatorの間に通常通りにコネクションを張ります。
Mediator側でInvitationを作成するためのAdmin API Endpointを叩きます。

(備考として、何かしらのサービス/システムのプロダクション環境におけるInvitationの使い回しについて述べます。各HolderはAgentとしてスマホアプリを使うことを考えると、開発側で事前にアプリ内にMediatorのInvitationを埋め込んでおき、Holderがインストール後の起動時にそれを読み込むことで、Mediatorとのコネクションを自動で張れるようにするのが定石になると思います。その前提ではHolderごとにInvitationを新たに作るのは困難になるわけですが、 ACA-Pyでは1つのInvitationを基に複数のHolderに対するコネクションを作ることが可能です。具体的なやり方としては、/connections/create-invitationを呼び出しInvitationを作成する際、クエリパラメータにてmulti_use=trueを設定します。

# Request
curl -X 'POST' \
  'http://localhost:8031/connections/create-invitation' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "my_label": "Mediator",
  "service_endpoint": "http://172.19.0.3:8030"
}'

# Response
{
  "connection_id": "97f23d8a-2377-43b4-8d88-a660459e7792",
  "invitation": {
    "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation",
    "@id": "b9ac7d52-6a0b-4b95-ba94-2099886e7d0f",
    "serviceEndpoint": "http://172.19.0.3:8030",
    "label": "Mediator",
    "recipientKeys": [
      "6HHTAmfp2LaXcUB4zKDgF7QjTjDZa2fCMKz1ZBTkNU5d"
    ]
  },
  "invitation_url": "http://172.19.0.3:8030?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiYjlhYzdkNTItNmEwYi00Yjk1LWJhOTQtMjA5OTg4NmU3ZDBmIiwgInNlcnZpY2VFbmRwb2ludCI6ICJodHRwOi8vMTcyLjE5LjAuMzo4MDMwIiwgImxhYmVsIjogIk1lZGlhdG9yIiwgInJlY2lwaWVudEtleXMiOiBbIjZISFRBbWZwMkxhWGNVQjR6S0RnRjdRalRqRFphMmZDTUt6MVpCVGtOVTVkIl19"
}

Alice側でInvitationを受容します。

# Request
curl -X 'POST' \
  'http://localhost:8041/connections/receive-invitation' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation",
    "@id": "b9ac7d52-6a0b-4b95-ba94-2099886e7d0f",
    "serviceEndpoint": "http://172.19.0.3:8030",
    "label": "Mediator",
    "recipientKeys": [
      "6HHTAmfp2LaXcUB4zKDgF7QjTjDZa2fCMKz1ZBTkNU5d"
    ]
  }

# Response
{
  "request_id": "66991a9d-b3e4-4e87-955f-dc8b7f56119f",
  "invitation_msg_id": "b9ac7d52-6a0b-4b95-ba94-2099886e7d0f",
  "created_at": "2022-05-13T04:51:10.283873Z",
  "invitation_key": "6HHTAmfp2LaXcUB4zKDgF7QjTjDZa2fCMKz1ZBTkNU5d",
  "accept": "auto",
  "invitation_mode": "once",
  "rfc23_state": "request-sent",
  "their_label": "Mediator",
  "their_role": "inviter",
  "my_did": "Y1s1EMLV6LThNDBAWoxGeZ",
  "updated_at": "2022-05-13T04:51:10.368495Z",
  "connection_id": "db812765-c678-4cbe-92ba-af27266c8056",
  "routing_state": "none",
  "state": "request",
  "connection_protocol": "connections/1.0"
}

(docker-compose.ymlにて、Connectionを張るプロトコルを自動で進める設定にしているため、微小な時間経過で接続状態がActiveに変わり、Connectionが確立されます。)

(4) AliceからMediatorへMediationの依頼

ACA-PyのAdmin APIには以下のMediationのための(Endpoint + HTTPメソッド)の組み合わせが用意されています。

まず、この中の”POST /mediation/request/{connection_id}”を使い、AliceからMediatorへ、Mediationを行ってくれることの(つまり、自身のMediatorになってくれることの)依頼をします。

# Reques
curl -X 'POST' \
  'http://localhost:8041/mediation/request/db812765-c678-4cbe-92ba-af27266c8056' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
}'

# Response
{
  "created_at": "2022-05-13T04:52:14.845578Z",
  "routing_keys": [],
  "role": "client",
  "updated_at": "2022-05-13T04:52:14.845578Z",
  "connection_id": "db812765-c678-4cbe-92ba-af27266c8056",
  "mediation_id": "5bd4d650-55ff-4844-8a92-3f103de25490",
  "state": "request"
}

リクエストが成功して、新たにMedition IDを取得できました。

上記でMediatorの起動パラメーターには”open-mediation”を含めていました。
公式Docに記載の通り、このパラメータを付与すると”open”というだけあってMediationリクエストに対し、ACA-Pyはそれを自動的に許可します。このパラメータを付与しないで起動した場合は、Mediationリクエストに対し、Endpoint”/mediation/requests/{mediation_id}/grant”を明示的に叩くことで、自身が相手にとってのMediatorになることを許可します。

Alice側のMediationの内容を改めて確認してみます。

# Request
curl -X 'GET' \
  'http://localhost:8041/mediation/requests' \
  -H 'accept: application/json'

# Response
{
  "results": [
    {
      # MediatorのEndpoint情報
      "endpoint": "http://172.19.0.3:8030",
      "created_at": "2022-05-13T04:52:14.845578Z",
      "routing_keys": [
        # BobとMediator間でのDIDCommメッセージ作成のためのMediatorの公開鍵
        "DvDuNdzA7fW92rxvSvrPEPM7YwPYpzLxTVeiznMPWNoR"
      ],
      "role": "client",
      "updated_at": "2022-05-13T04:52:15.253387Z",
      "connection_id": "db812765-c678-4cbe-92ba-af27266c8056",
      "mediation_id": "5bd4d650-55ff-4844-8a92-3f103de25490",
      "state": "granted"
    }
  ]
}

状態がgrantedに変わっていることがわかります。またroleがclientに設定されています。

Mediator側のMediationの内容を確認します。

# Request
curl -X 'GET' \
  'http://localhost:8031/mediation/requests' \
  -H 'accept: application/json'

# Response
{
  "results": [
    {
      "role": "server",
      "updated_at": "2022-05-13T04:52:15.109426Z",
      "routing_keys": [],
      "mediation_id": "b2792ca4-3b03-43ce-b624-654721850fa4",
      "connection_id": "97f23d8a-2377-43b4-8d88-a660459e7792",
      "state": "granted",
      "created_at": "2022-05-13T04:52:14.996515Z"
    }
  ]
}

状態がgrantedに、roleがserverになっていることがわかります。

2. メッセージフロー図の”Create a Mediated Connection – Invitation”部分

(1) Alice側でBob向けの”Out-Of-Band” Invitationの作成

ポイントとして、メッセージフロー図の通りAdmin APIのOut-of-Bandパス配下のEndpointを使います。Out-of-Bandについては、このAries RFCをご参照ください。Aries RFC 0434: Out-of-Band Protocols
またOut-of-Bandにインクルードされている実際のDID交換の仕様はDID Exchange Protocol 1.0に則っています。

# Request
curl -X 'POST' \
  'http://localhost:8041/out-of-band/create-invitation' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "alias": "Alice",
  "handshake_protocols": [
    "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/didexchange/1.0"
  ],
  "mediation_id": "5bd4d650-55ff-4844-8a92-3f103de25490",
  "my_label": "Invitation to Bob",
  "use_public_did": false
}'

# Response
{
  "invi_msg_id": "58b7977f-fac1-4272-adee-80ed67f1904e",
  "trace": false,
  "invitation": {
    "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.0/invitation",
    "@id": "58b7977f-fac1-4272-adee-80ed67f1904e",
    "services": [
      {
        "id": "#inline",
        "type": "did-communication",
        "recipientKeys": [
          # Bobに送る
          # BobとAlice間でのDIDCommメッセージ作成のためのAliceのDID
          "did:key:z6Mkk6jEk9usvfcavtxYvCdS7KDbnEnzCaYYWfTphNrk5Vtf"
        ],
        "routingKeys": [
         # Bobに送る
         # BobとMediator間でのDIDCommメッセージ作成のためのMediatorのDID
         # おそらく前述の"DvDuNdzA7fW92rxvSvrPEPM7YwPYpzLxTVeiznMPWNoR"をdid:keyに変換したもの
          "did:key:z6MksNUwxtEbTCzc9Mod8VpE5Uu7NWfQEsbK9WZeq4KQRbao"
        ],
        #  MediatorのEndpoint 
        "serviceEndpoint": "http://172.19.0.3:8030"
      }
    ],
    "label": "Invitation to Bob",
    "handshake_protocols": [
      "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/didexchange/1.0"
    ]
  },
  "invitation_url": "http://172.19.0.3:8030?oob=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9vdXQtb2YtYmFuZC8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiNThiNzk3N2YtZmFjMS00MjcyLWFkZWUtODBlZDY3ZjE5MDRlIiwgInNlcnZpY2VzIjogW3siaWQiOiAiI2lubGluZSIsICJ0eXBlIjogImRpZC1jb21tdW5pY2F0aW9uIiwgInJlY2lwaWVudEtleXMiOiBbImRpZDprZXk6ejZNa2s2akVrOXVzdmZjYXZ0eFl2Q2RTN0tEYm5FbnpDYVlZV2ZUcGhOcms1VnRmIl0sICJyb3V0aW5nS2V5cyI6IFsiZGlkOmtleTp6Nk1rc05Vd3h0RWJUQ3pjOU1vZDhWcEU1VXU3TldmUUVzYks5V1plcTRLUVJiYW8iXSwgInNlcnZpY2VFbmRwb2ludCI6ICJodHRwOi8vMTcyLjE5LjAuMzo4MDMwIn1dLCAibGFiZWwiOiAiSW52aXRhdGlvbiB0byBCb2IiLCAiaGFuZHNoYWtlX3Byb3RvY29scyI6IFsiZGlkOnNvdjpCekNic05ZaE1yakhpcVpEVFVBU0hnO3NwZWMvZGlkZXhjaGFuZ2UvMS4wIl19",
  "state": "initial"
}

Invitationの内容を確認すると、serviceEndpointにはAliceではなく、MediatorのEndpointが指定されています。これはBobが直接に接続するのはAliceではなくMediatorのためです。

(2) AliceからMediatorへのKeyListの更新

メッセージフロー図でのNo.6の部分です。
上記”(1) Alice側でBob向けの”Out-Of-Band” Invitationの作成”におけるrecipientKeysの内容をMediatorに通知し、Mediator上のKeyList(どの鍵がどのコネクションに対応するかのリスト)にそれを追加します。これにより、BobからのDIDCommメッセージのtoプロパティからそれをAliceにフォワードすることがわかるようになります。

この部分はInvitationを作成した際にACA-Py内部で自動で処理される様です。
Alice側でKeylistを取得すると、生成されたRecipient Keyが登録されていることがわかります。

# Request
curl -X 'GET' \
  'http://localhost:8041/mediation/keylists?role=client' \
  -H 'accept: application/json'

# Response
{
  "results": [
    {
      "created_at": "2022-05-13T04:55:17.607946Z",
      "role": "client",
      "updated_at": "2022-05-13T04:55:17.607946Z",
      "connection_id": "db812765-c678-4cbe-92ba-af27266c8056",
      # おそらく前述の"did:key:z6Mkk6jEk9usvfcavtxYvCdS7KDbnEnzCaYYWfTphNrk5Vtf"のIndyにおける鍵のエンコード表現
      "recipient_key": "6eUC9ufSb887pQ7rEdfbGDfbxfX8nhJBpeYts6tjAH7H",
      "record_id": "42261e9c-a538-40c1-824e-1350a13b812e"
    }
  ]
}

Mediation側でも取得しても同様です。

# Request
curl -X 'GET' \
  'http://localhost:8031/mediation/keylists?role=server' \
  -H 'accept: application/json'

# Response
{
  "results": [
    {
      "role": "server",
      "updated_at": "2022-05-13T04:55:17.516816Z",
      "connection_id": "97f23d8a-2377-43b4-8d88-a660459e7792",
      "recipient_key": "6eUC9ufSb887pQ7rEdfbGDfbxfX8nhJBpeYts6tjAH7H",
      "record_id": "adeb93cd-ff2f-48b4-afcd-adfe611a0ef9",
      "created_at": "2022-05-13T04:55:17.516816Z"
    }
  ]
}

3. メッセージフロー図の”Create a Mediated Connection – Connection Request / Connection Response”部分

(1) Bob(ACA-Py)の起動

詳細は割愛します。

(2) Bob側で上記Invitationの受容

ここでもout-of-bandパスのEndpointを叩きます。

# リクエスト
curl -X 'POST' \
  'http://localhost:8051/out-of-band/receive-invitation' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/out-of-band/1.0/invitation",
    "@id": "58b7977f-fac1-4272-adee-80ed67f1904e",
    "services": [
      {
        "id": "#inline",
        "type": "did-communication",
        "recipientKeys": [
          "did:key:z6Mkk6jEk9usvfcavtxYvCdS7KDbnEnzCaYYWfTphNrk5Vtf"
        ],
        "routingKeys": [
          "did:key:z6MksNUwxtEbTCzc9Mod8VpE5Uu7NWfQEsbK9WZeq4KQRbao"
        ],
        "serviceEndpoint": "http://172.19.0.3:8030"
      }
    ],
    "label": "Invitation to Bob",
    "handshake_protocols": [
      "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/didexchange/1.0"
    ]
  }'


# レスポンス
{
  "request_id": "9bc1f542-2b69-4c82-b2b5-233f3919b9f9",
  "invitation_msg_id": "58b7977f-fac1-4272-adee-80ed67f1904e",
  "accept": "auto",
  "their_label": "Invitation to Bob",
  "state": "request",
  "rfc23_state": "request-sent",
  "connection_protocol": "didexchange/1.0",
   # おそらく前述の"did:key:z6Mkk6jEk9usvfcavtxYvCdS7KDbnEnzCaYYWfTphNrk5Vtf"のIndyにおける鍵のエンコード表現
  "invitation_key": "6eUC9ufSb887pQ7rEdfbGDfbxfX8nhJBpeYts6tjAH7H",
  "invitation_mode": "once",
  "connection_id": "4fee31b2-0391-4d53-af9f-d5a0fda3376d",
  "routing_state": "none",
  "my_did": "XVYpbtfYku7eDhMf9CuJCE",
  "their_role": "inviter",
  "created_at": "2022-05-13T04:58:36.455887Z",
  "updated_at": "2022-05-13T04:58:36.658594Z"
}

2-(2)でMediatorに更新通知した鍵が”Invitation_key”に入っていることが確認できます。

(docker-compose.ymlにて、Connectionを張るプロトコルを自動で進める設定にしているため、微小な時間経過で接続状態がActiveに変わり、Connectionが確立されます。)

(3) AliceからMediatorへのKeyListの更新

メッセージフロー図でのNo.14の部分です。上記(2)でメッセージフロー図の通りAliceとBobは今後のDIDComm通信に使う互いのPairwise DIDを交換済みです。この今後の通信のためのAliceのPairwise DIDは、前述のDID Exchangeために使ったPairwise DID(did: key:z6Mkk6jEk9usvfcavtxYvCdS7KDbnEnzCaYYWfTphNrk5Vtf/6eUC9ufSb887pQ7rEdfbGDfbxfX8nhJBpeYts6tjAH7H)とは別物です。そこでAliceは再度、Mediatorが正しくルーティングできるよう、今後の通信のための新しいPairwise DIDをMediatorのKey Listに追加します。

# Alice側 KeyLists
{
  "results": [
    {
      "created_at": "2022-05-13T04:58:37.651408Z",
      "role": "client",
      "updated_at": "2022-05-13T04:58:37.651408Z",
      "connection_id": "db812765-c678-4cbe-92ba-af27266c8056",
      "recipient_key": "9SH2WtjGFGX9NtfWhx9nVBuztKBwTxXStJPZJvTEJTUs",
      "record_id": "fe652345-5e34-4dd2-854e-9403122728b5"
    }
  ]
}
# Mediator側 Keylists
{
  "results": [
    {
      "role": "server",
      "updated_at": "2022-05-13T04:58:37.560015Z",
      "connection_id": "97f23d8a-2377-43b4-8d88-a660459e7792",
      "recipient_key": "9SH2WtjGFGX9NtfWhx9nVBuztKBwTxXStJPZJvTEJTUs",
      "record_id": "b7be5d29-a1fb-4a09-a0b8-c73ea9adec14",
      "created_at": "2022-05-13T04:58:37.560015Z"
    }
  ]
}

Alice、Mediator両方のRecipient Keyが更新されていることがわかります。

(4) BobからAliceへのメッセージの送信

# Request
curl -X 'POST' \
  'http://localhost:8051/connections/4fee31b2-0391-4d53-af9f-d5a0fda3376d/send-message' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "content": "Hello,  Alice"
}'

以下に示すように、AliceのACA-Pyのログからメッセージを受信したことを確認できました。

重要なポイントとして、メッセージがMediatorを介して送信されたことを、Mediatorのログから確認します。以下はBobからMediatorへ送信されたメッセージです。(base64URLエンコードされている部分はわかりやすいよう、デコードした形で記述しています。)

{
   "message":{
      "@type":"did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/routing/1.0/forward",
      "@id":"6de70199-a7ca-4b92-812e-9b63671e56f4",
      // AliceのPairwise DID
      "to":"9SH2WtjGFGX9NtfWhx9nVBuztKBwTxXStJPZJvTEJTUs",
      "msg":{
         "protected":{
            "enc":"xchacha20poly1305_ietf",
            "typ":"JWM/1.0",
            "alg":"Authcrypt",
            "recipients":[
               {
                  "encrypted_key":"uxYWJHButIW_K5c-XxyM86BHFzxuqyDU4lKP8LQ42DGVZjDH1x_He7hhmXOVSREL",
                  "header":{
                     // AliceのPairwise DID
                     "kid":"9SH2WtjGFGX9NtfWhx9nVBuztKBwTxXStJPZJvTEJTUs",
                     "iv":"d6bxxufXuX_ohS2BR1W2RPlUdbbLZaXy",
                     "sender":"cGS3VLTWjZgERdSS8J0xM5gpqYpmj6kkpbRKCEM4lhXt5NE_-pe1BGEq4arLZcuBevsyS05inP79abgWNIs5WZMkKhR_SsRnSLqn8Ug-iQXakm60uu8GEhuxsQY="
                  }
               }
            ]
         },
         "iv":"1XImsGVoDfMJHDBO",
         "ciphertext":"nz5NOHR9FsNIuRsSPt7eKuWz2VUTwdqaJ7i8AHKUjKPrOa7l2uEr6KG-cgQV1RqMnbARe0L6jpeV3sN4WRx2e9EjdzTiCxbR_mk0vbTmm2iK6WTp9v96YClU3p3I2mD4BSGseT_zE1WUwsN3MwQOOyVs7jz07XU_g9UcBrAIkzI9qp1oo9BbpJhBtLHSYyb5zssCnmgLJVBx4_yCRbzDHRJt1Q8YLtzCWoOQDrwmGuFwK4kBkpa41bTyVrW0guN",
         "tag":"x7kJBPhHGSYkQDb4Cb0Z_g=="
      }
   },
    // MediatorのPairwise DID
   "recipient_verkey":"DvDuNdzA7fW92rxvSvrPEPM7YwPYpzLxTVeiznMPWNoR"
}

上記の参考資料No.5からmessageオブジェクト内部のmsgオブジェクトのフォーマットは、以下の通りです。

 "msg": {
        "protected": "b64URLencoded({
            "enc": "xchachapoly1305_ietf",
            "typ": "JWM/1.0",
            "alg": "Authcrypt",
            "recipients": [
                {
                  "encrypted_key": base64URLencode(libsodium.crypto_box(my_key, their_vk, cek, cek_iv))
                    "header": {
                        "kid": base58encode(recipient_verkey),
                        "sender": base64URLencode(libsodium.crypto_box_seal(their_vk, base58encode(sender_vk)),
                        "iv" : base64URLencode(cek_iv)
                    }
                },
            ],
          })",
        "iv": <b64URLencode(iv)>,
        "ciphertext": b64URLencode(encrypt_detached({'@type'...}, protected_value_encoded, iv, cek),
        "tag": <b64URLencode(tag)>
    }
}

簡単に読んでみます。(メッセージのPack/Unpackのアルゴリズムなどの詳細は参考資料No.5 (DIDComm v1)をご参照ください。またDIDComm v2については別に記事も書いています。ご興味ございましたらご参照ください。)

  • 全体
    • 外側にMediatorに向けた”message”オブジェクトがあり、内側にAliceに向けた”msg”オブジェクトがある構造になっています。(messageがmsgをラップしている。)
    • “message”オブジェクトは、BobからMediatorに向けたDIDComm Plaintext Message(JWM)です。ログには出ていませんが、実際にはJWEでラップされて通信されていると考えます。
    • “msg”オブジェクトは、JWEに則ったDIDComm Encrypted Messageです。
    • 最下部の”recipient_verkey”は、上記1-(4)でAliceがMediatorにMediation依頼をしたときに取得したMediatorのPairwise DID(公開鍵)であり、BobがMediatorに対するJWE作成に使われます。
  • message直下
    • “to”の値は、上記(3)にてAliceからMediatorに通知した、AliceのPairwise DID(公開鍵)です。Mediatorは、Aliceからの通知によりこの鍵が対応するEndpointを知っているため、Aliceにフォワードすることができるわけです。
  • msg配下
    • 共有鍵(CEK)でDIDComm Plaintextを暗号化し通信します。
      • encrypted_keyは、AliceとBobの互いのPairwise DIDに紐づく公開鍵ペアを使った鍵交換(X25519)で生成されたKEKにより、暗号化された使い捨てのCEKです。
      • CEKによる暗号化のアルゴリズムにはChaCha20-Poly1305です。(DIDComm v1固有の仕様)
    • 暗号化されていてMediatorであっても読むことができないプロパティは以下です。(Aliceしか読むことができない。)
      • “encrypted_key” – AliceとBobの公開鍵ペアによる鍵交換で生まれるKEKで暗号化されたCEK。
      • “cipher_text” – CEKで暗号化されたAliceへのメッセージ本文(JWM形式のDIDComm Plaintext Message)
    • JWEの仕様通り、protectedとbody(DIDComm Plain Text)から得られるAuthentication Tagによりメッセージが改竄されていないことをチェックできます。

MediatorからAliceへメッセージをポストしていることのログも出力されます。
Alice宛のJWEである”msg”部分だけ抜き出して送信していることがわかります。

Posting to http://172.19.0.4:8040; 
Data: b'{"protected": "eyJlbmMiOiJ4Y2hhY2hhMjBwb2x5M...(省略)"}'; 
Headers: {'Content-Type': 'application/ssi-agent-wire'}

ここまでで検証事項No.1の”Mediatorを介したメッセージの送受信”を確認できました。ここからは検証事項No.2の”Aliceがオフラインの際にMediatorがメッセージをキューイングすること”を確認します。

4. Aliceがオフラインの際にMediatorがメッセージをキューイングすること

(1) Aliceのコンテナの停止

docker compose stopで停止させます。

(2) BobからAliceへのメッセージ送信

BobからAliceへメッセージを送信します。

curl -X 'POST' \
  'http://localhost:8051/connections/ad94a4dd-cbb6-4b4d-a31b-04a231f3b0c1/send-message' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "content": "Hello,Alice 2"
}'

(3) Mediatorがキューイングすることの確認

Mediatorのログからキューイングすることを確認します。まず通常通りメッセージのフォワードを試みます。

すると当然ながらAliceはオフラインのためエラーが発生します。以下のログから、エラー発生後にMediatorがメッセージを再度キューに入れ戻し、再送信してまたエラーになりまたキューに入れ戻しを繰り返していることがわかります。(ログにタイムスタンプを出せばよかったのですが、体感で5秒間隔ほどで再送信していたと思います。)

(3) Aliceのコンテナの再起動

docker compose startで再起動します。

(4) キューイングされていたメッセージの送受信確認

Mediatorのログから、メッセージ再送信後にエラーが発生しないことを確認できました。

最後にAliceのログからメッセージを受信したことを確認しました。

(ACA-Pyのパラメータ”–max-outbound-retry”(デフォルト値は4)によりリトライ回数を調整できるようです。また上限に達するとメッセージを破棄する模様です。)

備考: Mediatorコンテナの再起動とメッセージの消失

Mediatorはメッセージをメモリ上で持ちます。
キューイングされたメッセージを持つ状態で、Mediatorコンテナを再起動すると、そのメッセージは消失し、Holderに送られないことを確認しています。

おわりに

2つのACA-Pyを仲介する形で、ACA-PyがMediatorとして動作することを検証できました。
どなたかのお役に立てたならば幸いです。