👋

Verifiable Credentials のフローを walt.id で試してみよう(VCハンズオン)

に公開

はじめに

この記事は何?

Verifiable Credentials(VC)の概念を理解したうえで、実際に「VC発行 → 受領 → 提示 → 検証」の一連のフローを自力で回せるようになるためのハンズオン記事です。

VC概念、やることのざっくりイメージ

シンプルにいうと、VCはデジタル世界における証明書です。実世界では免許証などを使って年齢確認(検証)を行いますが、年齢確認にとどまらず、色々な場面において検証可能なデジタルの証明書のことをVCといいます。
物理的な免許証やクレジットカードは財布の中に入れて保管しますね。これと同様に、デジタル世界の証明書であるVCはデジタルのアイデンティティを保管するためのウォレット(Digital Identity Wallet, DIW)に保管します。
このDIW自体、またはDIWを扱うためのインフラはいろいろな組織がそれぞれの形で作っていますが、walt.id というヨーロッパ・オーストリアの企業はそれをオープンソースとして公開しています(Community Stack)。
今回はこのwalt.id Community Stackを使ってVC発行 → 受領 → 提示 → 検証」の一連のフローを試します。
具体例として、航空機の搭乗チケットを航空会社がVCとして発行→搭乗者がVC受領→搭乗者がカウンターでVC提示→カウンター係員がVC検証という流れをイメージしながらハンズオンを行います。

この記事では扱わないこと

  • VC/VPの基礎
  • OID4VCI/OID4VP、SD-JWT の概要

必要な環境

  • Git
  • Docker

ハンズオン

1. walt.id Community Stack のDockerを起動

公式ドキュメント: https://docs.walt.id/community-stack/home#quick-start

まずはCommunity Stack のリポジトリを手元にクローンし、Docker Compose で起動するところから始めます。

git clone https://github.com/walt-id/waltid-identity.git && cd waltid-identity
cd docker-compose && docker compose up

このリポジトリの中にはIssuer/Verifier/Walletに関するそれぞれの内容がすべて入っているため、コンテナを起動するとそれだけですべての環境が一発で用意されます。
起動できたら、ターミナルの別タブでコンテナの状況を確認してみましょう。

docker-compose ps

下記のような形でコンテナが動いているはずです。

$ docker-compose ps
NAME                                  IMAGE                              COMMAND                   SERVICE              CREATED       STATUS                 PORTS
docker-compose-caddy-1                docker.io/caddy:2                  "caddy run --config …"   caddy                2 hours ago   Up 2 hours             0.0.0.0:7001-7003->7001-7003/tcp, [::]:7001-7003->7001-7003/tcp, 0.0.0.0:7101-7104->7101-7104/tcp, [::]:7101-7104->7101-7104/tcp, 0.0.0.0:8080->8080/tcp, [::]:8080->8080/tcp
docker-compose-issuer-api-1           waltid/issuer-api:0.15.0           "/waltid-issuer-api/…"   issuer-api           2 hours ago   Up 2 hours             7002/tcp
docker-compose-postgres-1             postgres                           "docker-entrypoint.s…"   postgres             2 hours ago   Up 2 hours (healthy)   0.0.0.0:5432->5432/tcp, [::]:5432->5432/tcp
docker-compose-vc-repo-1              waltid/vc-repository:latest        "/bin/sh -c 'node se…"   vc-repo              2 hours ago   Up 2 hours             3000/tcp
docker-compose-verifier-api-1         waltid/verifier-api:0.15.0         "/waltid-verifier-ap…"   verifier-api         2 hours ago   Up 2 hours             7003/tcp
docker-compose-wallet-api-1           waltid/wallet-api:0.15.0           "/waltid-wallet-api/…"   wallet-api           2 hours ago   Up 2 hours             7001/tcp
docker-compose-waltid-demo-wallet-1   waltid/waltid-demo-wallet:0.15.0   "node server/index.m…"   waltid-demo-wallet   2 hours ago   Up 2 hours             7101/tcp
docker-compose-waltid-dev-wallet-1    waltid/waltid-dev-wallet:0.15.0    "node server/index.m…"   waltid-dev-wallet    2 hours ago   Up 2 hours             7104/tcp
docker-compose-web-portal-1           waltid/portal:0.15.0               "docker-entrypoint.s…"   web-portal           2 hours ago   Up 2 hours             7102/tcp

各種APIのベースとなるURLは下記の通りです。アクセスすると、それぞれSwaggerのUIからAPIコールができます。

URL API種別
http://localhost:7001/ wallet
http://localhost:7002/ issuer
http://localhost:7003/ verifier

またポート番号に100を足す(e.g. 7001→7101)と、Web用のGUIにアクセスができます。お手軽に操作を試したいときに便利です。

URL ページ内容 画面
http://localhost:7101/ Webで操作可能なウォレット(簡易版) DIW login
http://localhost:7102/ VC発行・検証用のポータルページ VC issuer/verifier portal
http://localhost:7103/ W3C準拠のVCスキーマ集 type docs
http://localhost:7104/ Webで操作可能なウォレット(フル版) SSI wallet

2. VC発行

まずはissuerである航空会社として、航空券のVCを発行します。

2-1. issue用秘密鍵発行

VCのissueに必要な秘密鍵を発行するAPIをコールします。
手軽にハンズオンを進める関係で、Hashicorp VaultやOracle VaultといったKMSは使用せず、自身でキー管理する形で発行をします(自己管理型のキー発行 公式ドキュメント)。自己管理するキーの場合、APIリクエストbodyのkey.backendには jwk を指定します。
APIのレスポンスに含まれる秘密鍵とDIDは後で使いますので、テキストファイルなどにメモしておいてください。
実際のVC発行シーンでは、各航空会社がパブリッククラウドなどのインフラ上で秘密鍵を管理します。

項目
APIパス /onboard/issuer
HTTP Method POST

リクエスト(curl)

curl -X 'POST' \
  'http://localhost:7002/onboard/issuer' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "key": {
    "backend": "jwk",
    "keyType": "secp256r1"
  },
  "did": {
    "method": "jwk"
  }
}'

レスポンス例

{
  "issuerKey": {
    "type": "jwk",
    "jwk": "{\"kty\":\"EC\",\"d\":\"nnITt7w11Gehk_oA9bxOQrz1GGzMECiBNoVejKOn2CA\",\"crv\":\"P-256\",\"kid\":\"fQod7zGbGsO14yVnNT-cslwFyKgEPjKsYHQIfGqQMFI\",\"x\":\"EJHmd2wZpmLCnmN49bQeOSn6NDb8kfeXrfpr1K1U8FY\",\"y\":\"Wxzi0fayJU80JJZOl1-XUrZzEU5AcAQxUdO69-gmBmc\"}"
  },
  "issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Iiwia2lkIjoiZlFvZDd6R2JHc08xNHlWbk5ULWNzbHdGeUtnRVBqS3NZSFFJZkdxUU1GSSIsIngiOiJFSkhtZDJ3WnBtTENubU40OWJRZU9TbjZORGI4a2ZlWHJmcHIxSzFVOEZZIiwieSI6Ild4emkwZmF5SlU4MEpKWk9sMS1YVXJaekVVNUFjQVF4VWRPNjktZ21CbWMifQ"
}

備考

  • key.backend は鍵の保存場所
    • jwk ... 保存せず自身で管理
    • tse ... Hashicorp valut
    • その他の設定については公式ドキュメントを参照
  • key.keyType は秘密鍵の暗号化方式。自身で管理する形のキーは下記の方式に対応している
    • ed25519
    • secp256k1
    • secp256r1
    • RSA

2-2. VC発行 (JWT)

公式ドキュメント: https://docs.walt.id/community-stack/issuer/api/credential-issuance/vc-oid4vc
ステップ2-1で作成した秘密鍵、DIDを使って、航空券VCを発行します。
VCの形式はいろいろありますが、このハンズオンではW3C VC(JWT)のみ扱います。
またこの記事では独自のVCスキーマは扱わず、すでに定義されているスキーマ BoardingPass(https://credentials.walt.id/w3c-credentials/boardingpass またはローカルの http://localhost:7103/credentials/boardingpass でVC形式を確認可能)を使用します。

BoardingPass VC の形式は下記の通りです。
credentialSubjectに搭乗者や搭乗便の情報が記載されています。

{
    "@context": ["https://www.w3.org/2018/credentials/v1"],
    "type": ["VerifiableCredential", "VerifiableAttestation", "BoardingPass"],
    "id": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION",
    "credentialSubject": {
        "id": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION",
        "firstName": "TARO",
        "lastName": "YAMADA",
        "seat": "10A",
        "flight": "NH721",
        "date": "2025-12-04"
    },
    "issuer": {
        "id": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION",
        "name": "Airline"
    },
    "issuanceDate": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION",
    "expirationDate": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION"
}
項目
APIパス /openid4vc/jwt/issue
HTTP Method POST

credentialConfigurationId には BoardingPass_jwt_vc_json を指定します。
このカラムの値は {既存の定義済スキーマ名}_{署名形式} となります。

mapping ではwalt.idで使えるデータ関数とカラムのマッピングを指定します。これにより、VCが発行時にはマッピング先のカラムの値が自動的に入力されます。
使用できるデータ関数は 公式ドキュメント を参照ください。

リクエスト(curl)

curl -X 'POST' \
  'http://localhost:7002/openid4vc/jwt/issue' \
  -H 'accept: text/plain' \
  -H 'statusCallbackUri: https://example.com/$id' \
  -H 'Content-Type: application/json' \
  -d '{
  "issuerKey": {
    "type": "jwk",
    "jwk":{
      "kty": "EC",
      "d": "nnITt7w11Gehk_oA9bxOQrz1GGzMECiBNoVejKOn2CA",
      "crv": "P-256",
      "kid": "fQod7zGbGsO14yVnNT-cslwFyKgEPjKsYHQIfGqQMFI",
      "x": "EJHmd2wZpmLCnmN49bQeOSn6NDb8kfeXrfpr1K1U8FY",
      "y": "Wxzi0fayJU80JJZOl1-XUrZzEU5AcAQxUdO69-gmBmc"
    }
  },
  "issuerDid": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Iiwia2lkIjoiZlFvZDd6R2JHc08xNHlWbk5ULWNzbHdGeUtnRVBqS3NZSFFJZkdxUU1GSSIsIngiOiJFSkhtZDJ3WnBtTENubU40OWJRZU9TbjZORGI4a2ZlWHJmcHIxSzFVOEZZIiwieSI6Ild4emkwZmF5SlU4MEpKWk9sMS1YVXJaekVVNUFjQVF4VWRPNjktZ21CbWMifQ",
  "credentialConfigurationId": "BoardingPass_jwt_vc_json",
  "credentialData": {
    "@context": ["https://www.w3.org/2018/credentials/v1"],
    "type": ["VerifiableCredential", "VerifiableAttestation", "BoardingPass"],
    "id": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION",
    "credentialSubject": {
      "id": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION",
      "firstName": "TARO",
      "lastName": "YAMADA",
      "seat": "10A",
      "flight": "EX123",
      "date": "2025-12-04"
    },
    "issuer": {
        "id": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION",
        "name": "Example Airline"
    },
    "issuanceDate": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION",
    "expirationDate": "THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION"
  },
  "mapping": {
    "id": "<uuid>",
    "issuer": {
        "id": "<issuerDid>"
    },
    "credentialSubject": {
        "id": "<subjectDid>"
    },
    "issuanceDate": "<timestamp>",
    "validFrom": "<timestamp>",
    "expirationDate": "<timestamp-in:365d>"
  },
  "authenticationMethod": "PRE_AUTHORIZED",
  "standardVersion": "DRAFT13"
}'

APIコールが成功すると、OID4VCI形式のVC発行用URLが返ります。
航空会社の航空券予約サイト上でこのURLをQRコード化して表示し、搭乗者がウォレットアプリで読み込むことで、搭乗者に対してVCが発行できたことになります。

レスポンス例

openid-credential-offer://localhost:7002/draft13/?credential_offer_uri=http%3A%2F%2Flocalhost%3A7002%2Fdraft13%2FcredentialOffer%3Fid%3Dad8d849f-1c12-4e01-85a8-84457ecb3c31

ここまでが issuer である航空会社が行うフローです。次は搭乗者側(holder, wallet側)に移ります。

3. VC受領

搭乗者であるHolderは、VCを受け取り自身のウォレットに保存します。
walt.idから発行されたVCが受け取れればどんなウォレットを使ってもよいのですが、VC受け取り機能がついたWebベースのウォレットがDockerコンテナ上で起動しているので、今回はこちらを使って受領を行うことにします。

Webベースのウォレットは http://localhost:7101 でアクセス出来ます。
まずはアカウントを作るため、http://localhost:7101/signup にアクセスし名前とID・パスワードを入力の上アカウントを作成しましょう。

アカウントを作成したら、IDとパスワードを使ってWebウォレットにログインします。
ログインすると、自身が保有するVCの一覧画面に遷移します。新規登録したてなので、当然1つもVCは持っていません。
航空会社から発行されたVCを受領するため、Receive Credential をクリックします。

クリックすると、VC発行URLの直接入力、またはVC発行URLのQRコードのスキャンをすることでVCが受け取れる画面に遷移します。
今回はVC受け取り用URLを直接入力しVCを受け取ります。 Offer URL が青くなっている状態でフォームにVC発行用URL(2.VC発行の最後に出てきた openid-credential-offer://~~~)をコピペします。

コピペをするとVC受け取りに進むボタン Receive Credential が出現するので、ボタンをクリック。

搭乗券VCを受け取るかどうかの確認になるので、Accept をクリックしVCを受領します。

これでExample Airlineからの搭乗券VC受領に成功しました。VCはWebウォレットに保管されており、要求に応じていつでも提示可能な状態になっています。
先述の通り、本来であればHolderが自由にウォレットを選べるので、例えばモバイルアプリのウォレットで受け取ったのであればVCは端末のローカルに保存されるかもしれません。

4. VC提示・検証

さて、Holderである山田太郎さんは搭乗当日に預け入れ荷物の手続きをしに空港の手荷物カウンターに来ました。
ここではHolderがVerifierであるカウンター職員にVCを提示する流れを見てみます。

公式ドキュメント: https://docs.walt.id/community-stack/verifier/api/credential-verification/vc-oid4vc
VCの提示要求を出すためのVerifier用APIをコールします。Boarding Pass形式のJWT型VCだけを受け付けたいので、その旨をBodyに記述します。

項目
ホスト localhost:7003
APIパス /openid4vc/verify
HTTP Method POST
{
  "request_credentials": [
    {
      "format": "jwt_vc_json",
      "type": "BoardingPass"
    }
  ]
}

レスポンス例:

openid4vp://authorize?response_type=vp_token&client_id=http%3A%2F%2Flocalhost%3A7003%2Fopenid4vc%2Fverify&response_mode=direct_post&state=9Ay3acw9yqM7&presentation_definition_uri=http%3A%2F%2Flocalhost%3A7003%2Fopenid4vc%2Fpd%2F9Ay3acw9yqM7&client_id_scheme=redirect_uri&client_metadata=%7B%22authorization_encrypted_response_alg%22%3A%22ECDH-ES%22%2C%22authorization_encrypted_response_enc%22%3A%22A256GCM%22%7D&nonce=c9b43c1c-1ae9-4c20-9f05-02e53ea0d830&response_uri=http%3A%2F%2Flocalhost%3A7003%2Fopenid4vc%2Fverify%2F9Ay3acw9yqM7

これでVC提示要求用のURL(OID4VP形式)が発行できました。URLのうち、クエリパラメータの stateは後で使うのでメモを取っておいてください(上記の例だと state の値は 9Ay3acw9yqM7)。
航空会社はこのURLをQRコードとして搭乗者に表示し、搭乗者はそれをスキャンすることでVC提示を進めることができます。

それではこのVC提示要求用のURLを搭乗者のWebウォレット上で入力し、VCを提示してみましょう。
Webウォレット上でURL入力の画面を開きます。入力するフォームはVC受領時と同じですが、今回入力するのはVC提示要求用URLです。

入力されたURL形式がVC提示要求用URLであれば、「Present credential!」ボタンが出現します。これをクリックします。

提示するVCの内容を確認します。Example Airline から発行された搭乗券VCを提示しますので、これで合っていますね。

「Disclose」をクリックしてVCを提示すると、提示に成功した旨のアラート表示がされます。これでVC提示は完了です。

Verifier側のVC提示状況確認APIを叩いて、VC提示と検証が成功しているか確認してみます。
このとき、パスパラメータの state はVC提示要求用URLのクエリパラメータに入っていた state の値を指定します。

項目
ホスト localhost:7003
APIパス /openid4vc/session/{state}
HTTP Method GET

レスポンス例:

{
  "id": "9Ay3acw9yqM7",
  "presentationDefinition": {
    "id": "kR7DhcAYfUfS",
    "input_descriptors": [
      {
        "id": "BoardingPass",
        "format": {
          "jwt_vc_json": {
            "alg": [
              "EdDSA"
            ]
          }
        },
        "constraints": {
          "fields": [
            {
              "path": [
                "$.vc.type"
              ],
              "filter": {
                "type": "string",
                "pattern": "BoardingPass"
              }
            }
          ]
        }
      }
    ]
  },
  "tokenResponse": {
    "vp_token": "eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaVlXTnZWa2x0YVVWbVozcElaRVJhZUdGM2RIRXhXV2xqUkdSV2RVNVdPQzFVUjNKMGEzQjVZbFI2WnlJc0luZ2lPaUp0VkRkWldtb3RabmxmUVZWVWVUTlJja1pTWjA5dFRUYzNZV3gxTjBWcmJqVTNOV3hOTjFoMldrOXJJbjAjMCIsInR5cCI6IkpXVCIsImFsZyI6IkVkRFNBIn0.eyJzdWIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaVlXTnZWa2x0YVVWbVozcElaRVJhZUdGM2RIRXhXV2xqUkdSV2RVNVdPQzFVUjNKMGEzQjVZbFI2WnlJc0luZ2lPaUp0VkRkWldtb3RabmxmUVZWVWVUTlJja1pTWjA5dFRUYzNZV3gxTjBWcmJqVTNOV3hOTjFoMldrOXJJbjAiLCJuYmYiOjE3NjQ4MDUzMTQsImlhdCI6MTc2NDgwNTM3NCwianRpIjoia1I3RGhjQVlmVWZTIiwiaXNzIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lZV052VmtsdGFVVm1aM3BJWkVSYWVHRjNkSEV4V1dsalJHUldkVTVXT0MxVVIzSjBhM0I1WWxSNlp5SXNJbmdpT2lKdFZEZFpXbW90Wm5sZlFWVlVlVE5SY2taU1owOXRUVGMzWVd4MU4wVnJialUzTld4Tk4xaDJXazlySW4wIiwibm9uY2UiOiJjYTUwZTJiYi04Y2I0LTRmZjMtOTE3ZS1mMDk4MTU5NDVlNjMiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjcwMDMvb3BlbmlkNHZjL3ZlcmlmeSIsInZwIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZVByZXNlbnRhdGlvbiJdLCJpZCI6ImtSN0RoY0FZZlVmUyIsImhvbGRlciI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWEybGtJam9pWVdOdlZrbHRhVVZtWjNwSVpFUmFlR0YzZEhFeFdXbGpSR1JXZFU1V09DMVVSM0owYTNCNVlsUjZaeUlzSW5naU9pSnRWRGRaV21vdFpubGZRVlZVZVROUmNrWlNaMDl0VFRjM1lXeDFOMFZyYmpVM05XeE5OMWgyV2s5ckluMCIsInZlcmlmaWFibGVDcmVkZW50aWFsIjpbImV5SnJhV1FpT2lKa2FXUTZhbmRyT21WNVNuSmtTR3RwVDJsS1JsRjVTWE5KYlU1NVpHbEpOa2xzUVhSTmFsVXlTV2wzYVdFeWJHdEphbTlwV214R2RscEVaRFpTTWtwSVl6QTRlRTVJYkZkaWF6VlZURmRPZW1KSVpFZGxWWFJ1VWxaQ2NWTXpUbHBUUmtaS1dtdGtlRlZWTVVkVFUwbHpTVzVuYVU5cFNrWlRhMmgwV2tSS00xZHVRblJVUlU1MVlsVTBNRTlYU2xKYVZUbFVZbXBhVDFKSFNUUmhNbHBzVjBoS2JXTklTWGhUZWtaV1QwVmFXa2xwZDJsbFUwazJTV3hrTkdWdGEzZGFiVVkxVTJ4Vk5FMUZjRXRYYXpselRWTXhXVlpZU21GbGExWldUbFZHYWxGV1JqUldWMUpRVG1wcmRGb3lNVU5pVjAxcFpsRWpabEZ2WkRkNlIySkhjMDh4TkhsV2JrNVVMV056YkhkR2VVdG5SVkJxUzNOWlNGRkpaa2R4VVUxR1NTSXNJblI1Y0NJNklrcFhWQ0lzSW1Gc1p5STZJa1ZUTWpVMkluMC5leUpwYzNNaU9pSmthV1E2YW5kck9tVjVTbkprU0d0cFQybEtSbEY1U1hOSmJVNTVaR2xKTmtsc1FYUk5hbFV5U1dsM2FXRXliR3RKYW05cFdteEdkbHBFWkRaU01rcElZekE0ZUU1SWJGZGlhelZWVEZkT2VtSklaRWRsVlhSdVVsWkNjVk16VGxwVFJrWktXbXRrZUZWVk1VZFRVMGx6U1c1bmFVOXBTa1pUYTJoMFdrUktNMWR1UW5SVVJVNTFZbFUwTUU5WFNsSmFWVGxVWW1wYVQxSkhTVFJoTWxwc1YwaEtiV05JU1hoVGVrWldUMFZhV2tscGQybGxVMGsyU1d4a05HVnRhM2RhYlVZMVUyeFZORTFGY0V0WGF6bHpUVk14V1ZaWVNtRmxhMVpXVGxWR2FsRldSalJXVjFKUVRtcHJkRm95TVVOaVYwMXBabEVpTENKemRXSWlPaUprYVdRNmFuZHJPbVY1U25Ka1NHdHBUMmxLVUZNeFFXbE1RMHBxWTI1WmFVOXBTa1phUkVreFRsUkZOVWxwZDJsaE1teHJTV3B2YVZsWFRuWldhMngwWVZWV2JWb3pjRWxhUlZKaFpVZEdNMlJJUlhoWFYyeHFVa2RTVjJSVk5WZFBRekZWVWpOS01HRXpRalZaYkZJMldubEpjMGx1WjJsUGFVcDBWa1JrV2xkdGIzUmFibXhtVVZaV1ZXVlVUbEpqYTFwVFdqQTVkRlJVWXpOWlYzZ3hUakJXY21KcVZUTk9WM2hPVGpGb01sZHJPWEpKYmpBaUxDSjJZeUk2ZXlKQVkyOXVkR1Y0ZENJNld5Sm9kSFJ3Y3pvdkwzZDNkeTUzTXk1dmNtY3ZNakF4T0M5amNtVmtaVzUwYVdGc2N5OTJNU0pkTENKMGVYQmxJanBiSWxabGNtbG1hV0ZpYkdWRGNtVmtaVzUwYVdGc0lpd2lWbVZ5YVdacFlXSnNaVUYwZEdWemRHRjBhVzl1SWl3aVFtOWhjbVJwYm1kUVlYTnpJbDBzSW1sa0lqb2lkWEp1T25WMWFXUTZPVGRqTW1GaU56RXRZelk0TVMwME5EYzBMV0UyTXpjdE4yUmtORE0xTW1VNFpUQXpJaXdpWTNKbFpHVnVkR2xoYkZOMVltcGxZM1FpT25zaWFXUWlPaUprYVdRNmFuZHJPbVY1U25Ka1NHdHBUMmxLVUZNeFFXbE1RMHBxWTI1WmFVOXBTa1phUkVreFRsUkZOVWxwZDJsaE1teHJTV3B2YVZsWFRuWldhMngwWVZWV2JWb3pjRWxhUlZKaFpVZEdNMlJJUlhoWFYyeHFVa2RTVjJSVk5WZFBRekZWVWpOS01HRXpRalZaYkZJMldubEpjMGx1WjJsUGFVcDBWa1JrV2xkdGIzUmFibXhtVVZaV1ZXVlVUbEpqYTFwVFdqQTVkRlJVWXpOWlYzZ3hUakJXY21KcVZUTk9WM2hPVGpGb01sZHJPWEpKYmpBaUxDSm1hWEp6ZEU1aGJXVWlPaUpVUVZKUElpd2liR0Z6ZEU1aGJXVWlPaUpaUVUxQlJFRWlMQ0p6WldGMElqb2lNVEJCSWl3aVpteHBaMmgwSWpvaVJWZ3hNak1pTENKa1lYUmxJam9pTWpBeU5TMHhNaTB3TkNKOUxDSnBjM04xWlhJaU9uc2lhV1FpT2lKa2FXUTZhbmRyT21WNVNuSmtTR3RwVDJsS1JsRjVTWE5KYlU1NVpHbEpOa2xzUVhSTmFsVXlTV2wzYVdFeWJHdEphbTlwV214R2RscEVaRFpTTWtwSVl6QTRlRTVJYkZkaWF6VlZURmRPZW1KSVpFZGxWWFJ1VWxaQ2NWTXpUbHBUUmtaS1dtdGtlRlZWTVVkVFUwbHpTVzVuYVU5cFNrWlRhMmgwV2tSS00xZHVRblJVUlU1MVlsVTBNRTlYU2xKYVZUbFVZbXBhVDFKSFNUUmhNbHBzVjBoS2JXTklTWGhUZWtaV1QwVmFXa2xwZDJsbFUwazJTV3hrTkdWdGEzZGFiVVkxVTJ4Vk5FMUZjRXRYYXpselRWTXhXVlpZU21GbGExWldUbFZHYWxGV1JqUldWMUpRVG1wcmRGb3lNVU5pVjAxcFpsRWlMQ0p1WVcxbElqb2lSWGhoYlhCc1pTQkJhWEpzYVc1bEluMHNJbWx6YzNWaGJtTmxSR0YwWlNJNklqSXdNalV0TVRJdE1ESlVNVFU2TURVNk1URXVNVGt4T0RRMU9USTBXaUlzSW1WNGNHbHlZWFJwYjI1RVlYUmxJam9pTWpBeU5pMHhNaTB3TWxReE5Ub3dOVG94TVM0eE9USXhNamc1TWpSYUlpd2lkbUZzYVdSR2NtOXRJam9pTWpBeU5TMHhNaTB3TWxReE5Ub3dOVG94TVM0eE9USXdPVFV5TVRaYUluMHNJbXAwYVNJNkluVnlianAxZFdsa09qazNZekpoWWpjeExXTTJPREV0TkRRM05DMWhOak0zTFRka1pEUXpOVEpsT0dVd015SXNJbVY0Y0NJNk1UYzVOakl5TXpreE1Td2lhV0YwSWpveE56WTBOamczT1RFeExDSnVZbVlpT2pFM05qUTJPRGM1TVRGOS5kUmR5THdsblJmeE5ZZHFIdGhEcUNvYjl3WXYzMGlEakdRZDhIa01oeEhucFZGSFNzaTFKbHFTZHZsTlJhdWI1TXJNX3Rnd3NHNUZ6T3VybmdyUERzZyJdfX0.zdJEO3HC0gJho5YbrifImnpdxyRhVeMxZus-Bl99e4jtTnk1vCaHrCPoJSwCfHm5EOMsndAiCrJdiyuf-Xa2Cg",
    "presentation_submission": {
      "id": "kR7DhcAYfUfS",
      "definition_id": "kR7DhcAYfUfS",
      "descriptor_map": [
        {
          "id": "BoardingPass",
          "format": "jwt_vp",
          "path": "$",
          "path_nested": {
            "id": "BoardingPass",
            "format": "jwt_vc",
            "path": "$.vp.verifiableCredential[0]"
          }
        }
      ]
    },
    "state": "9Ay3acw9yqM7"
  },
  "verificationResult": true,
  "policyResults": {
    "results": [
      {
        "credential": "VerifiablePresentation",
        "policyResults": [
          {
            "policy": "signature",
            "description": "Checks a JWT credential by verifying its cryptographic signature using the key referenced by the DID in `iss`.",
            "is_success": true,
            "result": {
              "sub": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiYWNvVkltaUVmZ3pIZERaeGF3dHExWWljRGRWdU5WOC1UR3J0a3B5YlR6ZyIsIngiOiJtVDdZWmotZnlfQVVUeTNRckZSZ09tTTc3YWx1N0VrbjU3NWxNN1h2Wk9rIn0",
              "nbf": 1764805314,
              "iat": 1764805374,
              "jti": "kR7DhcAYfUfS",
              "iss": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiYWNvVkltaUVmZ3pIZERaeGF3dHExWWljRGRWdU5WOC1UR3J0a3B5YlR6ZyIsIngiOiJtVDdZWmotZnlfQVVUeTNRckZSZ09tTTc3YWx1N0VrbjU3NWxNN1h2Wk9rIn0",
              "nonce": "ca50e2bb-8cb4-4ff3-917e-f09815945e63",
              "aud": "http://localhost:7003/openid4vc/verify",
              "vp": {
                "@context": [
                  "https://www.w3.org/2018/credentials/v1"
                ],
                "type": [
                  "VerifiablePresentation"
                ],
                "id": "kR7DhcAYfUfS",
                "holder": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiYWNvVkltaUVmZ3pIZERaeGF3dHExWWljRGRWdU5WOC1UR3J0a3B5YlR6ZyIsIngiOiJtVDdZWmotZnlfQVVUeTNRckZSZ09tTTc3YWx1N0VrbjU3NWxNN1h2Wk9rIn0",
                "verifiableCredential": [
                  "eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJbU55ZGlJNklsQXRNalUySWl3aWEybGtJam9pWmxGdlpEZDZSMkpIYzA4eE5IbFdiazVVTFdOemJIZEdlVXRuUlZCcVMzTlpTRkZKWmtkeFVVMUdTU0lzSW5naU9pSkZTa2h0WkRKM1duQnRURU51YlU0ME9XSlJaVTlUYmpaT1JHSTRhMlpsV0hKbWNISXhTekZWT0VaWklpd2llU0k2SWxkNGVta3dabUY1U2xVNE1FcEtXazlzTVMxWVZYSmFla1ZWTlVGalFWRjRWV1JQTmprdFoyMUNiV01pZlEjZlFvZDd6R2JHc08xNHlWbk5ULWNzbHdGeUtnRVBqS3NZSFFJZkdxUU1GSSIsInR5cCI6IkpXVCIsImFsZyI6IkVTMjU2In0.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJbU55ZGlJNklsQXRNalUySWl3aWEybGtJam9pWmxGdlpEZDZSMkpIYzA4eE5IbFdiazVVTFdOemJIZEdlVXRuUlZCcVMzTlpTRkZKWmtkeFVVMUdTU0lzSW5naU9pSkZTa2h0WkRKM1duQnRURU51YlU0ME9XSlJaVTlUYmpaT1JHSTRhMlpsV0hKbWNISXhTekZWT0VaWklpd2llU0k2SWxkNGVta3dabUY1U2xVNE1FcEtXazlzTVMxWVZYSmFla1ZWTlVGalFWRjRWV1JQTmprdFoyMUNiV01pZlEiLCJzdWIiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaVlXTnZWa2x0YVVWbVozcElaRVJhZUdGM2RIRXhXV2xqUkdSV2RVNVdPQzFVUjNKMGEzQjVZbFI2WnlJc0luZ2lPaUp0VkRkWldtb3RabmxmUVZWVWVUTlJja1pTWjA5dFRUYzNZV3gxTjBWcmJqVTNOV3hOTjFoMldrOXJJbjAiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVmVyaWZpYWJsZUF0dGVzdGF0aW9uIiwiQm9hcmRpbmdQYXNzIl0sImlkIjoidXJuOnV1aWQ6OTdjMmFiNzEtYzY4MS00NDc0LWE2MzctN2RkNDM1MmU4ZTAzIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaVlXTnZWa2x0YVVWbVozcElaRVJhZUdGM2RIRXhXV2xqUkdSV2RVNVdPQzFVUjNKMGEzQjVZbFI2WnlJc0luZ2lPaUp0VkRkWldtb3RabmxmUVZWVWVUTlJja1pTWjA5dFRUYzNZV3gxTjBWcmJqVTNOV3hOTjFoMldrOXJJbjAiLCJmaXJzdE5hbWUiOiJUQVJPIiwibGFzdE5hbWUiOiJZQU1BREEiLCJzZWF0IjoiMTBBIiwiZmxpZ2h0IjoiRVgxMjMiLCJkYXRlIjoiMjAyNS0xMi0wNCJ9LCJpc3N1ZXIiOnsiaWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJbU55ZGlJNklsQXRNalUySWl3aWEybGtJam9pWmxGdlpEZDZSMkpIYzA4eE5IbFdiazVVTFdOemJIZEdlVXRuUlZCcVMzTlpTRkZKWmtkeFVVMUdTU0lzSW5naU9pSkZTa2h0WkRKM1duQnRURU51YlU0ME9XSlJaVTlUYmpaT1JHSTRhMlpsV0hKbWNISXhTekZWT0VaWklpd2llU0k2SWxkNGVta3dabUY1U2xVNE1FcEtXazlzTVMxWVZYSmFla1ZWTlVGalFWRjRWV1JQTmprdFoyMUNiV01pZlEiLCJuYW1lIjoiRXhhbXBsZSBBaXJsaW5lIn0sImlzc3VhbmNlRGF0ZSI6IjIwMjUtMTItMDJUMTU6MDU6MTEuMTkxODQ1OTI0WiIsImV4cGlyYXRpb25EYXRlIjoiMjAyNi0xMi0wMlQxNTowNToxMS4xOTIxMjg5MjRaIiwidmFsaWRGcm9tIjoiMjAyNS0xMi0wMlQxNTowNToxMS4xOTIwOTUyMTZaIn0sImp0aSI6InVybjp1dWlkOjk3YzJhYjcxLWM2ODEtNDQ3NC1hNjM3LTdkZDQzNTJlOGUwMyIsImV4cCI6MTc5NjIyMzkxMSwiaWF0IjoxNzY0Njg3OTExLCJuYmYiOjE3NjQ2ODc5MTF9.dRdyLwlnRfxNYdqHthDqCob9wYv30iDjGQd8HkMhxHnpVFHSsi1JlqSdvlNRaub5MrM_tgwsG5FzOurngrPDsg"
                ]
              }
            }
          }
        ]
      },
      {
        "credential": "BoardingPass",
        "policyResults": [
          {
            "policy": "signature",
            "description": "Checks a JWT credential by verifying its cryptographic signature using the key referenced by the DID in `iss`.",
            "is_success": true,
            "result": {
              "iss": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Iiwia2lkIjoiZlFvZDd6R2JHc08xNHlWbk5ULWNzbHdGeUtnRVBqS3NZSFFJZkdxUU1GSSIsIngiOiJFSkhtZDJ3WnBtTENubU40OWJRZU9TbjZORGI4a2ZlWHJmcHIxSzFVOEZZIiwieSI6Ild4emkwZmF5SlU4MEpKWk9sMS1YVXJaekVVNUFjQVF4VWRPNjktZ21CbWMifQ",
              "sub": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiYWNvVkltaUVmZ3pIZERaeGF3dHExWWljRGRWdU5WOC1UR3J0a3B5YlR6ZyIsIngiOiJtVDdZWmotZnlfQVVUeTNRckZSZ09tTTc3YWx1N0VrbjU3NWxNN1h2Wk9rIn0",
              "vc": {
                "@context": [
                  "https://www.w3.org/2018/credentials/v1"
                ],
                "type": [
                  "VerifiableCredential",
                  "VerifiableAttestation",
                  "BoardingPass"
                ],
                "id": "urn:uuid:97c2ab71-c681-4474-a637-7dd4352e8e03",
                "credentialSubject": {
                  "id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoiYWNvVkltaUVmZ3pIZERaeGF3dHExWWljRGRWdU5WOC1UR3J0a3B5YlR6ZyIsIngiOiJtVDdZWmotZnlfQVVUeTNRckZSZ09tTTc3YWx1N0VrbjU3NWxNN1h2Wk9rIn0",
                  "firstName": "TARO",
                  "lastName": "YAMADA",
                  "seat": "10A",
                  "flight": "EX123",
                  "date": "2025-12-04"
                },
                "issuer": {
                  "id": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Iiwia2lkIjoiZlFvZDd6R2JHc08xNHlWbk5ULWNzbHdGeUtnRVBqS3NZSFFJZkdxUU1GSSIsIngiOiJFSkhtZDJ3WnBtTENubU40OWJRZU9TbjZORGI4a2ZlWHJmcHIxSzFVOEZZIiwieSI6Ild4emkwZmF5SlU4MEpKWk9sMS1YVXJaekVVNUFjQVF4VWRPNjktZ21CbWMifQ",
                  "name": "Example Airline"
                },
                "issuanceDate": "2025-12-02T15:05:11.191845924Z",
                "expirationDate": "2026-12-02T15:05:11.192128924Z",
                "validFrom": "2025-12-02T15:05:11.192095216Z"
              },
              "jti": "urn:uuid:97c2ab71-c681-4474-a637-7dd4352e8e03",
              "exp": 1796223911,
              "iat": 1764687911,
              "nbf": 1764687911
            }
          }
        ]
      }
    ],
    "time": "PT1.161643667S",
    "policiesRun": 2
  }
}

VC提示に成功しVerifierによる検証が成功すると、レスポンスのうち verificationResulttrue で返ってきます。上記のレスポンスはまさにそのようになっています。
これで、Holder(搭乗者)が搭乗券VCを空港カウンターのVerifierに提示し検証する、という一連の流れができました。

さいごに

今回はwalt.idのオープンソースリポジトリの内容を使って、航空機の搭乗チケットを航空会社がVCとして発行→搭乗者がVC受領→搭乗者がカウンターでVC提示→カウンター係員がVC検証という流れをイメージしながらハンズオンを行いました。
実際には各航空会社がVC発行システムを独自に持つでしょうし、搭乗者は自分にあったウォレットアプリをインストールしているはずです。カウンター係員もURLを直接伝える形ではなくQRコードを提示することで、手続きを簡易化するかもしれません。
大枠は今回の記事で理解をしつつ、細かい部分はニーズに合わせてチューニングしていってくださいね。

Discussion