2. デジタルアイデンティティウォレットセッション: 電通総研

2. デジタルアイデンティティウォレットセッション: 電通総研

この文字起こしは、2024 年 12 月 11 日に開催された Authlete Customer and Partner Meetup 2024 ののプレゼンテーションのひとつです。株式会社電通総研 オープンイノベーションラボの比嘉康雄氏に、Authlete 3.0 を活用した EUDI Wallet / EUDI Verifier と mDL の実証実験についてお話しいただきました。

はじめに

こんにちは、電通総研の比嘉です。よろしくお願いします。

Authlete 3.0 の目玉のひとつは、OID4VCI (OpenID for Verifiable Credentials Issuance) と呼ばれる仕様の実装です。OID4VCI は、Verifiable Credentials、デジタル証明書に関する API のひとつです。OID4VCI は、ウォレットからイシュアーの API をどんな感じで呼んでいるのかという視点で見ていくと、かなり理解しやすいです。今日はそのかたちで説明していきます。

Verifiable Credentials のポイント

OID4VCI と OID4VP

Verifiable Credentials の世界は、クレデンシャルを発行するイシュアー、そのイシュアーからクレデンシャルを受け取るウォレット、そのウォレットからクレデンシャルを受け取るベリファイアーから成ります。

ウォレットは自分が持っているクレデンシャルを、サービスの提供者側であるベリファイアーに、協調しながら渡します。ベリファイアーは、ユーザーがクレデンシャルを持っていることを確認し、サービス提供を認可します。Verifiable Credentials の世界を理解するためには、OID4VCI だけではなく、ウォレットとベリファイアーのやり取りを規定する OID4VP (OpenID for Verifiable Presentations) 仕様も重要になってきます。OID4VCI と OID4VP はペアで抑えておく必要があるのかなと思います。

今日は説明しませんが、ウォレットとベリファイアーは、UI などを提供するフロントエンドと、バックエンド(エンドポイント)、ウォレットの 3 者が、API を互いに呼び出します。そのあたりをまずは頭に入れておくと、今後 Verifiable Credentials を仕事で使う時でも理解しやすいと思います。

Verifiable Credentials の中心は EU

違う見方をすると、Verifiable Credentials の中心は EU です。EU では 2026 年から各国の住人に対してデジタルアイデンティティウォレットを提供し、それを使うことが法的に決まっています。

EU の開発者は 2026 年に間に合わせるために必死で取り組んでいます。日本とアメリカは期限が決まっておらず、そこまでの真剣さが無いというところもあります。EU がどう動いているかに注目していくのが重要です。

EU がどのようにデジタルアイデンティティウォレットと Verifiable Credentials を実装しようとしているかを知るためには、EU のデジタルアイデンティティウォレット (EUDIW) のリファレンスインプリメンテーション (RI) を見ていくのが一番手っ取り早いと思います。

EUDIW RI の構成

EUDIW の RI は今年の3月末くらいから提供されており、ソースコードも全部見ることができます。まずはそこから掴んでいくのが重要かなと思います。

EUDIW の構成は、ウォレット、ベリファイアー、イシュアーの 3 つに分かれています。

ウォレット

一番複雑なのはウォレットです。

UI は Android と iPhone で提供されています。UI に依存しない部分はコアと呼ばれます。さらに OID4VCI と OID4VP があります。OID4VP と SIOP v2 はたいていひとつのセットとして使われるので、同じモジュールとして実装されています。またOID4VP には、ベリファイアーがウォレットに対して「こんなクレデンシャルがほしい」と SQL 風のクエリーをする、Presentation Exchange が加わります。

OID4VP のところはリモートと近接通信の 2 つの部分に分かれていますが、厳密に言えばちょっと違います。正確には、クレデンシャルをベリファイアーに提供する際に、インターネット経由の場合は「リモート」となり、OID4VP が使われます。一方 NFC などの場合には「近接通信」となり、ISO/IEC 18013-5、mDL と呼ばれる仕様が使われます。

最後の部分は、クレデンシャルや秘密鍵をセキュアに保存するためのストレージ機能です。iOS ではキーチェーンを使って保存しています。

ベリファイアー

Web のフロントエンドと、バックエンドのエンドポイントに分かれます。フロントエンドは Angular フレームワークが使われています。バックエンド実装は Kotlin と Python から構成されます。

イシュアー

Python 版と Kotlin 版があります。ここについては、弊社は Authlete を使う選択をしています。

EUDIW RI ベースの PoC

2023 年度のデジタル庁の Trusted Web の案件で、我々は EUDIW や eIDAS 2.0 のアーキテクチャーで実証実験を行いました。さらに 2024 年度、ゴールデンウィーク明けから 5 月末にかけて、お客様と PoC を行っています。

ベリファイアー、ウォレット、イシュアーの3者の実装を理解することで、Verifiable Credentials がどのように連携して機能するかを把握できます。まずは PoC などでその 3 つの登場人物を自分たちでひと通り実装してみるのが、非常におすすめです。

PoC の企画・実装の期間は 3 ヶ月弱です。この中で、Authlete のフロントエンドである authlete-java-common, authlete-java-jaxrs, java-oauth-server を CloudFlare で動かすために、TypeScript にポーティングしています。その理由は後ほど説明します。

EUDI ベリファイアーについても、CloudFlare 上で動作させるために TypeScript にポーティングしています。フロントエンドのサンプル実装を他社に提供し、その方がお客さまと相談しながら PoC におけるベリファイアーのフロントエンドを構築し、テストを行っていただく、という流れです。

ウォレットとイシュアーに手を入れることは、基本的にほとんどありません。一方でベリファイアーの、とくにフロントエンド部分は案件ごとに全部違うものになります。実証実験でも本番でも、ベリファイアーのフロントエンドは案件ごとに作っていく必要があります。

Authlete Frontend を TypeScript にポーティング

Authlete のフロントエンドの Java 実装は、authlete-java-common, authlete-java-jaxrs, java-oauth-server の 3 つのモジュールから構成されています。このうち java-oauth-server には、ユーザー認証のような、認可サーバーを作る時に案件ごとにカスタマイズが入りそうな部分が集約されています。サーバーレス/エッジコンピューティングに対応するために、そしてこれらを CloudFlare で動かすために、TypeScript に変換しました。

理由 1. エッジ/サーバーレスコンピューティングへの対応

Edge は速い

エッジコンピューティングでは、よりユーザーに近いところ、例えば東京の人であれば東京のエッジポイントに、大阪であれば大阪に、海外でも日本ではなく近くのエッジにアクセスし、そのエッジごとにサーバーが立ち上がります。これによってより迅速にユーザーに対してレスポンスを返せるようになります。まずはこれを実現したかったので、CloudFlare という環境を選びました。

Serverless は安い

サーバーレスはやはり安いです。使うときしか課金されません。OIDC や OAuth 2.0 の認証・認可サーバーは始終使われるものなので、IaaS などを使って常時立ち上げっぱなしでもいいのかもしれません。一方 OID4VCI でのイシュアーはクレデンシャルを発行するものなので、毎日朝から晩まで使われるようなものではありません。コストを抑えるため、OID4VCI に関してはサーバーレスであるというのが望ましいということで、CloudFlare 上に移しています。

Java は起動が遅い

Java は、一旦立ち上がった後はかなり速いものの、起動が遅いです。起動に 5 秒かかったりします。Java はサーバーレスに向いていません。そのため TypeScript にポーティングしています。

理由 2. メンテナンスを容易に

すべてのロジックをクロージャーに分解

今回の OD4VCI では、PAR、オーソリゼーション、トークン、クレデンシャルという、大別して4つの java-oauth-server のエンドポイントを使っています。これらに関する全てのロジックを JavaScript/TypeScript のクロージャーに分解しています。

既存の Authlete のフロントエンドは、基本的にはインターフェイスがあって、ベースになるクラスを継承してカスタマイズしていく、クラスベースの作りです。そのため、ベースのクラスのメソッドに対して何か変更したい場合、そのベースのクラスを利用しているいろいろな継承先で、同じようなロジックを実装していく必要があります。

全部をクロージャーにして、クロージャーで必要な別のクロージャーをディペンデンシーインジェクション (DI) で使うことによって、修正したいロジックをピンポイントで変更できるような仕組みを今回作っています。

単一責任の原則

クロージャーは一つひとつが単機能です。1 ファイル 1 クロージャー、単一責任の原則が、より良いクラスを設計するためには重要です。単一責任の原則とは「何かを変更する理由はただひとつだけ」です。

クラスやファイルの中身が大きければ大きいほど、そのモジュールはいろいろな役割を抱え込み、メンテナンスがやりにくくなるし、テストもしづらくなります。それを全部単一責任の原則で、1 個のクロージャーを 1 個のファイルにすることによって、非常にメンテナンスがしやすくなります。

クロージャーを DI で組み立て、エンドポイントごとに設定を分割

あるクロージャーで必要な別のクロージャーがあったら、そのクロージャーを作る時に依存性が挿入される、クロージャーがコンストラクターで渡ってくるみたいなイメージで、DI として組み立ててます。ポイントは、エンドポイントごとに DI の設定ファイルが分かれていることです。

一般的に DI は、システム全体で1個の設定ファイルになっていることがほとんどです。何かリクエストが入ってきて、あるエンドポイントが処理するときに、それ以外のエンドポイントのいろんなオブジェクトも全部作られてしまいます。そういうのはすごく重くなるし、サーバーレス/エッジコンピューティングには向いていません。よって、DI の設定をエンドポイントごとに分けることによって、リクエストで起動されたエンドポイントが必要とするオブジェクトのみ、必要なクロージャーのみが作られる仕組みを今回実現しています。

理由 3. テストを完璧に

短機能・単一責任の原則・1 ファイル 1 クロージャーにしたことで、別のクロージャーはリアルなクロージャーとしてだけでなくモックとして渡すことができます。モックベースのテストが非常に簡単にできるので、カバレッジは 100% です。

テストの 95% は AI で自動生成しています。AI は、複雑なクラスのテストは完璧には作ってくれませんが、そうではないものは全部自動で作ってくれます。今回は Cursor という AI に特化した VS Code を使ってテストを書いています。

残り 5% は結合テストです。こちらは複数のエンドポイントを組み合わせて作るわけですけど、AI がそれを自動で考えて作ってくれるはずもありません。ちゃんと人間が考えて結合テストを書く必要があります。

今回の話に限らず、AI にテストを書かせるのは今後必須になってくると思います。そのときにはできる限り 1 個のファイルの機能を減らして、できれば単一責任の原則にして、AI に書かせるのがおすすめです。

理由 4. 中身を完全に理解するため

実はこれが一番言いたかったことかもしれないんですけど、自分でコードを書いてポーティングしたのは、Authlete のフロントエンドの中身を完全に理解するためです。単に趣味で何かの API を理解したいのであれば、ソースコードを読んで何かするだけで十分ですが、仕事で使う時にはなんとなく知ってるって感じではなく、100% 中身を理解しておく必要があると思っています。

100% 理解するためには、ここのコードが動いたらどういう結果が出るかを、ソースコードを見るだけではなくちゃんと動かして目で見ることが必要です。そのためには 100% のカバレッジを持ったテストを作って実行してみるのが重要だと思います。

EUDI Verifier をポーティングした理由

EUDI のベリファイアーの Kotlin 版を CloudFlare 上にポーティングしています。これもさきほどお話ししたように、サーバーレスコンピューティングに対応するためであり、中身を完全に理解するためです。

フロントエンドで使われてるフレームワークは Angular です。しかし、日本の会社で Angular を使ってる人をあまり見たことがなく、使ってる方がいたら申し訳ないですけど、メジャーなフレームワークであるとは言えません。他社さんにとっては Angular を渡されても困るんじゃないのかなと思い、Hono/JSX ベースでの実装に置き換えてます。

Hono 自体はそれほどメジャーなフレームワークではないですけど、JSX のほうは React とかで使われている記法で書いています。React なり Next.js を使ってる方はたくさんいらっしゃるので、JSX ベースだったら理解しやすいかなと思い、フロントエンドは Hono/JSX ベースでやってます。

バックエンドは全部 Hono でできているので、我々としても、Hono に JSX を付け加えて作るのがやりやすかったところがあります。

EUDIW が 使っているAuthlete Endpoint

ここから、EUDIW が Authlete のエンドポイントをどんな感じで呼び出してるのかを具体的にお話しします。呼び出している java-oauth-server のエンドポイントは4つ、/api/par, /api/authorization, /api/token, /api/credentialです。

/api/par へのリクエスト

まず /api/par のエンドポイントをどうやって呼び出しているのか、わかりやすくするために curl に変換するとこんな感じです。赤線を引いてるところ、PAR のエンドポイントを呼び出すんだったら DPoP のヘッダーもあってもいいんじゃないのかなと僕的には思ってるんですけど、EUDIW では DPoP のヘッダーは無しです。それ以外は、PKCE の設定とステートがある感じです。

ポイントはスコープです。org.iso.18013.5.1.mDL を scope として指定することにより、この後 mDL 用のクレデンシャルが発行されることになります。

credential_configurations_supported

Credential Issuer Metadata というのが OID4VCI の仕様として決められているんですが、その中に credential_configurations_supported というフィールドがあります。そこにさっきのスコープで指定したものを書いておきます。

この中で重要なのは format と doctype です。スコープは上のキーに合わせて指定します。claims で、どういうクレームを使うのかを指定します。ただし mdoc の場合には、claims の下にさらに namespace があります。doctype, namespace, claims というかたちで定義されます。

/api/par からのレスポンス

レスポンスで重要なのは request_uri です。/api/authorization エンドポイントへのアクセス時に request_uri を指定することにより、先ほど /api/par エンドポイントで渡した情報が参照できるようになります。

/api/authorization へのリクエスト

/api/authorization エンドポイントへのリクエストは、ウォレットからは WebView を用います。この時に先ほどの request_uri と state を指定します。ウォレット内に WebView が立ち上がって、そこに Authlete の /api/authorization エンドポイントの画面が表示されるかたちです。

/api/authorization エンドポイントを呼び出すと、Authlete 側では ticket というものが発行されて、セッションに格納されます。これが後で認可コードの発行につながっていきます。

/api/authorization から返却されるページ

これが /api/authorization のページです。ここではログインIDとパスワードを入れて Authorize のボタンを押すかたちになります。

/api/authorization/decision へのリクエストの内容

ボタンを押すと、/api/authorization/decision エンドポイントに対してリクエストが送られます。そのときリクエストのボディーを通じて、authorized, loginId, password が渡ります。エンドポイントでは、ユーザーがちゃんとボタンを押したことを確認するために authorized をチェックします。そして loginId と password を使って subject を取得し、その subject と ticket を用いて Authlete の authorization/issue API を呼び出します。

ここで見てわかる通り、どうユーザー認証するかは Authlete は全然気にしていません。Authlete から見ると、subject に文字列が入ってくるっていう認識しかなくて、認証そのものは自由にやったら、っていうスタンスなんだと思います。

/api/authorization/issue からのレスポンス

/api/authorization/issue のレスポンスは、302 リダイレクトで、ロケーションが指定されるかたちになります。eudi-openid4vci://authorize/ というところまでがカスタム URL スキームです。EUDI のウォレットはこのようなカスタムURLを使うようになっています。そこに認可コード (code) 、ステート (state) 、イシュアー (iss) が渡ってくるかたちです。

/api/token へのリクエスト

さきほど取得した認可コードをトークンエンドポイントに対して送ります。このあたりはふつうの OpenID Connect や OAuth 2.0 と大きな違いはないと思います。

/api/token からのレスポンス

レスポンスも、アクセストークンはふつうの Bearer として返ってきます。

/api/credential へのリクエスト

次にクレデンシャルリクエストです。/api/credential エンドポイントに対して、Authorization ヘッダーで先ほどのアクセストークンを指定し、リクエストのボディとしてformatとdoctypeを指定します。

EUDIウォレットはレスポンスの暗号化を求めており、その方式が credential_response_encryption に指定されています。

credential_response_encryption

credential_response_encryptionは jwk (公開鍵) と alg と enc で成り立っています。enc は、レスポンスのボディを暗号化するための対称鍵暗号のロジックです。alg は、その暗号化に使ったキーをさらに暗号化するときのロジックです。この場合は RSA を使っていますが、最近だと ECDH によるキー交換(ECDH-ES など)のほうが多いのかなという気はします。

proof

proof は、ホルダー(ウォレットの所有者)の公開鍵が指定されるところだと思ってください。

/api/credential からのレスポンス

クレデンシャルレスポンスは、JWT ではなく、CBOR でエンコードされたものがさらに Base64URL のかたちでエンコードされます。

まとめ

以上が、ウォレットから見たイシュアーの呼び出し方になります。もっと時間があればベリファイアー側をどんな感じで呼び出してるのかも説明したかったんですけど、また機会があればお話しさせてください。本日はありがとうございました。