デジタル認証情報のオンラインでの受領

デジタル ID は、アプリ内フローとウェブフローの両方で受け付けることができます。Google ウォレットの認証情報を受け入れるには、次の操作を行う必要があります。

  1. 提供された手順に沿って、アプリまたはウェブを使用して統合します。
  2. テスト ID を使用して、Google ウォレットのサンドボックスでフローをテストします。
  3. 公開の準備が整ったら、こちらのフォームに記入して、Google ウォレットの認証情報の受け入れに関する利用規約に同意してください。

前提条件

ID のデジタル表示をテストするには、まず、目的のテスト アカウント(Gmail アカウントである必要があります)を使用して公開ベータ版プログラムに登録する必要があります。その後、Google の担当者に以下の詳細情報を提供してください。

  • 利用規約のリンク
  • ロゴ
  • ウェブサイト
  • アプリ パッケージ ID(Android アプリ統合の場合)
    • 開発 / デバッグビルドを含む
  • アプリ署名
    • $ $ANDROID_SDK/build-tools/$BUILD_TOOLS_VERSION/apksigner verify --print-certs -v $APK
  • 公開ベータ版への参加に使用した Gmail ID

サポートされている認証情報の形式

デジタル ID ドキュメントのデータ形式を定義する標準案がいくつか存在し、そのうち 2 つが業界で大きな支持を得ています。

  1. mdocs - ISO によって定義されます。
  2. w3c Verifiable Credentials - w3c によって定義されます。

Android 認証情報マネージャーは両方の形式をサポートしていますが、現時点では Google ウォレットは mdoc ベースのデジタル ID のみをサポートしています。

サポートされている認証情報

Google ウォレットは 2 種類の認証情報に対応しています。

  1. モバイル運転免許証(mDL)
  2. ID パス

フローで 1 つのパラメータを変更するだけで、どちらの認証情報もリクエストできます。

ユーザー エクスペリエンス

このセクションでは、推奨されるオンライン プレゼンテーションのフローについて説明します。このフローは、アルコール飲料の配達アプリに年齢を提示する流れを示していますが、ウェブや他の種類の提示でも UX は同様です。

アプリまたはウェブサイトで年齢確認を求められる 利用可能な対象となる認証情報が表示される Google ウォレットに確認ページが表示される ユーザーが認証して共有を確認する アプリまたはウェブサイトに送信されるデータ
アプリまたはウェブサイトで年齢確認を求められる 利用可能な対象となる認証情報が表示される Google ウォレットに確認ページが表示される ユーザーが認証して共有を確認する アプリまたはウェブサイトに送信されるデータ

重要な注意事項

  1. アプリやウェブサイトは、API へのエントリ ポイントの作成方法を柔軟に選択できます。ステップ 1 で示したように、API を通じて Google ウォレット以外のオプションも利用できるようになることが見込まれるため、[デジタル ID で確認] などの汎用的なボタンを表示することをおすすめします。
  2. ステップ 2 のセレクタ画面は Android によってレンダリングされます。有効な認証情報は、各ウォレットが提供する登録ロジックと、証明書利用者から送信されたリクエストとの照合によって決定されます。
  3. ステップ 3 は Google ウォレットによってレンダリングされます。この画面には、デベロッパーが提供した名前、ロゴ、プライバシー ポリシーが表示されます。

デジタル ID フローを追加する

ユーザーが認証情報を持っていない場合は、[デジタル ID で確認] ボタンの横に、Google ウォレットへのディープリンクを提示して、ユーザーがデジタル ID を追加できるようにすることをおすすめします。

アプリまたはウェブサイトで年齢確認を求められる ユーザーが Google ウォレットに移動してデジタル ID を取得する
アプリまたはウェブサイトで年齢確認を求められる ユーザーが Google ウォレットに移動してデジタル ID を取得する

デジタル ID を利用できません

ユーザーがデジタル ID を持っていない状態で [デジタル ID で確認] オプションを選択すると、このエラー メッセージが表示されます。

アプリまたはウェブサイトで年齢確認を求められる デジタル ID をお持ちでない場合はエラーが表示される
アプリまたはウェブサイトで年齢確認を求められる デジタル ID をお持ちでない場合はエラーが表示される

ユーザーのプライバシーを保護するため、ユーザーが利用可能なデジタル ID を持っているかどうかをサイレントに学習する機能は API でサポートされていません。そのため、図のようにオンボーディング リンク オプションを含めることをおすすめします。

ウォレットから ID 認証情報をリクエストするためのリクエスト フォーマット

Android デバイスまたはウェブ上の任意のウォレットから ID 認証情報を取得するための mdoc requestJson リクエストのサンプルを次に示します。

{
      "requests" : [
        {
          "protocol": "openid4vp",
          "data": {<credential_request>} // This is an object, shouldn't be a string.
        }
      ]
}

リクエストの暗号化

client_metadata には、リクエストごとの暗号化公開鍵が含まれています。リクエストごとの秘密鍵を保存し、それを使用してウォレット アプリから受け取ったトークンを認証、認可する必要があります。

requestJsoncredential_request パラメータは、次のフィールドで構成されます。

{
  "response_type": "vp_token",
  "response_mode": "dc_api.jwt",
  "nonce": "1234",
  "dcql_query": {
    "credentials": [
      {
        "id": "cred1",
        "format": "mso_mdoc",
        "meta": {
          "doctype_value": "org.iso.18013.5.1.mDL"  // this is for mDL. Use com.google.wallet.idcard.1 for ID pass
        },
        "claims": [
          {
            "path": [
              "org.iso.18013.5.1",
              "family_name"
            ]
          },
          {
            "path": [
              "org.iso.18013.5.1",
              "given_name"
            ]
          },
          {
            "path": [
              "org.iso.18013.5.1",
              "age_over_18"
            ]
          }
        ]
      }
    ]
  },
  "client_metadata": {
    "jwks": {
      "keys": [ // sample request encryption key
        {
          "kty": "EC",
          "crv": "P-256",
          "x": "pDe667JupOe9pXc8xQyf_H03jsQu24r5qXI25x_n1Zs",
          "y": "w-g0OrRBN7WFLX3zsngfCWD3zfor5-NLHxJPmzsSvqQ",
          "use": "enc",
          "kid" : "1",
          "alg" : "ECDH-ES",
        }
      ]
    },
    "authorization_encrypted_response_alg": "ECDH-ES",
    "authorization_encrypted_response_enc": "A128GCM"
  }
}

Google ウォレットに保存されている ID 認証情報から、任意の数のサポートされている属性をリクエストできます。

アプリ内

Android アプリから ID 認証情報をリクエストする手順は次のとおりです。

依存関係を更新する

プロジェクトの build.gradle で、認証情報マネージャー(ベータ版)を使用するように依存関係を更新します。

dependencies {
    implementation("androidx.credentials:credentials:1.5.0-beta01")
    // optional - needed for credentials support from play services, for devices running Android 13 and below.
    implementation("androidx.credentials:credentials-play-services-auth:1.5.0-beta01")
}

Credential Manager を構成する

CredentialManager オブジェクトを構成して初期化するには、次のようなロジックを追加します。

// Use your app or activity context to instantiate a client instance of CredentialManager.
val credentialManager = CredentialManager.create(context)

ID 属性をリクエストする

ID リクエストに個々のパラメータを指定する代わりに、アプリは CredentialOption 内の JSON 文字列としてすべてのパラメータをまとめて提供します。Credential Manager は、この JSON 文字列の内容を検証せずに、利用可能なデジタル ウォレットに渡します。各ウォレットは、JSON 文字列を解析して ID リクエストを理解する役割を担います。- 保存されている認証情報のうち、リクエストを満たすものを特定する。

Android アプリの統合の場合でも、サーバーでリクエストを作成することをおすすめします。

GetDigitalCredentialOption() 関数呼び出しで request を構成する リクエスト形式requestJson を使用します。

// The request in the JSON format to conform with
// the JSON-ified Digital Credentials API request definition.
val requestJson = generateRequestFromServer()
val digitalCredentialOption =
    GetDigitalCredentialOption(requestJson = requestJson)

// Use the option from the previous step to build the `GetCredentialRequest`.
val getCredRequest = GetCredentialRequest(
    listOf(digitalCredentialOption)
)

coroutineScope.launch {
    try {
        val result = credentialManager.getCredential(
            context = activityContext,
            request = getCredRequest
        )
        verifyResult(result)
    } catch (e : GetCredentialException) {
        handleFailure(e)
    }
}

レスポンスを確認して検証する

ウォレットからレスポンスが返ってきたら、レスポンスが成功し、credentialJson レスポンスが含まれているかどうかを確認します。

// Handle the successfully returned credential.
fun verifyResult(result: GetCredentialResponse) {
    val credential = result.credential
    when (credential) {
        is DigitalCredential -> {
            val responseJson = credential.credentialJson
            validateResponseOnServer(responseJson) // make a server call to validate the response
        }
        else -> {
            // Catch any unrecognized credential type here.
            Log.e(TAG, "Unexpected type of credential ${credential.type}")
        }
    }
}

// Handle failure.
fun handleFailure(e: GetCredentialException) {
  when (e) {
        is GetCredentialCancellationException -> {
            // The user intentionally canceled the operation and chose not
            // to share the credential.
        }
        is GetCredentialInterruptedException -> {
            // Retry-able error. Consider retrying the call.
        }
        is NoCredentialException -> {
            // No credential was available.
        }
        else -> Log.w(TAG, "Unexpected exception type ${e::class.java}")
    }
}

credentialJson レスポンスには、W3C で定義された暗号化された identityToken(JWT)が含まれます。このレスポンスの作成はウォレット アプリが行います。

例:

{
  "protocol" : "openid4vp",
  "data" : {
    <encrpted_response>
  }
}

このレスポンスをサーバーに渡して、信頼性を検証します。認証情報のレスポンスを検証する手順をご覧ください。

ウェブ

Chrome で Digital Credentials API を使用して Identity Credentials をリクエストするには、Digital Credentials API のオリジン トライアルに登録する必要があります。

const credentialResponse = await navigator.credentials.get({
          digital : {
          requests : [
            {
              protocol: "openid4vp",
              data: {<credential_request>} // This is an object, shouldn't be a string.
            }
          ]
        }
      })

この API からのレスポンスをサーバーに送り返して、認証情報のレスポンスを検証します。

認証情報のレスポンスを検証する手順

アプリまたはウェブサイトから暗号化された identityToken を受け取ったら、レスポンスを信頼する前に複数の検証を行う必要があります。

  1. 秘密鍵を使用してレスポンスを復号する

    まず、保存した秘密鍵を使用してトークンを復号し、レスポンス JSON を取得します。

    Python の例:

    from jwcrypto import jwe, jwk
    
    # Retrieve the Private Key from Datastore
    reader_private_jwk = jwk.JWK.from_json(jwe_private_key_json_str)
    
    # Decrypt the JWE encrypted response from Google Wallet
    jwe_object = jwe.JWE()
    jwe_object.deserialize(encrypted_jwe_response_from_wallet)
    jwe_object.decrypt(reader_private_jwk)
    decrypted_payload_bytes = jwe_object.payload
    decrypted_data = json.loads(decrypted_payload_bytes)
    

    decrypted_data は、認証情報を含む vp_token JSON を返します。

    {
      "vp_token":
      {
        "cred1": "<credential_token>"
      }
    }
    
  2. セッションの文字起こしを作成する

    次のステップでは、Android またはウェブ固有のハンドオーバー構造を使用して、ISO/IEC 18013-5:2021 から SessionTranscript を作成します。

    SessionTranscript = [
      null,                // DeviceEngagementBytes not available
      null,                // EReaderKeyBytes not available
      [
        "OpenID4VPDCAPIHandover",
        AndroidHandoverDataBytes   // BrowserHandoverDataBytes for Web
      ]
    ]
    

    Android とウェブの両方のハンドオーバーで、credential_request の生成に使用したのと同じ nonce を使用する必要があります。

    Android ハンドオーバー

        AndroidHandoverData = [
          origin,             // "android:apk-key-hash:<base64SHA256_ofAppSigningCert>",
          clientId,           // "android-origin:<app_package_name>",
          nonce,              // nonce that was used to generate credential request
        ]
    
        AndroidHandoverDataBytes = hashlib.sha256(cbor2.dumps(AndroidHandoverData)).digest()
        

    ブラウザのハンドオーバー

        BrowserHandoverData =[
          origin,               // Origin URL
          clientId,             // "web-origin:<origin>"
          nonce,               //  nonce that was used to generate credential request
        ]
    
        BrowserHandoverDataBytes = hashlib.sha256(cbor2.dumps(BrowserHandoverData)).digest()
        

    SessionTranscript を使用して、ISO/IEC 18013-5:2021 の条項 9 に従って DeviceResponse を検証しなければなりません。これには、次のような複数のステップが含まれます。

  3. State Issuer Cert を確認します。サポートされている発行者の IACA 証明書を確認する

  4. MSO 署名を検証する(18013-5 セクション 9.1.2)

  5. データ要素の ValueDigest を計算して確認する(18013-5 セクション 9.1.2)

  6. deviceSignature 署名を検証する(18013-5 セクション 9.1.3)

{
  "version": "1.0",
  "documents": [
    {
      "docType": "org.iso.18013.5.1.mDL",
      "issuerSigned": {
        "nameSpaces": {...}, // contains data elements
        "issuerAuth": [...]  // COSE_Sign1 w/ issuer PK, mso + sig
      },
      "deviceSigned": {
        "nameSpaces": 24(<< {} >>), // empty
        "deviceAuth": {
          "deviceSignature": [...] // COSE_Sign1 w/ device signature
        }
      }
    }
  ],
  "status": 0
}

ソリューションをテストする

ソリューションをテストするには、Google のオープンソース リファレンス ホルダーである Android アプリをビルドして実行します。リファレンス ホルダー アプリをビルドして実行する手順は次のとおりです。

  • リファレンス アプリのリポジトリのクローンを作成します。
  • Android Studio でプロジェクトを開きます。
  • appholder ターゲットをビルドし、Android デバイスまたはエミュレータで実行します。

ゼロ知識証明(ZKP)に基づく検証

ゼロ知識証明(ZKP)は、個人(証明者)が、実際の基盤となるデータ自体を開示することなく、特定の身元情報を持っていることや特定の基準を満たしていること(18 歳以上である、有効な資格情報を保持しているなど)を検証者に証明できる暗号手法です。つまり、機密情報を非公開のまま、自分の身元に関するステートメントの真実性を確認する方法です。

ID データの直接共有に依存するデジタル ID システムでは、ユーザーが過剰な個人情報を共有する必要があることが多く、データ侵害や ID 盗用のリスクが高まります。ZKP はパラダイム シフトをもたらし、最小限の開示で検証を可能にします。

デジタル ID における ZKP の主なコンセプト:

  • 証明者: 自分の身元の一面を証明しようとしている個人。
  • 検証ツール: ID 属性の証明をリクエストするエンティティ。
  • 証明: 秘密情報を開示することなく、証明者が検証者に主張の真実性を納得させることを可能にする暗号化プロトコル。

ゼロ知識証明のコア プロパティ:

  • 完全性: ステートメントが true で、証明者と検証者の両方が正直であれば、検証者は納得します。
  • 健全性: ステートメントが false の場合、不正な証明者は(非常に高い確率で)正直な検証者にステートメントが true であると納得させることができません。
  • ゼロ知識: 検証者は、ステートメントが真実であるという事実以外は何も学習しません。証明者の身元に関する実際のデータは公開されません。

Google ウォレットからゼロ知識証明を取得するには、リクエスト形式を mso_mdoc_zk に変更し、リクエストzk_system_type を追加する必要があります。

  ...
  "dcql_query": {
    "credentials": [{
      "id": "cred1",
      "format": "mso_mdoc_zk",
      "meta": {
        "doctype_value": "org.iso.18013.5.1.mDL"
        "zk_system_type": [
         {
            "system": "longfellow-libzk-v1",
            "circuit_hash": "2093f64f54c81fb2f7f96a46593951d04005784da3d479e4543e2190dcf205d6", //This will differ if you need more than 1 attribute.
            "num_attributes": 1, // number of attributes (in claims) this has can support
            "version": 2
        },
        {
            "system": "longfellow-libzk-v1",
            "circuit_hash": "2836f0df5b7c2c431be21411831f8b3d2b7694b025a9d56a25086276161f7a93", // This will differ if you need more than 1 attribute.
            "num_attributes": 1, // number of attributes (in claims) this has can support
            "version": 1
        }
       ],
       "verifier_message": "challenge"
      },
     "claims": [{
         ...