Hyperledger ACA-PyとOpen Wallet Foundation Credoを用いてOID4VCI上でJWT-VCの発行を試す

SSI

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

この記事では、次の2つのフレームワークを用いてOID4VCI(Draft11)のPre-Authorized Code Flowに則りJWT-VCの発行を試します。

  • Hyperledger AriesのACA-Py
    • Issuerとして使います。
    • 本体とは別のOID4VCI Pluginを使います。
    • 詳細は後述しますが、このPluginをFolkして修正を加えます。
  • Open Wallet Foundation(OWF)のCredo
    • Holderとして使います。
    • 旧Aries Framework JavaScriptです。
    • 詳細は後述しますが、Folkして修正を加えます。

Aries Frameworkの最近の動き

昨今の界隈の流れを汲んで、ACA-PyとCredoはOID4VCの実装を取り込んでいます。

ACA-PyのOID4VCIプラグイン

ACA-Pyの主目的はAries RFCを実装することであり、本体にはOID4VCIの実装は含まれていません。ACA-Pyには公式に認定、テストされたプラグイン群があり、その中の1つがOID4VCIプラグインです。

Initial Commitは2023年12月と最近できたものです。READMEにはアーキテクチャ図や、典型的な形式(HoldeがWalletでIssuer Webアプリ上のQRコードを読み取り、WalletにVCを発行してもらう)におけるシーケンス図、Usageの説明などがあり、ドキュメントは充実しているように見えます。

未実装 / 制限部分

現状、OID4VCIの全てが実装されているわけではありません。フローはPre-Authorized Code Flowのみ実装されています(Authorized Code Flowは未実装)。Batch Credential Issueanceなどには未対応です。

他にも、Credential Formatの部分ではLDP-VCとSD-JWTに未対応です。またVC署名のDID Methodとアルゴリズム部分はACA-Py本体から特に拡張されてはいないため、これまで通りDID Methodはdid:sovまたはdid:key、アルゴリズムはEdDSA(Ed25519)のみです。
(DID Methodに関して、この記事では筆者自作のdid:webプラグインを使います。)

詳細はREADMEのNot Implementedでリスト化されています。

CredoのOID4VCIパッケージ

旧AFJは、ACA-Pyと同様、Aries RFCの実装を主目的としてスタートしていますが、ACA-Pyと異なり汎用的なAgentフレームワークを目指している模様です。その中でv0.4(2023年7月リリース)からHolderの役回りとしてOID4VCIを実装しています。これはリポジトリ本体内のNPMパッケージ群の1つとして提供されています。(@aries-framework/openid4vc-client)

未実装 / 制限部分

このCredoの最新バージョンにおける上記パッケージのコードを読むと、AuthorizedとPre-Authorized両方のフローに対応しているように見えますが、これが依存するSphereon社のライブラリ@sphereon/oid4vci-clientには現状、Pre-Authorized Code Flowにのみ対応していると書かれており、そのように認識しています。

実装するOID4VCIのバージョンと調整

Credoが実装するOID4VCIのバージョンは明記されてはいませんが、コードを読むとDraft 8だと理解しています。(正確に言うと依存するSphereonのライブラリのバージョン(0.4.0)がDraft 8の実装です。)ACA-PyはDraft 11でありこれに合わせる必要があるため、Credoをフォークし、Draft 11を実装するSphereライブラリのバージョン(0.8.1)に適合するようCredoのパッケージを調整し、NPMにパブリッシュします。

(2024/2/8追記)

開発が進むmainブランチを見ると、2/1にこのパッケージに大幅なアップデートがあった模様です。そこではこれまでのHolderだけはなく、IssuerそしてVerifier(OID4VP、SIOPv2)の実装も追加されています。また依存するSphereonのライブラリも最新のモノを利用しています。

備考: 旧AFJのOWFへの移管

2023年9月の記事”Hyperledger Aries Framework Javascript Identity Wallets Standards Push“によれば、EUDI WalletのARFへの準拠を目指していることが見受けられます。今後はOID4VCIだけでなく、OID4VC内の他の標準(OID4VP、SIOPv2)やSD-JWT、mDLへの対応が成されるのではないかと思います。(SD-JWTの実装は、次期リリース(v0.5)に含まれる模様です。)

この流れの中で2023年12月に、AFJはCredoとして開発コミュニティによりOWFに移管されました。(GitHub Issue: Moving to the Open Wallet Foundation)

全体像

大まかな流れ

  1. 筆者がACA-Pyの(プラグインにより追加された)Admin API Endpointを叩き、仕様10.2.3. Credential Issuer Metadata Parametersの必須プロパティであるcredentials_supportedと、VCに署名するためのDIDを作成します。
  2. 筆者がACA-Pyの(プラグインにより追加された)Admin API Endpointを叩き、No.1のcredentials_supportedのIDやPre-Authorized Codeなどをプロパティとする仕様4.1 Credential Offerを作成します。
  3. ハードコードする形で、起動したCredoにCredential Offerを読み込ませます。
    • 実際のユースケースでの典型的な形として、Holder Wallet(モバイルアプリ)でQRコードを読む部分に該当します。
  4. Holder Agentが”{Issuer Origin}/.well-known/openid-credential-issuer”にアクセスし、Credential Issuer Metadataを取得します。
  5. Holder Agentが”{Issuer Origin}/token”に対しCredential Offer内のPre-Authorized Codeを渡し、Access Token(Issuerのdid:webで署名されたJWT)とProof of Posessionの内容になるnonceを取得します。
  6. Holder AgentがCredential Issuer Metadataの内容から、Proof of Posession(JWT)に署名するDIDのVerification Methodを決定します。
    • 今回はdid:key(EdDSA/Ed25519)を作成します。
  7. Holder Agentが”{Issuer Origin}/credential”にAccess TokenとProof of Possession(Holderのdid:keyで署名されたJWT)を渡し、JWT-VC(Issuerのdid:webで署名されたJWT)を取得します。

その他ポイント

  • 全体図の通り、IssuerはAuthorization Serverを兼ねます。
  • ACA-Py(Issuer)、Credo(Holder)はローカルのコンテナ上で、それぞれPython、Node.jsプロセスとして動かします。
  • ACA-Pyはプラグイン導入により、Admin APIとDIDComm以外の3つ目のOrigin(開発ドキュメント上のPublic Route)を持ちます。
  • IssuerによるVCへの署名にはdid:webを使います。

バージョン

それぞれ執筆時点で最新のバージョンです。

  • ACA-Py: 0.11.0
  • Credo: 0.4.2

準備

ACA-Py周り

コンテナ周り

以下、Dockerfileです。
aries-cloudagent-python/docker/Dockerfile.runをコピーし修正を加えています。
修正部分についてコメントしています。

# aries-cloudagent-python/docker/Dockerfile.run-oid4vci
FROM python:3.9-slim-bullseye

ENV ENABLE_PTVSD 0

# Gitインストール部分追加
RUN apt-get update && apt-get install -y curl && apt-get install -y git && apt-get clean

RUN pip install --no-cache-dir poetry

RUN mkdir -p aries_cloudagent && touch aries_cloudagent/__init__.py
ADD pyproject.toml poetry.lock README.md ./
RUN mkdir -p logs && chmod -R ug+rw logs

RUN poetry install -E "askar bbs"

ADD . .

# 上記拙著の自作プラグインのインストール(ローカルでビルドファイル生成)
RUN poetry add ./did_web_plugin-0.1.0-py3-none-any.whl
# OID4VCIプラグインを微調整したモノのインストール(詳細は後述)
RUN poetry add git+https://bitbucket.org/gmogshd-cto-office-ssi/aries-acapy-plugins@master#subdirectory=oid4vci

ENTRYPOINT ["/bin/bash", "-c", "poetry run aca-py \"$@\"", "--"]

以下、docker-compose.ymlとenvファイルです。
ポイントはコメントで記述しています。

# aries-cloudagent-python/docker/issuer/docker-compose.yml
services:
  acapy:
    build:
      context: ../..
      dockerfile: ./docker/Dockerfile.run-oid4vci
    ports:
      - "${DIDCOMM_PORT}:${DIDCOMM_PORT}"
      - "${ADMIN_API_PORT}:${ADMIN_API_PORT}"
    entrypoint: /bin/bash
    command: [
      "-c",
      # v0.11より、pipからpoetryに移行している
      "poetry run aca-py \
      start \
      --label ${LABEL} \
      --inbound-transport http '0.0.0.0' ${DIDCOMM_PORT} \
      --endpoint ${DIDCOMM_ENDPOINT} \
      --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 askar \
      --wallet-name ${WALLET_NAME} \
      --wallet-key ${WALLET_KEY} \
      --auto-provision \
      --no-ledger \
      # 上記拙著の自作プラグイン
      --plugin did_web_plugin \
      # OID4VCIプラグイン
      --plugin oid4vci \
      # OID4VCIプラグインの構成設定
      --plugin-config-value oid4vci.host=0.0.0.0 oid4vci.port=${OID4VCI_PORT} oid4vci.endpoint=${OID4VCI_ENDPOINT} \
      --trace \
      --trace-target log \
      --trace-label oid4vci-issuer \
      --timing \
      --log-level ${LOG_LEVEL} "
    ]
    networks:
      agent_nw:
        ipv4_address: ${IP}
networks:
  agent_nw:
    external: true
# aries-cloudagent-python/docker/issuer/.env
COMPOSE_PROJECT_NAME=issuer
LABEL=issuer
IP=172.19.0.3
DIDCOMM_ENDPOINT=http://172.19.0.3:8030
DIDCOMM_PORT=8030
ADMIN_API_PORT=8031
WALLET_NAME=issuer-wallet
WALLET_KEY=thisissecret
LOG_LEVEL=debug
ACAPY_VERSION=0.11.0

# OID4VCI対応
OID4VCI_PORT=8032
OID4VCI_ENDPOINT=http://172.19.0.3:8032

OID4VCIプラグインの微調整

以下の通り、OID4VCIプラグインをフォークし微調整を入れました。

  • did:webへの対応
    • VC発行にてHolderから飛んできたAccess Token(JWT)に対する一連の妥当性チェックの中で、JWTに署名したDIDメソッドのチェックをしています。これがdid:keyに限定されているため、did:webを追加します。
    • コミット内容
  • User Pin周りのバグ?
    • Access Token取得でHolderから飛んでくるリクエストの中のPinをクエリパラメーターから取得しています。しかしCredoは(仕様通り)Pinをボディに入れて来ます。ボディから取るように修正しています。
    • コミット内容

ACA-Py起動後

OID4VCIプラグインを読み込んでいることを確認できました。

またAdmin APIにOID4VCIのためのエンドポイント群ができていることも確認できました。

Credo周り

プロジェクト / コンテナ周り

以下、今回のHolder Appのpackage.jsonです。
@gmogshd-cto-office/credo-openid4vci-clientは@aries-framework/openid4vci-clientをカスタマイズし一時的にパブリッシュしたパッケージです。

{
  "name": "afj-oid4vci-client",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "license": "MIT",
  "scripts": {
    "start": "tsc && node dist/index.js",
    "format": "prettier --write .",
    "lint": "eslint ."
  },
  "dependencies": {
    "@aries-framework/core": "^0.4.2",
    "@aries-framework/node": "^0.4.2",
    "@aries-framework/askar": "^0.4.2",
    "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5",
    "@gmogshd-cto-office/credo-oid4vci-client": "0.0.10",
    "log4js": "^6.9.1"
  },
  "devDependencies": {
    "typescript": "5.3.3",
    "@tsconfig/node18": "^18.2.2",
    "eslint": "^8.56.0",
    "@typescript-eslint/eslint-plugin": "^6.19.0",
    "@typescript-eslint/parser": "^6.19.0",
    "eslint-config-prettier": "^9.1.0",
    "prettier": "^3.2.5"
  }
}

Dockerfileです。

FROM ubuntu:20.04 as base

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update -y && apt-get install -y \
    software-properties-common \
    apt-transport-https \
    curl \
    build-essential \
    git

RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash -

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

RUN apt-get update -y && apt-get install -y --allow-unauthenticated nodejs

RUN apt-get install -y --no-install-recommends yarn

FROM base as final

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

# install credo-ts
COPY package.json yarn.lock tsconfig.json .

RUN chown -R holder ./
USER holder

RUN yarn add node-gyp && yarn install --ignore-engines

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

docker-compose.ymlです。

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

Credoのoid4vci-clientパッケージのカスタマイズ

以下の通り、Credoをフォークし調整しました。

  • Draft 11への適合
    • 前述の通り、Draft11に適合するように修正しました。
    • コミット内容
  • User Pin周りのバグ?
    • Pre-Authorized Code Flowを担当する関数のパラメーターにPinが無く(実装し忘れ?)、これを追加しました。
    • コミット内容

アプリケーションコード

// index.ts
import {
  Agent,
  InitConfig,
  KeyDidCreateOptions,
  VerificationMethod,
  ConsoleLogger,
  LogLevel,
} from "@aries-framework/core";
import { agentDependencies } from "@aries-framework/node";
import { AskarModule } from "@aries-framework/askar";
import { ariesAskar } from "@hyperledger/aries-askar-nodejs";
import {
  OpenId4VcClientModule,
  PreAuthCodeFlowOptions,
  ProofOfPossessionVerificationMethodResolver,
  ProofOfPossessionVerificationMethodResolverOptions,
} from "@gmogshd-cto-office/credo-oid4vci-client";
import log4js from "log4js";

const logger = log4js.getLogger("app");
logger.level = log4js.levels.INFO;

const initializeHolder = async (): Promise<Agent> => {
  const config: InitConfig = {
    label: "oid4vci-holder-agent",
    walletConfig: {
      id: "oid4vci-holder-wallet",
      key: "testkey0000000000000000000000000",
    },
    logger: new ConsoleLogger(LogLevel.debug),
  };

  const holder = new Agent({
    config: config,
    dependencies: agentDependencies,
    modules: {
      askar: new AskarModule({
        ariesAskar,
      }),
      openId4VcClient: new OpenId4VcClientModule(),
    },
  });

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

const main = async (): Promise<void> => {
  const holder = await initializeHolder();

  // コールバック関数
  // JWT-VC上のsubに当たるHolder DIDとそのVerification Methodを決定する。
  // Verification MethodはProof of Possession(JWT)の署名に使われる。
  const resolver: ProofOfPossessionVerificationMethodResolver = async (
    resolverOption: ProofOfPossessionVerificationMethodResolverOptions
  ): Promise<VerificationMethod> => {
    // optionの内容を確認しながら、新規のDIDを作るか、既存の中から選択するかなどロジックを組み立てられる。
    // 今回はシンプルにresloverOption.keyType(今回の場合はed25519)だけを用いて、did:keyを新規作成する。

    logger.info(
      `ProofOfPossessionVerificationMethodResolverOptions: ${JSON.stringify(
        resolverOption,
        null,
        2
      )}`
    );

    const did = await holder.dids.create<KeyDidCreateOptions>({
      method: "key",
      options: {
        keyType: resolverOption.keyType,
      },
    });

    const { didDocument } = did.didState;
    if (didDocument && didDocument.verificationMethod) {
      // TODO 今回は実質的に関係ないが、本来はauthentication(verification relationship)を返すべき
      return didDocument.verificationMethod[0];
    } else {
      throw new Error("didDocument or verificationMethod is not found.");
    }
  };

  const preAuthCodeFlowOptions: PreAuthCodeFlowOptions = {
    // ACA-Py(Issuer)で作成したCredential OfferのURLエンコードの直書き
    issuerUri:
      "openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22http%3A%2F%2F172.19.0.3%3A8032%22%2C%22credentials%22%3A%5B%22PermanentResidentCredential%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22Xp03frRKPfHjKYKFMn9juA%22%2C%22user_pin_required%22%3Atrue%7D%7D%7D",
    proofOfPossessionVerificationMethodResolver: resolver,
    pin: "12345",
    verifyCredentialStatus: true,
  };

  //  Metadata、Access Token、JWT-VCの取得までを一気に行うメソッドの呼び出し
  const jwtVc =
    await holder.modules.openId4VcClient.requestCredentialUsingPreAuthorizedCode(
      preAuthCodeFlowOptions
    );

  logger.info(`jwt-vc issued: ${JSON.stringify(jwtVc, null, 2)}}`);

  // Agentへの保管の確認
  const jwtVcRetrieved = await holder.w3cCredentials.getCredentialRecordById(
    jwtVc.id
  );
  logger.info(`jwt-vc retrieved: ${JSON.stringify(jwtVcRetrieved, null, 2)}}`);
};

logger.info("start oid4vci-holder-agent");
await main();
logger.info("end oid4vci-holder-agent");

以下、アプリケーションコードのポイントです。

ポイント1: Offer URIの埋め込み

preAuthCodeFlowOptions.issuerUriに、後述するACA-Pyで作成したCredential OfferをURLエンコードして直接埋め込んでいます。スキーム(openid-credential-offer://)は、仕様の10.1. Client Metadata部分に則って記述しましたが、今回はモバイルアプリ型のHolder WalletでQRコード読込みやDeep Linkクリックをしないため、特に何も作用しません。

ポイント2: Holder DIDとそのVerification Method(公開鍵)の選定

コールバック関数であるproofOfPossessionVerificationMethodResolverの実装についてです。この関数はその名の通り、Access Tokenを取得した後にProof of Posession作成のために呼び出されます。

Credoの実装として、Credential Offer内のcredentials(型はstring[][], e.g. [[foo,bar], [卒業証明書], [運転免許証]])の要素ごとに、つまりCredentialの種別(string[])ごとに繰り返しVCの発行を要求します。

関数のパラメーターProofOfPossessionVerificationMethodResolverOptionsには、IssuerのCredential Offer内の上記Credential種別をキーに、Credential Issuer Metadata内の(後述する)credentials_supported配列の1要素を抽出した結果(DIDメソッド、署名アルゴリズム、VCフォーマット)と、Holder(Credo)で対応可能なそれらの情報を突き合わせて合致した内容が入っています。 これはProof of Posessonの作成、検証においてIssuer/Holder双方が対応できるように、またVC発行後にHolderがVP作成でそれを問題なく取り扱えるようにするためだと考えます。(詳しくは、OpenId4VcClientService#getCredentialRequestOptions(..)OpenId4VcClientService#getProofOfPossessionRequirements(..)をご参照ください。)

アプリケーションにはProofOfPossessionVerificationMethodResolverOptionsパラメーターの情報から、VCのsubの値に成るHolder DIDと、Proof生成のためのVerification Method(id, type, controller, 公開鍵)を決定するロジックを実装をします。

例えば、名寄せを気にするならVC発行ごとに新しい鍵を作る、または特定のcredentialType(e.g.卒業証明書)に対し、Wallet内の既存の特定のDIDを使うなどの設計ができます。今回は、シンプルにACA-Py側で対応可能なdid:key(署名アルゴリズム: EdDSA)を新規作成しています。

ここから試行に入ります。

試行

1. ACA-Py: credentials_supportedの作成

Admin APIから、Credential Issuer Metadataの必須プロパティであるcredentials_supportedを作ります。

以下のRequest Bodyの内容について述べます。

  • 上から4つのプロパティ(format, id, cryptographic_binding_methods_supported, cryptographic_suites_supported)は、仕様の10.2.3. Credential Issuer Metadata Parametersで定義されています。
  • cryptographic_binding_methods_supportedとcryptographic_suites_supportedについてです。前述の実装状況から、Holder DIDによるProof作成(nonce署名)にACA-Pyが対応できるモノは限られており、それぞれdid:keyとEdDSAに設定しています。(Credo側でもこれらに対応しているため機能します。前述のHolderアプリコードの説明”ポイント2: Holder DIDとそのVerification Method(公開鍵)の選定”をご参照ください。)
  • format_data内の2つのプロパティは、formatが”jwt_vc_json”の場合に追加されるものです。仕様のE.1.1. VC signed as a JWT, not using JSON-LDで定義されています。
  • これらの中で必須なプロパティは、formatと(format_data配下の)typesのみです。
  • 記述しなかったオプションのプロパティもあります。詳細は仕様をご確認ください。
  • vc_additional_dataはOID4VCIの仕様とは直接関係ないものです。ここには発行するVC上に追加したい情報を記載します。下記の通り、VCの必須プロパティである@contextとtypeを記載しています。vc_additional_data無しではこれらの情報がVCに記載されないためです。
# request
curl -X 'POST' \
  'http://localhost:8031/oid4vci/credential-supported/create' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "format":"jwt_vc_json",
  "id":"PermanentResidentCredential",
  "cryptographic_binding_methods_supported":[
    "did:key"
  ],
  "cryptographic_suites_supported":[
    "EdDSA"
  ],
  "format_data":{
    "credentialSubject":{
      "givenName":{
        "display":[
          {
            "name":"名前"
          }
        ]
      },
      "familyName":{
        "display":[
          {
            "name":"名字"
          }
        ]
      },
      "gender":{
        "display":[
          {
            "name":"性別"
          }
        ]
      },
      "birthCountry":{
        "display":[
          {
            "name":"出身国"
          }
        ]
      },
      "birthDate":{
        "display":[
          {
            "name":"生年月日"
          }
        ]
      }
    },
    "types":[
      "PermanentResidentCredential"
    ]
  },
  "vc_additional_data": {
    "@context": [
      "https://www.w3.org/2018/credentials/v1"
    ],
    "type": [
      "VerifiableCredential",
      "PermanentResidentCredential"
    ]
  }
}'

ResponseとしてACA-Py内で扱うためのsupported_cred_idが振られます。

# response
{
  "created_at": "2024-01-29T07:10:36.592008Z",
  "updated_at": "2024-01-29T07:10:36.592008Z",
  "supported_cred_id": "8f2e422a-228b-4428-ad42-e1105182f5af",
  "format": "jwt_vc_json",
  "identifier": "PermanentResidentCredential",
  "cryptographic_binding_methods_supported": [
    "did:key"
  ],
  "cryptographic_suites_supported": [
    "EdDSA"
  ],
  "format_data": {
    "credentialSubject": {
      "givenName": {
        "display": [
          {
            "name": "名前"
          }
        ]
      },
      "familyName": {
        "display": [
          {
            "name": "名字"
          }
        ]
      },
      "gender": {
        "display": [
          {
            "name": "性別"
          }
        ]
      },
      "birthCountry": {
        "display": [
          {
            "name": "出身国"
          }
        ]
      },
      "birthDate": {
        "display": [
          {
            "name": "生年月日"
          }
        ]
      }
    },
    "types": [
      "PermanentResidentCredential"
    ]
  },
  "vc_additional_data": {
    "@context": [
      "https://www.w3.org/2018/credentials/v1"
    ],
    "type": [
      "VerifiableCredential",
      "PermanentResidentCredential"
    ]
  }
}

2. ACA-Py: VCに署名するDID(did:web)の作成

Admin APIからdid:webの鍵ペアを作成し、それを手作成したDID Documentに記載してAWSにホストします。以下のdid:webの拙著の内容と同様であるため、割愛します。

【Hyperledger Aries】did:webでLDP-VCの発行と検証を試す

3. ACA-Py: Credential Offerの作成

  • Admin APIから、No.1、2の内容を基にしてExchange Recordを作ります。
    • ACA-Py内で、このExchange RecordにCredential Offer、Access Token、JWT-VCが紐付いて管理される模様です。
  • credential_subjectプロパティにて、Claim値を設定しています。
  • Holderにメールなどで送り、WalletでCredential Offerを読み込む際にUIで入力してもらう形式が典型であろうPINコード(オプション)をここで付与します。
# request
curl -X 'POST' \
  'http://localhost:8031/oid4vci/exchange/create' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "did": "did:web:dijx0shbtuy59.cloudfront.net",
  "supported_cred_id": "8f2e422a-228b-4428-ad42-e1105182f5af",
  "verification_method": "did:web:dijx0shbtuy59.cloudfront.net#key-1",
  "pin":"12345",
  "credential_subject": {
      "givenName": "Taro",
      "familyName": "Yamada",
      "gender": "Male",
      "birthCountry": "Japan",
      "birthDate": "2020-01-01"
    }
}'

Responseとして ACA-Py内でExchange Recordに対するID(exchange_id)が振られます。

# response
{
  "state": "created",
  "created_at": "2024-01-29T07:16:13.972733Z",
  "updated_at": "2024-01-29T07:16:13.972733Z",
  "exchange_id": "53dc7b1a-3560-4c87-b348-f0308092ac51",
  "supported_cred_id": "8f2e422a-228b-4428-ad42-e1105182f5af",
  "credential_subject": {
    "givenName": "Taro",
    "familyName": "Yamada",
    "gender": "Male",
    "birthCountry": "Japan",
    "birthDate": "2020-01-01"
  },
  "verification_method": "did:web:dijx0shbtuy59.cloudfront.net#key-1",
  "pin": "12345"
}

Admin APIから、上記のExchange Record IDを基に出来上がったCredential Offerを取得します。

# request
curl -X 'GET' \
  'http://localhost:8031/oid4vci/credential-offer?exchange_id=53dc7b1a-3560-4c87-b348-f0308092ac51' \
  -H 'accept: application/json'
作成したCredential Offer

以下、Credential Offerです。仕様の4.1.1. Credential Offer Parametersの通りになっていることが見て取れます。

Holderはオファー読み取り後、credential_issuerのURLに対しCredential Issuer Metdata取得のリクエストを投げ、上記でACA-Py側で作成したcredentials_supportedやCredential Endpoint情報を取得します。

credentials[]のPermanentResidentCredentialは上記で作成したcredentials_supportedのIDです。

grantsプロパティには、Access Token取得のためにToken Endpointへのリクエストに含めるPre-Authorized Codeがあり、またそこに事前にHolderに(例えばメールなどで)通知したPINコードを含める必要があることを示しています。

# response
{
  "credential_issuer": "http://172.19.0.3:8032",
  "credentials": [
    "PermanentResidentCredential"
  ],
  "grants": {
    "urn:ietf:params:oauth:grant-type:pre-authorized_code": {
      "pre-authorized_code": "Xp03frRKPfHjKYKFMn9juA",
      "user_pin_required": true
    }
  }
}

4. Holder Agent: 起動とCredential Offerの読み込み

Holder Agentを起動します。
上記で示した通り、No.3で作成したCredential OfferをURLエンコードしてアプリコードに埋め込み読み込ませます。(またUser Pinもハードコードしています。)

(実際のユースケースの典型例としては、QRコード、もしくはDeep LinkにしたCredential OfferをHolderにモバイルデバイスでそれを読むかクリックしてもらい、Holder Walletを起動し、その際にHolderにUser Pinの入力を求める形になると思います。)

ここからは筆者を介すことなく、ソフトウェア間でプロトコルが進められます。

5. Holder Agent: Credential Issuer Metadataの取得

以下、ログに出力されたMetadataです。
Authorization Server Metadataと合わさっている模様です。
コードを読む限り、ACA-Pyの方でAuthorization Server Metadataは提供していないと思います。おそらくCredoの方でIssuer Metadataの中にauthorization_serverプロパティがないことからACA-PyがIssuer兼Authorization Serverであると認識し、Credo側で作ったデータだと思われます。

{
  "issuer": "http://172.19.0.3:8032",
  "token_endpoint": "http://172.19.0.3:8032/token",
  "credential_endpoint": "http://172.19.0.3:8032/credential",
  "authorization_server": "http://172.19.0.3:8032",
  "authorizationServerType": "OID4VCI",
  "credentialIssuerMetadata": {
    "credential_issuer": "http://172.19.0.3:8032/",
    "credential_endpoint": "http://172.19.0.3:8032/credential",
    "credentials_supported": [
      {
        "format": "jwt_vc_json",
        "cryptographic_binding_methods_supported": ["did:key"],
        "cryptographic_suites_supported": ["EdDSA"],
        "id": "PermanentResidentCredential",
        "credentialSubject": {
          "givenName": {
            "display": [
              {
                "name": "名前"
              }
            ]
          },
          "familyName": {
            "display": [
              {
                "name": "名字"
              }
            ]
          },
          "gender": {
            "display": [
              {
                "name": "性別"
              }
            ]
          },
          "birthCountry": {
            "display": [
              {
                "name": "出身国"
              }
            ]
          },
          "birthDate": {
            "display": [
              {
                "name": "生年月日"
              }
            ]
          }
        },
        "types": ["PermanentResidentCredential"]
      }
    ]
  }
}

6. Holder Agent: Access Tokenのリクエストとレスポンス

オファーのpre-authorized codeとPINコードを含めて、Issuer Metadata内のToken Endpointに対してAccess Tokenとnonce取得のHTTPリクエストを投げます。

以下、ログに出力されたAccess Token(JWT)です。

  • Issuerのdid:webで署名されています。
  • 有効期間は1日です。(ACA-Py OID4VCI Plugin内にハードコードされています)
  • payloadのidは前述のACA-Py内で管理されるExchange RecordのIDになっています。

header

{
  "typ": "JWT",
  "alg": "EdDSA",
  "kid": "did:web:dijx0shbtuy59.cloudfront.net#key-1"
}

payload

{
  "id": "53dc7b1a-3560-4c87-b348-f0308092ac51",
  "exp": 1706599409.892043
}

7. Verification Methodの決定とProof of Possessionの作成

上記コールバック関数が動き、Proof of Possesion作成のためにJWT-VC上のsubに成るHolder DIDのVerification Methodが決定されます。

コールバック関数のパラメーターProofOfPossessionVerificationMethodResolverOptionsの今回の内容はログ出力より以下の通りです。

{
  "credentialFormat":"jwt_vc_json",
  "proofOfPossessionSignatureAlgorithm":"EdDSA",
  "supportedVerificationMethods":[
    "Ed25519VerificationKey2018",
    "Ed25519VerificationKey2020",
    "JsonWebKey2020"
  ],
  "keyType":"ed25519",
  "credentialType":[
    "PermanentResidentCredential"
  ],
  "supportsAllDidMethods":false,
  "supportedDidMethods":[
    "did:key"
  ]
}

以下、ログに出力されたProof of Posession(JWT)です。

  • Holder側で新規作成したdid:keyで署名されています。
  • 有効期間は10分になっています。(Sphereonのライブラリ内でハードコードされていると思います)

header

{
  "typ": "openid4vci-proof+jwt",
  "alg": "EdDSA",
  "kid": "did:key:z6Mkt4tyNA65DrACnSk45byGTZYpXag2soZMBqD1mVKEMQJW#z6Mkt4tyNA65DrACnSk45byGTZYpXag2soZMBqD1mVKEMQJW"
}

payload

{
  "aud": "http://172.19.0.3:8032",
  "iat": 1706512949.937,
  "exp": 1706513609.937,
  "nonce": "Bfe0fI0TBUX2fMQLrRz4vQ"
}

また以下はHolderのdid:keyのDID Documentです。

{
  "@context": [
    "https://www.w3.org/ns/did/v1",
    {
      "Ed25519VerificationKey2018": "https://w3id.org/security#Ed25519VerificationKey2018",
      "publicKeyJwk": {
        "@id": "https://w3id.org/security#publicKeyJwk",
        "@type": "@json"
      }
    }
  ],
  "id": "did:key:z6Mkt4tyNA65DrACnSk45byGTZYpXag2soZMBqD1mVKEMQJW",
  "verificationMethod": [
    {
      "id": "did:key:z6Mkt4tyNA65DrACnSk45byGTZYpXag2soZMBqD1mVKEMQJW#z6Mkt4tyNA65DrACnSk45byGTZYpXag2soZMBqD1mVKEMQJW",
      "type": "Ed25519VerificationKey2018",
      "controller": "did:key:z6Mkt4tyNA65DrACnSk45byGTZYpXag2soZMBqD1mVKEMQJW",
      "publicKeyJwk": {
        "kty": "OKP",
        "crv": "Ed25519",
        "x": "ykkDE2MBfKGyDuz5Dvjt7KtoF58qL_apDFXBau9pkWM"
      }
    }
  ],
  "authentication": [
    "did:key:z6Mkt4tyNA65DrACnSk45byGTZYpXag2soZMBqD1mVKEMQJW#z6Mkt4tyNA65DrACnSk45byGTZYpXag2soZMBqD1mVKEMQJW"
  ],
  "assertionMethod": [
    "did:key:z6Mkt4tyNA65DrACnSk45byGTZYpXag2soZMBqD1mVKEMQJW#z6Mkt4tyNA65DrACnSk45byGTZYpXag2soZMBqD1mVKEMQJW"
  ]
}

8. Holder Agent: JWT-VCのリクエストとレスポンス(発行)

Credential Endpointに対してHTTPリクエストを投げJWT-VCを取得します。

以下、ログに出力されたJWT-VCです。
payloadのvc.idがURIでないこと、またvc.credentialSubject.id、sub、issがDIDではなくDID URLとしてVerification Methodを指しているところが気になりますが、とりあえずVCの発行までを完遂できました。

header

{
  "typ": "JWT",
  "alg": "EdDSA",
  "kid": "did:web:dijx0shbtuy59.cloudfront.net#key-1"
}

payload

{
  "vc":{
    "@context":[
      "https://www.w3.org/2018/credentials/v1"
    ],
    "type":[
      "VerifiableCredential",
      "PermanentResidentCredential"
    ],
    "id":"05d03459-65ca-45c4-812e-02a221d308a1",
    "issuer":"did:web:dijx0shbtuy59.cloudfront.net#key-1",
    "issuanceDate":"2024-01-29T07:23:30Z",
    "credentialSubject":{
      "givenName":"Taro",
      "familyName":"Yamada",
      "gender":"Male",
      "birthCountry":"Japan",
      "birthDate":"2020-01-01",
      "id":"did:key:z6Mkt4tyNA65DrACnSk45byGTZYpXag2soZMBqD1mVKEMQJW#z6Mkt4tyNA65DrACnSk45byGTZYpXag2soZMBqD1mVKEMQJW"
    }
  },
  "iss":"did:web:dijx0shbtuy59.cloudfront.net#key-1",
  "nbf":1706513010,
  "jti":"05d03459-65ca-45c4-812e-02a221d308a1",
  "sub":"did:key:z6Mkt4tyNA65DrACnSk45byGTZYpXag2soZMBqD1mVKEMQJW#z6Mkt4tyNA65DrACnSk45byGTZYpXag2soZMBqD1mVKEMQJW"
}

おわりに

Hyperledger Ariesは目指す姿と現状のWebの姿の間にあるギャップが比較的大きく、その分ConnectionやMediationなど従来には無い面倒なことが必要になってくると考えます。今回(4VCIのPre-Authorized Code Flowのみで、また簡略的な試行ではありますが)OID4VCのシンプルさ、支持が高まっている理由を実感できたと思います。(AriesとOID4VCの考え方の違いについての私見は、Speaker Deck: Hyperledger Ariesの全体像、現況、アプリ開発手法について(P.63~)で述べています。)

OID4VCのオープンソース実装しては、現状Sphereon社walt.id社のモノが知られていると思いますが、ACA-PyとCredoにおけるOID4VCとEUDIW ARF関連の取り込みについても追っていきたいと思います。どなたかのお役に立てたならば幸いです。