Get your free and exclusive 80-page Banking Passkey Report
Back to Overview

デジタルクレデンシャルVerifierの作り方(開発者ガイド)

Next.js、OpenID4VP、ISO mDocを使用して、デジタルクレデンシャルVerifierをゼロから構築する方法を学びましょう。このステップバイステップの開発者ガイドでは、モバイル運転免許証やその他のデジタルクレデンシャルを要求、受信、検証できるVerifierの作成方法を解説します。

Amine

Created: August 20, 2025

Updated: August 21, 2025

Blog-Post-Header-Image

See the original blog version in English here.

DigitalCredentialsDemo Icon

Want to experience digital credentials in action?

Try Digital Credentials

1. はじめに#

オンラインでの本人確認は常に課題であり、パスワードへの依存や、安全でないチャネルでの機密文書の共有につながっています。これにより、企業にとって本人確認は、時間とコストがかかり、不正行為が発生しやすいプロセスとなっていました。デジタルクレデンシャルは、ユーザーが自分のデータを再び管理できるようにする、新しいアプローチを提供します。これらは物理的なwalletのデジタル版であり、運転免許証から大学の学位まであらゆるものを格納できますが、暗号学的に安全で、プライバシーを保護し、即座に検証可能であるという付加的な利点があります。

このガイドでは、開発者向けにデジタルクレデンシャルのVerifierを構築するための実践的なステップバイステップのチュートリアルを提供します。標準は存在しますが、その実装に関するガイダンスはほとんどありません。このチュートリアルではそのギャップを埋め、ブラウザネイティブのDigital Credential API、プレゼンテーションプロトコルとしてのOpenID4VP、クレデンシャルフォーマットとしてのISO mDoc(例:モバイル運転免許証)を使用してVerifierを構築する方法を紹介します。

最終的には、互換性のあるモバイルwalletからデジタルクレデンシャルを要求、受信、検証できる、シンプルかつ機能的なNext.jsアプリケーションが完成します。

完成したアプリケーションの動作を簡単に見てみましょう。プロセスは主に4つのステップで構成されます。

ステップ1:初期ページ ユーザーは初期ページにアクセスし、「Digital Identityで検証」をクリックしてプロセスを開始します。

ステップ2:信頼プロンプト ブラウザはユーザーに信頼の確認を求めます。ユーザーは「続行」をクリックして次に進みます。

ステップ3:QRコードのスキャン QRコードが表示され、ユーザーは互換性のあるwalletアプリケーションでスキャンします。

ステップ4:デコードされたクレデンシャル 検証が成功すると、アプリケーションはデコードされたクレデンシャルデータを表示します。

1.1 仕組み#

デジタルクレデンシャルの背後にある仕組みは、3つの主要な役割が関わるシンプルかつ強力な「信頼の三角形」モデルにあります。

  • Issuer(発行者): 信頼できる機関(例:政府機関、大学、銀行など)で、ユーザーにクレデンシャルを暗号学的に署名して発行します。
  • Holder(保有者): ユーザー本人。クレデンシャルを受け取り、デバイス上の個人のデジタルwalletに安全に保管します。
  • Verifier(検証者): ユーザーのクレデンシャルを確認する必要があるアプリケーションやサービス。

ユーザーがサービスにアクセスしたい場合、自分のwalletからクレデンシャルを提示します。Verifierは、元のIssuerに直接問い合わせることなく、その真正性を即座に確認できます。

1.2 Verifierが不可欠な理由(そして、あなたがここにいる理由)#

この分散型アイデンティティエコシステムが発展するためには、Verifierの役割が絶対に不可欠です。彼らはこの新しい信頼インフラストラクチャのゲートキーパーであり、クレデンシャルを利用して実世界で役立つものにする存在です。下の図が示すように、VerifierはHolderからクレデンシャルを要求、受信、検証することで信頼の三角形を完成させます。

開発者であれば、この検証を実行するサービスを構築することは、次世代の安全でユーザー中心のアプリケーションにとって基本的なスキルです。このガイドは、まさにそのプロセスを順を追って説明するために設計されています。中核となる概念や標準から、署名の検証やクレデンシャルの状態確認といった実装の詳細まで、あなた自身の検証可能なクレデンシャルVerifierを構築するために知っておくべきすべてをカバーします。

先に進みたいですか? このチュートリアルの完成版プロジェクトはGitHubで公開しています。自由にクローンして試してみてください: https://github.com/corbado/digital-credentials-example

さあ、始めましょう。

2. Verifierを構築するための前提条件#

始める前に、以下の準備ができていることを確認してください。

  1. デジタルクレデンシャルとmdocの基本的な理解
    • このチュートリアルはISO mDocフォーマット(例:モバイル運転免許証)に焦点を当てており、W3C Verifiable Credentials (VCs)などの他のフォーマットは扱いません。mdocの基本概念に精通していると役立ちます。
  2. DockerとDocker Compose
    • 私たちのプロジェクトでは、OIDCセッション状態を管理するためにDockerコンテナ内でMySQLデータベースを使用します。両方がインストールされ、実行されていることを確認してください。
  3. 選択したプロトコル:OpenID4VP
    • クレデンシャル交換フローにはOpenID4VP (OpenID for Verifiable Presentations) プロトコルを使用します。
  4. 技術スタックの準備
    • バックエンドロジックにはTypeScript (Node.js) を使用します。
    • バックエンド(APIルート)とフロントエンド(UI)の両方にNext.jsを使用します。
    • 主要ライブラリ:mdoc解析用のCBORデコードライブラリとMySQLクライアント。
  5. テスト用のクレデンシャルとWallet
    • OpenID4VPリクエストを理解し、mdocクレデンシャルを提示できる**CMWallet**(Android用)を使用します。
  6. 暗号技術の基本知識
    • mdocとOIDCフローに関連するデジタル署名と公開鍵/秘密鍵の概念を理解していること。

これから、これらの前提条件を一つずつ詳しく見ていき、mdocベースのVerifierを支える標準とプロトコルから始めます。

2.1 プロトコルの選択#

私たちのVerifierは、以下に基づいて構築されています。

標準 / プロトコル説明
W3C VCW3C Verifiable Credentialsデータモデル。クレーム、メタデータ、証明を含むデジタルクレデンシャルの標準構造を定義します。
SD-JWTJWTの選択的開示。JSON Web TokenをベースにしたVCのフォーマットで、保有者がクレデンシャルから特定のクレームのみを選択的に開示できるようにし、プライバシーを向上させます。
ISO mDocISO/IEC 18013-5。モバイル運転免許証(mDL)やその他のモバイルIDの国際標準で、オフラインおよびオンラインでの使用のためのデータ構造と通信プロトコルを定義します。
OpenID4VPOpenID for Verifiable Presentations。OAuth 2.0上に構築された相互運用可能なプレゼンテーションプロトコル。Verifierがクレデンシャルを要求し、保有者のwalletがそれを提示する方法を定義します。

このチュートリアルでは、具体的に以下を使用します。

  • クレデンシャルを要求および受信するためのプロトコルとしてOpenID4VP
  • クレデンシャルフォーマットとしてISO mDoc(例:モバイル運転免許証)。

スコープに関する注意: より広い文脈を提供するためにW3C VCとSD-JWTを簡単に紹介しますが、このチュートリアルではOpenID4VPを介したISO mDocクレデンシャルの実装に限定しています。W3CベースのVCはこの例のスコープ外です。

2.1.1 ISO mDoc (Mobile Document)#

ISO/IEC 18013-5 mDoc標準は、モバイル運転免許証(mDL)などのモバイルドキュメントの構造とエンコーディングを定義します。mDocクレデンシャルはCBORでエンコードされ、暗号学的に署名されており、検証のためにデジタルで提示できます。私たちのVerifierは、これらのmdocクレデンシャルのデコードと検証に焦点を当てます。

2.1.2 OpenID4VP (OpenID for Verifiable Presentations)#

OpenID4VPは、OAuth 2.0とOpenID Connectの上に構築された、デジタルクレデンシャルを要求および提示するための相互運用可能なプロトコルです。この実装では、OpenID4VPは以下の目的で使用されます。

  • クレデンシャル提示フローの開始(QRコードまたはブラウザAPI経由)
  • ユーザーのwalletからmdocクレデンシャルを受信
  • 安全でステートフル、かつプライバシーを保護するクレデンシャル交換の保証

2.2 技術スタックの選択#

標準とプロトコルを明確に理解したところで、次はVerifierを構築するための適切な技術スタックを選択する必要があります。私たちの選択は、堅牢性、開発者体験、そして現代のウェブエコシステムとの互換性を考慮して設計されています。

2.2.1 言語:TypeScript#

フロントエンドとバックエンドの両方のコードにTypeScriptを使用します。JavaScriptのスーパーセットとして、静的型付けを追加することで、エラーを早期に発見し、コードの品質を向上させ、複雑なアプリケーションの管理を容易にします。クレデンシャル検証のようなセキュリティに敏感な文脈では、型安全性は大きな利点です。

2.2.2 フレームワーク:Next.js#

Next.jsは、フルスタックアプリケーションを構築するためのシームレスで統合された体験を提供するため、私たちが選択したフレームワークです。

  • フロントエンド用: 検証プロセスが開始されるユーザーインターフェース(例:QRコードの表示)を構築するために、Reactを備えたNext.jsを使用します。
  • バックエンド用: Next.js API Routesを活用してサーバーサイドのエンドポイントを作成します。これらのエンドポイントは、有効なOpenID4VPリクエストを作成し、CMWalletからの最終的なレスポンスを安全に受信して検証するためのredirect_uriとして機能する責任を負います。

2.2.3 主要なライブラリ#

私たちの実装は、フロントエンドとバックエンドで特定のライブラリセットに依存しています。

  • next: Next.jsフレームワーク。バックエンドのAPIルートとフロントエンドのUIの両方で使用されます。
  • reactreact-dom: フロントエンドのユーザーインターフェースを動かします。
  • cbor-web: CBORでエンコードされたmdocクレデンシャルを使用可能なJavaScriptオブジェクトにデコードするために使用されます。
  • mysql2: チャレンジと検証セッションを保存するためのMySQLデータベース接続を提供します。
  • uuid: 一意のチャレンジ文字列(nonce)を生成するためのライブラリです。
  • @types/uuid: UUID生成のためのTypeScript型定義。

openid-clientに関する注意: より高度な本番グレードのVerifierは、バックエンドでOpenID4VPプロトコルを直接処理するためにopenid-clientライブラリを使用するかもしれません。これにより、動的なredirect_uriなどの機能が可能になります。redirect_uriを持つサーバー駆動のOpenID4VPフローでは、openid-clientvp_tokenレスポンスを直接解析および検証するために使用されます。このチュートリアルでは、よりシンプルでブラウザを介したフローを使用しているため、これは不要であり、プロセスが理解しやすくなっています。

この技術スタックにより、ブラウザのDigital Credential APIとISO mDocクレデンシャルフォーマットに焦点を当てた、堅牢で型安全、かつスケーラブルなVerifierの実装が保証されます。

2.3 テスト用のWalletとクレデンシャルの入手#

Verifierをテストするには、ブラウザのDigital Credential APIと対話できるモバイルwalletが必要です。

私たちは、Android用の堅牢なOpenID4VP準拠のテストwalletである**CMWallet**を使用します。

CMWalletのインストール方法(Android):

  1. 上記のリンクを使用して、Androidデバイスに直接APKファイルをダウンロードします。
  2. デバイスの設定 > セキュリティを開きます。
  3. ファイルをダウンロードしたブラウザに対して**「不明なアプリのインストール」**を有効にします。
  4. 「ダウンロード」フォルダでダウンロードしたAPKファイルを見つけ、タップしてインストールを開始します。
  5. 画面の指示に従ってインストールを完了します。
  6. CMWalletを開くと、検証フローの準備が整ったテスト用クレデンシャルがプリロードされています。

注意: 信頼できるソースからのAPKファイルのみをインストールしてください。提供されているリンクは公式プロジェクトリポジトリのものです。

2.4 暗号技術の知識#

実装に入る前に、検証可能なクレデンシャルを支える暗号技術の概念を理解することが不可欠です。これが、クレデンシャルを「検証可能」で信頼できるものにしているのです。

2.4.1 デジタル署名:信頼の基盤#

Verifiable Credentialの核心は、Issuerによってデジタル署名された一連のクレーム(名前、生年月日など)です。デジタル署名は、2つの重要な保証を提供します。

  • 真正性: クレデンシャルが偽造者ではなく、確かにIssuerによって作成されたことを証明します。
  • 完全性: クレデンシャルが署名されてから変更または改ざんされていないことを証明します。

2.4.2 公開鍵/秘密鍵暗号方式#

デジタル署名は、公開鍵/秘密鍵暗号方式(非対称暗号とも呼ばれる)を使用して作成されます。私たちの文脈では、次のように機能します。

  1. Issuerは鍵ペアを持っています: 秘密に保管される秘密鍵と、誰もが利用できる公開鍵(通常はDIDドキュメントを介して提供される)です。
  2. 署名: Issuerがクレデンシャルを作成する際、その秘密鍵を使用して、特定のクレデンシャルデータに対する一意のデジタル署名を生成します。
  3. 検証: 私たちのVerifierがクレデンシャルを受け取ると、Issuer公開鍵を使用して署名を確認します。チェックが成功すれば、Verifierはクレデンシャルが本物であり、改ざんされていないことを確信できます。クレデンシャルデータに少しでも変更があれば、署名は無効になります。

DIDに関する注意: このチュートリアルでは、DIDを介してIssuerの鍵を解決しません。本番環境では、通常、IssuerはDIDやその他の信頼できるエンドポイントを介して公開鍵を公開し、Verifierはそれを暗号検証に使用します。

2.4.3 JWTとしての検証可能なクレデンシャル#

検証可能なクレデンシャルは、しばしば**JSON Web Tokens (JWTs)**としてフォーマットされます。JWTは、二者間で転送されるクレームを表現するためのコンパクトでURLセーフな方法です。署名付きJWT(JWSとも呼ばれる)は、ドット(.)で区切られた3つの部分から構成されます。

  • ヘッダー: 使用された署名アルゴリズム(alg)など、トークンに関するメタデータを含みます。
  • ペイロード: issuercredentialSubjectなどを含む、Verifiable Credentialの実際のクレーム(vcクレーム)を含みます。
  • 署名: Issuerによって生成されたデジタル署名で、ヘッダーとペイロードをカバーします。
// JWT構造の例 [ヘッダー].[ペイロード].[署名]

注意: JWTベースの検証可能なクレデンシャルは、このブログ記事のスコープ外です。この実装は、ISO mDocクレデンシャルとOpenID4VPに焦点を当てており、W3C Verifiable CredentialsやJWTベースのクレデンシャルは対象外です。

2.4.4 Verifiable Presentation:所有の証明#

Verifierにとって、クレデンシャルが有効であることだけを知るだけでは不十分です。クレデンシャルを提示している人物が正当な保有者であることも知る必要があります。これにより、誰かが盗まれたクレデンシャルを使用するのを防ぎます。

これは**Verifiable Presentation (VP)**を使用して解決されます。VPは、1つ以上のVCを包むラッパーであり、保有者自身によって署名されています。

フローは次のとおりです。

  1. Verifierはユーザーにクレデンシャルの提示を求めます。
  2. ユーザーのwalletはVerifiable Presentationを作成し、必要なクレデンシャルをその中にバンドルし、保有者の秘密鍵を使用してプレゼンテーション全体に署名します。
  3. walletはこの署名付きVPをVerifierに送信します。

私たちのVerifierは、次に2つの別々の署名チェックを実行する必要があります。

  1. クレデンシャルの検証: プレゼンテーション内の各VCの署名を、Issuerの公開鍵を使用して確認します。(クレデンシャルが本物であることを証明します)。
  2. プレゼンテーションの検証: VP自体の署名を、保有者の公開鍵を使用して確認します。(それを提示している人物が所有者であることを証明します)。

この2段階のチェックにより、クレデンシャルの真正性とそれを提示している人物のアイデンティティの両方が保証され、堅牢で安全な信頼モデルが構築されます。

注意: W3C VCエコシステムで定義されているVerifiable Presentationsの概念は、このブログ記事のスコープ外です。ここでのVerifiable Presentationという用語は、OpenID4VPのvp_tokenレスポンスを指し、これはW3C VPと似た動作をしますが、W3CのJSON-LD署名モデルではなくISO mDocのセマンティクスに基づいています。このガイドはISO mDocクレデンシャルとOpenID4VPに焦点を当てており、W3C Verifiable Presentationsやその署名検証は対象外です。

3. アーキテクチャ概要#

私たちのVerifierアーキテクチャは、ブラウザに組み込まれたDigital Credential APIを安全な仲介役として使用し、WebアプリケーションとユーザーのモバイルCMWalletを接続します。このアプローチは、ブラウザにネイティブのQRコード表示とwallet通信を処理させることで、フローを簡素化します。

  • フロントエンド (Next.js & React): 軽量なユーザー向けウェブサイト。バックエンドからリクエストオブジェクトを取得し、それをブラウザのnavigator.credentials.get() APIに渡し、結果を受け取って検証のためにバックエンドに転送するのが仕事です。
  • バックエンド (Next.js API Routes): Verifierの心臓部。ブラウザAPIのための有効なリクエストオブジェクトを生成し、最終的な検証のためにフロントエンドからクレデンシャルプレゼンテーションを受信するエンドポイントを公開します。
  • ブラウザ (Credential API): ファシリテーター。フロントエンドからリクエストオブジェクトを受け取り、openid4vpプロトコルを理解し、ネイティブにQRコードを生成します。その後、walletがレスポンスを返すのを待ちます。
  • CMWallet (モバイルアプリ): ユーザーのwallet。QRコードをスキャンし、リクエストを処理し、ユーザーの同意を得て、署名付きレスポンスをブラウザに返します。

以下は、完全かつ正確なフローを示すシーケンス図です。

フローの説明:

  1. 開始: ユーザーがフロントエンドの「検証」ボタンをクリックします。
  2. リクエストオブジェクト: フロントエンドがバックエンド/api/verify/start)を呼び出し、バックエンドはクエリとnonceを含むリクエストオブジェクトを生成して返します。
  3. ブラウザAPI呼び出し: フロントエンドがリクエストオブジェクトを指定してnavigator.credentials.get()を呼び出します。
  4. ネイティブQRコード: ブラウザopenid4vpプロトコルリクエストを認識し、ネイティブにQRコードを表示します。.get()のpromiseは保留状態になります。

注意: このQRコードフローはデスクトップブラウザで発生します。モバイルブラウザ(実験的フラグを有効にしたAndroid Chrome)では、ブラウザは同じデバイス上の互換性のあるwalletと直接通信できるため、QRコードのスキャンは不要です。Android Chromeでこの機能を有効にするには、chrome://flags#web-identity-digital-credentialsに移動し、フラグを「Enabled」に設定してください。

  1. スキャンと提示: ユーザーがCMWalletでQRコードをスキャンします。walletはユーザーの承認を得て、Verifiable Presentationをブラウザに返します。
  2. Promiseの解決: ブラウザがレスポンスを受信し、フロントエンドでの元の.get() promiseが最終的に解決され、プレゼンテーションのペイロードが渡されます。
  3. バックエンドでの検証: フロントエンドはプレゼンテーションのペイロードをバックエンドの/api/verify/finishエンドポイントにPOSTします。バックエンドはnonceとクレデンシャルを検証します。
  4. 結果: バックエンドは最終的な成功または失敗のメッセージをフロントエンドに返し、UIが更新されます。

4. Verifierの構築#

標準、プロトコル、アーキテクチャフローについてしっかりと理解できたので、Verifierの構築を開始できます。

一緒に進めるか、完成版コードを使用するか

これから、セットアップとコード実装をステップバイステップで進めていきます。もし完成品に直接ジャンプしたい場合は、GitHubリポジトリから完全なプロジェクトをクローンしてローカルで実行できます。

git clone https://github.com/corbado/digital-credentials-example.git

4.1 プロジェクトのセットアップ#

まず、新しいNext.jsプロジェクトを初期化し、必要な依存関係をインストールし、データベースを起動します。

4.1.1 Next.jsアプリの初期化#

ターミナルを開き、プロジェクトを作成したいディレクトリに移動して、次のコマンドを実行します。このプロジェクトではApp Router、TypeScript、Tailwind CSSを使用します。

npx create-next-app@latest . --ts --eslint --tailwind --app --src-dir --import-alias "@/*" --use-npm

このコマンドは、現在のディレクトリに新しいNext.jsアプリケーションの雛形を作成します。

4.1.2 依存関係のインストール#

次に、CBORデコーディング、データベース接続、UUID生成を処理するライブラリをインストールする必要があります。

npm install cbor-web mysql2 uuid @types/uuid

このコマンドは以下をインストールします。

  • cbor-web: mdocクレデンシャルペイロードをデコードするため。
  • mysql2: データベース用のMySQLクライアント。
  • uuid: 一意のチャレンジ文字列を生成するため。
  • @types/uuid: uuidライブラリ用のTypeScript型定義。

4.1.3 データベースの起動#

私たちのバックエンドは、OIDCセッションデータを保存するためにMySQLデータベースを必要とします。これにより、各検証フローが安全でステートフルであることが保証されます。これを簡単にするためにdocker-compose.ymlファイルを含めました。

リポジトリをクローンした場合は、単にdocker-compose up -dを実行するだけです。ゼロから構築している場合は、docker-compose.ymlという名前のファイルに以下の内容で作成してください。

services: mysql: image: mysql:8.0 restart: always environment: MYSQL_ROOT_PASSWORD: rootpassword MYSQL_DATABASE: digital_credentials MYSQL_USER: app_user MYSQL_PASSWORD: app_password ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] timeout: 20s retries: 10 volumes: mysql_data:

このDocker Compose設定には、SQL初期化スクリプトも必要です。sqlという名前のディレクトリを作成し、その中にinit.sqlという名前のファイルに必要なテーブルをセットアップするために以下の内容で作成します。

-- Create database if not exists CREATE DATABASE IF NOT EXISTS digital_credentials; USE digital_credentials; -- Table for storing challenges CREATE TABLE IF NOT EXISTS challenges ( id VARCHAR(36) PRIMARY KEY, challenge VARCHAR(255) NOT NULL UNIQUE, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, used BOOLEAN DEFAULT FALSE, INDEX idx_challenge (challenge), INDEX idx_expires_at (expires_at) ); -- Table for storing verification sessions CREATE TABLE IF NOT EXISTS verification_sessions ( id VARCHAR(36) PRIMARY KEY, challenge_id VARCHAR(36), status ENUM('pending', 'verified', 'failed', 'expired') DEFAULT 'pending', presentation_data JSON, verified_at TIMESTAMP NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE CASCADE, INDEX idx_challenge_id (challenge_id), INDEX idx_status (status) ); -- Table for storing verified credentials data (optional) CREATE TABLE IF NOT EXISTS verified_credentials ( id VARCHAR(36) PRIMARY KEY, session_id VARCHAR(36), credential_type VARCHAR(255), issuer VARCHAR(255), subject VARCHAR(255), claims JSON, verified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (session_id) REFERENCES verification_sessions(id) ON DELETE CASCADE, INDEX idx_session_id (session_id), INDEX idx_credential_type (credential_type) );

両方のファイルが配置されたら、プロジェクトのルートでターミナルを開き、次を実行します。

docker-compose up -d

このコマンドは、バックグラウンドでMySQLコンテナを起動します。

4.2 Next.jsアプリのアーキテクチャ概要#

私たちのNext.jsアプリケーションは、同じプロジェクトの一部でありながら、フロントエンドとバックエンドの関心事を分離するように構成されています。

  • フロントエンド (src/app/page.tsx): 検証フローを開始し、結果を表示する単一のReactページ。ブラウザのDigital Credential APIと対話します。
  • バックエンドAPIルート (src/app/api/verify/...):
    • start/route.ts: OpenID4VPリクエストとセキュリティnonceを生成します。
    • finish/route.ts: walletからのプレゼンテーション(ブラウザ経由)を受信し、nonceを検証し、クレデンシャルをデコードします。
  • ライブラリ (src/lib/):
    • database.ts: すべてのデータベース操作(チャレンジの作成、セッションの検証)を管理します。
    • crypto.ts: CBORベースのmDocクレデンシャルのデコードを処理します。

以下は、内部アーキテクチャを示す図です。

4.3 フロントエンドの構築#

私たちのフロントエンドは意図的に軽量です。その主な責任は、検証フローのユーザー向けトリガーとして機能し、バックエンドとブラウザのネイティブなクレデンシャル処理機能の両方と通信することです。それ自体には複雑なプロトコルロジックは含まれていません。それはすべて委任されています。

具体的には、フロントエンドは以下を処理します。

  • ユーザーインタラクション: ユーザーがプロセスを開始するための「検証」ボタンのようなシンプルなインターフェースを提供します。
  • 状態管理: UIの状態を管理し、検証中のローディングインジケーターを表示し、最終的な成功またはエラーメッセージを表示します。
  • バックエンド通信(リクエスト): /api/verify/startを呼び出し、walletが何を提示すべきかを正確に記述した構造化されたJSONペイロード(protocolrequeststate)を受け取ります。
  • ブラウザAPIの呼び出し: そのJSONオブジェクトをnavigator.credentials.get()に渡し、ネイティブのQRコードをレンダリングしてwalletの応答を待ちます。
  • バックエンド通信(レスポンス): ブラウザAPIがVerifiable Presentationを返したら、このデータをPOSTリクエストで/api/verify/finishエンドポイントに送信し、最終的なサーバーサイドの検証を行います。
  • 結果の表示: バックエンドからの応答に基づいて、検証が成功したか失敗したかをユーザーに通知するためにUIを更新します。

中心となるロジックはstartVerification関数にあります。

// src/app/page.tsx const startVerification = async () => { setLoading(true); setVerificationResult(null); try { // 1. ブラウザがAPIをサポートしているか確認 if (!navigator.credentials?.get) { throw new Error("Browser does not support the Credential API."); } // 2. バックエンドにリクエストオブジェクトを要求 const res = await fetch("/api/verify/start"); const { protocol, request } = await res.json(); // 3. そのオブジェクトをブラウザに渡す – これがネイティブQRコードをトリガーする const credential = await (navigator.credentials as any).get({ mediation: "required", digital: { requests: [ { protocol, // "openid4vp" data: request, // contains dcql_query, nonce, etc. }, ], }, }); // 4. walletの応答(ブラウザから)をサーバーサイドのチェックのためにfinishエンドポイントに転送 const verifyRes = await fetch("/api/verify/finish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(credential), }); const result = await verifyRes.json(); if (verifyRes.ok && result.verified) { setVerificationResult(`Success: ${result.message}`); } else { throw new Error(result.message || "Verification failed."); } } catch (err) { setVerificationResult(`Error: ${(err as Error).message}`); } finally { setLoading(false); } };

この関数は、APIサポートのチェック、バックエンドからのリクエスト取得、ブラウザAPIの呼び出し、そして検証のための結果の送り返しという、フロントエンドロジックの4つの主要なステップを示しています。ファイルの残りの部分は、状態とUIレンダリングのための標準的なReactの定型コードで、GitHubリポジトリで確認できます。

なぜdigitalmediation: 'required'なのか?#

navigator.credentials.get()の呼び出しが、より単純な例とは異なって見えることに気づくかもしれません。これは、私たちが公式のW3C Digital Credentials API仕様に厳密に従っているためです。

  • digitalメンバー: 仕様では、すべてのデジタルクレデンシャルリクエストをdigitalオブジェクト内にネストすることが要求されています。これにより、このAPIに明確で標準化された名前空間が提供され、他のクレデンシャルタイプ(passwordfederatedなど)と区別し、競合することなく将来の拡張が可能になります。

  • mediation: 'required' このオプションは、セキュリティとユーザーエクスペリエンスにとって重要な機能です。ユーザーがクレデンシャルリクエストを承認するために、プロンプト(生体認証スキャン、PIN入力、同意画面など)と能動的に対話することを強制します。これがないと、ウェブサイトがバックグラウンドで静かにクレデンシャルにアクセスしようとする可能性があり、重大なプライバシーリスクとなります。仲介を要求することで、ユーザーが常にコントロールを握り、すべてのトランザクションに対して明示的な同意を与えることを保証します。

4.4 バックエンドエンドポイントの構築#

React UIができたので、次はサーバーで重労働をこなす2つのAPIルートが必要です。

  1. /api/verify/start – OpenID4VPリクエストを構築し、一度限りのチャレンジをMySQLに永続化し、すべてをブラウザに返します。
  2. /api/verify/finish – walletの応答を受け取り、チャレンジを検証し、クレデンシャルを検証&デコードし、最終的に簡潔なJSON結果をUIに返します。

4.4.1 /api/verify/start: OpenID4VPリクエストの生成#

// src/app/api/verify/start/route.ts import { NextResponse } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { createChallenge, cleanupExpiredChallenges } from "@/lib/database"; export async function GET() { // 1️⃣ 短命でランダムなnonce(チャレンジ)を作成 const challenge = uuidv4(); const challengeId = uuidv4(); const expiresAt = new Date(Date.now() + 5 * 60 * 1000); await createChallenge(challengeId, challenge, expiresAt); cleanupExpiredChallenges().catch(console.error); // 2️⃣ 私たちが*欲しいもの*を記述するDCQLクエリを構築 const dcqlQuery = { credentials: [ { id: "cred1", format: "mso_mdoc", meta: { doctype_value: "eu.europa.ec.eudi.pid.1" }, claims: [ { path: ["eu.europa.ec.eudi.pid.1", "family_name"] }, { path: ["eu.europa.ec.eudi.pid.1", "given_name"] }, { path: ["eu.europa.ec.eudi.pid.1", "birth_date"] }, ], }, ], }; // 3️⃣ ブラウザがnavigator.credentials.get()に渡せるオブジェクトを返す return NextResponse.json({ protocol: "openid4vp", // walletが使用するプロトコルをブラウザに伝える request: { dcql_query: dcqlQuery, // 何を提示するか nonce: challenge, // リプレイ攻撃対策 response_type: "vp_token", response_mode: "dc_api", // walletは直接/finishにPOSTする }, state: { credential_type: "mso_mdoc", // 後のチェックのために保持 nonce: challenge, challenge_id: challengeId, }, }); }

主要なパラメータ

nonce – リクエストとレスポンスを結びつける暗号学的チャレンジ(リプレイ攻撃を防ぐ)。• dcql_query – 私たちが必要とする正確なクレームを記述するオブジェクト。このガイドでは、まだ最終的な標準ではないものの、Digital Credential Query Languageの最近のドラフトに触発されたdcql_query構造を使用します。• state – walletによってエコーバックされる任意のJSON。これによりDBレコードを検索できます。

4.4.2 データベースヘルパー#

ファイルsrc/lib/database.tsは、チャレンジと検証セッションに関する基本的なMySQL操作(挿入、読み取り、使用済みマーク)をラップします。このロジックを単一のモジュールに保持することで、後でデータストアを交換するのが簡単になります。


4.5 /api/verify/finish: プレゼンテーションの検証とデコード#

// src/app/api/verify/finish/route.ts import { NextResponse, NextRequest } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { getChallenge, markChallengeAsUsed, createVerificationSession, updateVerificationSession, } from "@/lib/database"; import { decodeDigitalCredential, decodeAllNamespaces } from "@/lib/crypto"; export async function POST(request: NextRequest) { const body = await request.json(); // 1️⃣ 検証可能なプレゼンテーションの各部分を抽出 const vpTokenMap = body.vp_token ?? body.data?.vp_token; const state = body.state; const mdocToken = vpTokenMap?.cred1; // dcqlQueryでこのIDを要求した if (!vpTokenMap || !state || !mdocToken) { return NextResponse.json( { verified: false, message: "Malformed response" }, { status: 400 }, ); } // 2️⃣ 一度限りのチャレンジ検証 const stored = await getChallenge(state.nonce); if (!stored) { return NextResponse.json( { verified: false, message: "Invalid or expired challenge" }, { status: 400 }, ); } const sessionId = uuidv4(); await createVerificationSession(sessionId, stored.id); // 3️⃣ (疑似)暗号チェック – 本番では実際のmDL検証に置き換える // 実際のアプリケーションでは、専用のライブラリを使用して、発行者の公開鍵に対してmdoc署名の完全な // 暗号検証を実行します。 const isValid = mdocToken.length > 0; if (!isValid) { await updateVerificationSession(sessionId, "failed", { reason: "mdoc validation failed", }); return NextResponse.json( { verified: false, message: "Credential validation failed" }, { status: 400 }, ); } // 4️⃣ モバイルDL(mdoc)ペイロードを人間が読めるJSONにデコード const decoded = await decodeDigitalCredential(mdocToken); const readable = decodeAllNamespaces(decoded)["eu.europa.ec.eudi.pid.1"]; await markChallengeAsUsed(state.nonce); await updateVerificationSession(sessionId, "verified", { readable }); return NextResponse.json({ verified: true, message: "mdoc credential verified successfully!", credentialData: readable, sessionId, }); }

walletのレスポンスにおける重要なフィールド

vp_token – walletが返すクレデンシャルを保持するマップ。デモではvp_token.cred1を取得します。• state/startで提供したブロブのエコー。DBレコードを検索するためのnonceを含みます。• mdocToken – ISO mDocを表すBase64URLエンコードされたCBOR構造体。

4.6 mdocクレデンシャルのデコード#

Verifierがブラウザからmdocクレデンシャルを受け取ると、それはCBORエンコードされたバイナリデータを含むBase64URL文字列です。実際のクレームを抽出するために、finishエンドポイントはsrc/lib/crypto.tsのヘルパー関数を使用して多段階のデコードプロセスを実行します。

4.6.1 ステップ1:Base64URLとCBORのデコード#

decodeDigitalCredential関数は、エンコードされた文字列から使用可能なオブジェクトへの変換を処理します。

// src/lib/crypto.ts export async function decodeDigitalCredential(encodedCredential: string) { // 1. Base64URLを標準のBase64に変換 const base64UrlToBase64 = (input: string) => { let base64 = input.replace(/-/g, "+").replace(/_/g, "/"); const pad = base64.length % 4; if (pad) base64 += "=".repeat(4 - pad); return base64; }; const base64 = base64UrlToBase64(encodedCredential); // 2. Base64をバイナリにデコード const binaryString = atob(base64); const byteArray = Uint8Array.from(binaryString, (char) => char.charCodeAt(0)); // 3. CBORをデコード const decoded = await cbor.decodeFirst(byteArray); return decoded; }
  • Base64URLからBase64へ: クレデンシャルをBase64URLから標準のBase64エンコーディングに変換します。
  • Base64からバイナリへ: Base64文字列をバイナリのバイト配列にデコードします。
  • CBORデコーディング: cbor-webライブラリを使用して、バイナリデータを構造化されたJavaScriptオブジェクトにデコードします。

4.6.2 ステップ2:名前空間付きクレームの抽出#

decodeAllNamespaces関数は、デコードされたCBORオブジェクトをさらに処理して、関連する名前空間から実際のクレームを抽出します。

// src/lib/crypto.ts export function decodeAllNamespaces(jsonObj) { const decoded = {}; try { jsonObj.documents.forEach((doc, idx) => { // 1) issuerSigned.nameSpaces: const issuerNS = doc.issuerSigned?.nameSpaces || {}; Object.entries(issuerNS).forEach(([nsName, entries]) => { if (!decoded[nsName]) decoded[nsName] = {}; (entries as any[]).forEach((entry) => { const bytes = Uint8Array.from(entry.value); const decodedEntry = cbor.decodeFirstSync(bytes); Object.assign(decoded[nsName], decodedEntry); }); }); // 2) deviceSigned.nameSpaces (存在する場合): const deviceNS = doc.deviceSigned?.nameSpaces; if (deviceNS?.value?.data) { const bytes = Uint8Array.from(deviceNS.value); decoded[`deviceSigned_ns_${idx}`] = cbor.decodeFirstSync(bytes); } }); } catch (e) { console.error(e); } return decoded; }
  • デコードされたクレデンシャル内のすべてのドキュメントを反復処理します。
  • 各名前空間(例:eu.europa.ec.eudi.pid.1)をデコードして、実際のクレーム値(名前、生年月日など)を抽出します。
  • Issuer署名とデバイス署名の両方の名前空間が存在する場合に対応します。

出力例#

これらのステップを実行した後、finishエンドポイントは、mdocからのクレームを含む人間が読めるオブジェクトを取得します。例:

{ "family_name": "Doe", "given_name": "John", "birth_date": "1990-01-01" }

このプロセスにより、Verifierは表示やさらなる処理のために、mdocクレデンシャルから必要な情報を安全かつ確実に抽出できます。

4.7 UIでの結果表示#

finishエンドポイントは、フロントエンドに最小限のJSONオブジェクトを返します。

{ "verified": true, "message": "mdoc credential verified successfully!", "credentialData": { "family_name": "Doe", "given_name": "John", "birth_date": "1990-01-01" } }

フロントエンドはstartVerification()でこの応答を受け取り、それをReactの状態に保持するだけです。これにより、確認カードをレンダリングしたり、個々のクレームを表示したりできます。例:「ようこそ、John Doeさん(1990-01-01生まれ)!」。

5. Verifierの実行と次のステップ#

これで、ブラウザのネイティブなクレデンシャル処理機能を使用する、完全で動作するVerifierが完成しました。ここでは、それをローカルで実行する方法と、概念実証から本番対応アプリケーションに移行するためにできることを説明します。

5.1 サンプルの実行方法#

  1. リポジトリのクローン:

    git clone https://github.com/corbado/digital-credentials-example.git cd digital-credentials-example
  2. 依存関係のインストール:

    npm install
  3. データベースの起動: マシンでDockerが実行されていることを確認し、MySQLコンテナを起動します。

    docker-compose up -d
  4. アプリケーションの実行:

    npm run dev

    ブラウザでhttp://localhost:3000を開くと、VerifierのUIが表示されます。CMWalletを使用してQRコードをスキャンし、検証フローを完了させることができます。

5.2 次のステップ:デモから本番へ#

このチュートリアルは、Verifierの基本的な構成要素を提供します。これを本番対応にするには、いくつかの追加機能を実装する必要があります。

  • 完全な暗号検証: 現在の実装では、プレースホルダーチェック(mdocToken.length > 0)を使用しています。実際のシナリオでは、Issuerの公開鍵に対してmdoc署名の完全な暗号検証を実行する必要があります(例:DIDを解決するか、公開鍵証明書を取得する)。DID解決標準については、W3C DID Resolution仕様を参照してください。

  • Issuerの失効確認: クレデンシャルは、有効期限前にIssuerによって失効されることがあります。本番のVerifierは、Issuerが提供する失効リストまたはステータスエンドポイントをクエリして、クレデンシャルのステータスを確認する必要があります。W3C Verifiable Credentials Status Listは、クレデンシャル失効リストの標準を提供します。

  • 堅牢なエラーハンドリングとセキュリティ: 包括的なエラーハンドリング、入力検証、APIエンドポイントのレート制限を追加し、転送中のデータを保護するためにすべての通信がHTTPS(TLS)経由であることを確認します。OWASP API Security Guidelinesは、包括的なAPIセキュリティのベストプラクティスを提供します。

  • 複数のクレデンシャルタイプのサポート: ヨーロッパのDigital Identity(EUDI)PIDクレデンシャル以外にもクレデンシャルを受け取る予定がある場合は、異なるdoctype値やクレデンシャル形式を処理するようにロジックを拡張します。W3C Verifiable Credentials Data Modelは、包括的なVC形式の仕様を提供します。

5.3 このチュートリアルのスコープ外#

この例は、理解しやすくするために、意図的にブラウザを介したコアフローに焦点を当てています。以下のトピックはスコープ外と見なされます。

  • 本番環境レベルのセキュリティ: このVerifierは教育目的であり、本番環境で必要とされる堅牢化が欠けています。
  • W3C Verifiable Credentials: このチュートリアルは、モバイル運転免許証用のISO mDoc形式にのみ焦点を当てています。JWT-VCやLinked Data Proofs(LD-Proofs)を持つVCなどの他の一般的な形式は扱いません。
  • 高度なOpenID4VPフロー: redirect_uriを使用したwalletからバックエンドへの直接通信や、動的クライアント登録など、より複雑なOpenID4VP機能は実装していません。

この基盤の上に構築し、これらの次のステップを取り入れることで、独自のアプリケーションでデジタルクレデンシャルを信頼し検証できる、堅牢で安全なVerifierを開発できます。

まとめ#

以上です!250行未満のTypeScriptで、エンドツーエンドのVerifierが完成しました。これは以下のことを行います。

  1. ブラウザのクレデンシャルAPI用のリクエストを公開します。
  2. 準拠する任意のwalletがVerifiable Presentationを提供できるようにします。
  3. サーバーでプレゼンテーションを検証します。
  4. リアルタイムでUIを更新します。

本番環境では、プレースホルダーの検証を完全なISO 18013-5チェックに置き換え、Issuerの失効確認、レート制限、監査ログ、そしてもちろんエンドツーエンドのTLSを追加しますが、中心となる構成要素はまったく同じです。

参考資料#

このチュートリアルで使用または参照した主要なリソース、仕様、ツールの一部を以下に示します。

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start Free Trial

Share this article


LinkedInTwitterFacebook

Table of Contents