【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は必要なのか?〜

Indy/Ariesの世界では、Issuer、HolderおよびVerifierはP2Pで通信をします。IssuerとVerifierが企業の場合、クラウドにエージェントを配備するのが一般的な形だと考えますが、この場合インターネット上でアクセス可能なIP(言い換えればエンドポイント)を持てます。しかし、一般的に個人であるHolderが使うモバイルアプリ型のエージェントはエンドポイントを持てません。直接に双方向で通信するにはWebSocketを使えますが、1つ問題があります。それはモバイルアプリ型であるHolderは常時オンラインでいることはできず、Issuer/Verifierとの接続が切れる特性を持つことです。Holderがオフライン時にIssuerやVerifierがDIDCommメッセージを貯めておき、オンライン復帰時に再送信する機構を持てば良いですが、不特定多数のHolderを相手にするIssuer/Verifierには負担が大きいと考えます。そこでMediatorの出番です。

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

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

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

Mediator – Holder間のトランスポート

Mediator – Holder間のトランスポート方式については、Aries RFCで言及されていないと認識しています。そのプロトコル上でDIDCommが動作するモノ(HTTPやWebSocket、Bluetooth)であれば良いということだと考えますが、Holderがモバイルアプリ形式でMediator -> Holder方向で通信可能でなくてはならないという面を考えると現実的にはWebSocket一択ではないかと思います。

参考情報

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メッセージの具体的な構造について述べています。

検証

検証事項

  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を叩きます。

# 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": [
    {
      "endpoint": "http://172.19.0.3:8030",
      "created_at": "2022-05-13T04:52:14.845578Z",
      "routing_keys": [
        "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に設定されています。

ここで重要なポイントは、routing_keys配列に値が入っていることです。Routing KeyはMediatorの公開鍵です。AliceとBobはこの公開鍵を用いてセキュアにMediatorにメッセージを渡しています。言わば、Mediatorにしか開けられない封筒になるわけです。
Mediatorが封筒を空けると、そこにはさらに別の封筒が入っています。その封筒には、当記事のケースで言えばAliceまたはBobの宛先が書いてあります。Mediatorはこの宛先を見てメッセージをフォワードします。またMediatorはこの封筒を空けて手紙を読むことはできません。これを開けられるのは宛先に書いてある人(AliceまたはBob)だけです。(ここについては後述の3-(4)でも詳しく述べます。)

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のconnectionパス配下のEndpointを使うのではなく、out-of-bandパス配下のEndpointを使います。(connectionパス配下のEndpointも試しましたが機能しませんでした。)

out-of-boundはconnectionの後継に当たるのですが、そのInvitation作成および受容に関する違いについては、このAries RFCをご参照ください。Aries RFC 0434: Out-of-Band Protocols

# 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": [
          "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"
    ]
  },
  "invitation_url": "http://172.19.0.3:8030?oob=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9vdXQtb2YtYmFuZC8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiNThiNzk3N2YtZmFjMS00MjcyLWFkZWUtODBlZDY3ZjE5MDRlIiwgInNlcnZpY2VzIjogW3siaWQiOiAiI2lubGluZSIsICJ0eXBlIjogImRpZC1jb21tdW5pY2F0aW9uIiwgInJlY2lwaWVudEtleXMiOiBbImRpZDprZXk6ejZNa2s2akVrOXVzdmZjYXZ0eFl2Q2RTN0tEYm5FbnpDYVlZV2ZUcGhOcms1VnRmIl0sICJyb3V0aW5nS2V5cyI6IFsiZGlkOmtleTp6Nk1rc05Vd3h0RWJUQ3pjOU1vZDhWcEU1VXU3TldmUUVzYks5V1plcTRLUVJiYW8iXSwgInNlcnZpY2VFbmRwb2ludCI6ICJodHRwOi8vMTcyLjE5LjAuMzo4MDMwIn1dLCAibGFiZWwiOiAiSW52aXRhdGlvbiB0byBCb2IiLCAiaGFuZHNoYWtlX3Byb3RvY29scyI6IFsiZGlkOnNvdjpCekNic05ZaE1yakhpcVpEVFVBU0hnO3NwZWMvZGlkZXhjaGFuZ2UvMS4wIl19",
  "state": "initial"
}

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

またConnectionプロトコルではなく、その後継のOut-Of-Bandプロトコルを使ったことで、recipientKeyとroutingKeyに”did:key”のprefixが付きました。did:keyは、Pairwise DIDの1種で、Method Specific Identifier(did:key:xxxのxxx部分)から公開鍵の種別と値をresolveできるメソッドです。DIDのupdateとdeactivateはサポートされておらず、長期使用は非推奨なモノで、Ariesでは、後に置き換えられるconnection確立のためのrecipientKeyや、mediation確立のためのroutingKeyなど、一時的な用途で使われます。

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

メッセージフロー図でのNo.6の部分です。
AliceとBob間でConnectionを張るために使う一時的な鍵を作る模様です。Aliceの公開鍵だと推測しますが、これをMediatorに共有します。BobがInvitationを受容した際にAliceへ送るConnection Requestにこの鍵を使いますが、Mediatorは鍵と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",
      "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の受容

ここでもconnectionパスではなく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",
  "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の部分です。
この時点で既にAliceとBobはMediatorを通したConnectionを確立しており、つまりは互いのPairwise DIDに紐づくDID Documentを交換済みです。

ここでもKeyListの更新はACA-Pyの内部で処理されます。KeyListを取得してみます。

# 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が更新されていることがわかります。
詳しくは後述しますが、このRecipient KeyはAliceの(Pairwise DIDに紐づく)公開鍵だと考えます。

(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へ送信されたメッセージです。

{
   "message":{
      "@type":"did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/routing/1.0/forward",
      "@id":"6de70199-a7ca-4b92-812e-9b63671e56f4",
      "to":"9SH2WtjGFGX9NtfWhx9nVBuztKBwTxXStJPZJvTEJTUs",
      "msg":{
         "protected":{
            "enc":"xchacha20poly1305_ietf",
            "typ":"JWM/1.0",
            "alg":"Authcrypt",
            "recipients":[
               {
                  "encrypted_key":"uxYWJHButIW_K5c-XxyM86BHFzxuqyDU4lKP8LQ42DGVZjDH1x_He7hhmXOVSREL",
                  "header":{
                     "kid":"9SH2WtjGFGX9NtfWhx9nVBuztKBwTxXStJPZJvTEJTUs",
                     "iv":"d6bxxufXuX_ohS2BR1W2RPlUdbbLZaXy",
                     "sender":"cGS3VLTWjZgERdSS8J0xM5gpqYpmj6kkpbRKCEM4lhXt5NE_-pe1BGEq4arLZcuBevsyS05inP79abgWNIs5WZMkKhR_SsRnSLqn8Ug-iQXakm60uu8GEhuxsQY="
                  }
               }
            ]
         },
         "iv":"1XImsGVoDfMJHDBO",
         "ciphertext":"nz5NOHR9FsNIuRsSPt7eKuWz2VUTwdqaJ7i8AHKUjKPrOa7l2uEr6KG-cgQV1RqMnbARe0L6jpeV3sN4WRx2e9EjdzTiCxbR_mk0vbTmm2iK6WTp9v96YClU3p3I2mD4BSGseT_zE1WUwsN3MwQOOyVs7jz07XU_g9UcBrAIkzI9qp1oo9BbpJhBtLHSYyb5zssCnmgLJVBx4_yCRbzDHRJt1Q8YLtzCWoOQDrwmGuFwK4kBkpa41bTyVrW0guN",
         "tag":"x7kJBPhHGSYkQDb4Cb0Z_g=="
      }
   },
   "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をご参照ください。)

  • 全体
    • 外側にMediatorに向けた”message”オブジェクトがあり、内側にAliceに向けた”msg”オブジェクトがある構造になっています。(messageがmsgをラップしている。)
    • 最下部の”recipient_verkey”は、上記1-(4)でAliceがMediatorにMediation依頼をしたときに取得したMediatorの(Pairwise DIDに紐づくと推測する)公開鍵であり、messageオブジェクトを保護するものです。
  • message直下
    • “to”の値は、上記(3)にてAliceからMediatorに通知した、Aliceの(Pairwise DIDに紐づく)公開鍵だと考えます。Mediatorは、Aliceからの通知によりこの鍵が対応するEndpointを知っているため、Aliceにフォワードすることができるわけです。
  • msg配下
    • 共有鍵(CEK)、そして認証付き暗号を用いて通信する模様です。(CEKはAliceとBobの互いのPairwise DIDに紐づく公開鍵からの鍵交換で生成されると推測します。)
    • 認証付き暗号のアルゴリズムにはChaCha20-Poly1305に近いもの?を使っているようです。
    • 構造はJWMに則っている模様です。
    • CEKもしくはAliceの公開鍵により暗号化されている要素は以下の3つです。これらはMediatorであっても読むことができません。(Aliceしか読むことができない。)
      • “encrypted_key” – 暗号化されたCEK。
      • “sender” – Bobの(Pairwise DIDに紐づくと推測する)公開鍵が含まれている。これによりAliceは誰と通信しているか(どのコネクションなのか)がわかると考える。
      • “cipher_text” – CEKで暗号化されたAliceへのメッセージ本文(それに加えて完全性の検証のためと考えるが”protected”の値も含まれている。)

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

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

上図の通りBob用のMediatorは作っていません。詳細は省きますが、AliceからBobへのメッセージ送信がMediatorを介さずに直接届くことも確認しています。

ここまでで検証事項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のログからメッセージを受信したことを確認しました。

おわりに

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