Verifier Registrar オンボーディング ガイド

概要

このセクションでは、検証機関登録機関が Google ウォレット アカウント管理サービスにオンボーディングするための手順について説明します。

検証機関登録事業者(他のエンティティに代わって検証を行う IDV 企業など)は、独自の認証局(CA)として機能し、管理するダウンストリームのエンド リライング パーティ(RP)の ID リクエストに署名します。

オンボーディング プロセス

ステップ 1: インテーク フォームとルート証明書を送信し、利用規約に同意する

検証機関登録機関のオンボーディング インテーク フォームに記入して送信します。このフォームでは、サンドボックスと本番環境の両方のルート証明書 を提供します。このオンボーディング インテーク フォームを送信すると、Google ウォレット検証機関登録機関の利用規約に正式に同意したことになります。

ステップ 2: サンドボックスの信頼とテスト

インテーク フォームを送信すると、Google はサンドボックスのルート証明書を Google ウォレットのサンドボックス トラストストアに追加し、通知します。その後、サンドボックスのルートで署名された証明書を使用して、サンドボックスで統合のテストを開始できます。

ステップ 3: E2E 動画デモを録画する

サンドボックス テストが完了したら、最初(1 回目)のリライング パーティの検証フローのエンドツーエンドの動画デモを録画して Google に送信します。

  • 動画の要件:
    • 検証機関ホスト型(セルフホスト型) フローと販売者ホスト型(RP ホスト型) フローの両方のデモを録画します(該当する場合)。
    • 動画では、実際の販売者の表示アセット(名前、ロゴ、利用規約の URL)とアグリゲータの表示アセットを使用します。
    • 検証フローを開始するユーザー インターフェースと画面を明確に示します。

ステップ 4: 承認と本番環境ルートの信頼

エンドツーエンドの動画デモを受け取ると、Google は動画の審査と承認プロセスを開始し、並行して本番環境のルート証明書の信頼プロセスを開始します。両方のプロセスが完了して承認されると、ダウンストリームのエンド RP のサービスを開始できます。

ステップ 5: エンド RP のオンボーディングを継続する

署名するエンド RP ごとに、次の操作を行う必要があります。

  • Google に通知する: 検証機関登録機関クライアント オンボーディング フォームを使用して、新しい RP とその想定されるユースケースを Google に通知します。
  • メタデータを構成する: RP の表示情報(名前、ロゴ、プライバシー ポリシーの URL)を入力し、証明書にグローバルに一意の識別名(サブジェクト) を設定します。

技術仕様

A. 証明書プロファイル

リクエストには、P-256 / ECDSA を使用して生成され、カスタムの Google 拡張機能を含む標準の X.509 v3 証明書で署名する必要があります。

  • カスタム拡張機能 OID: 1.3.6.1.4.1.11129.10.1
  • 重要度: 重要ではない。
  • コンテンツ: RelyingPartyMetadataBytes の SHA256 ハッシュ。ASN.1 OCTET STRING でエンコードされます。

B. メタデータ スキーマ(CBOR)

メタデータは CBOR 形式でエンコードする必要があります。

; in CDDL for CBOR encoding
; schemaVersion = "v1"

RelyingPartyMetadataBytes = #6.24(bstr .cbor RelyingPartyMetadata)


RelyingPartyMetadata = {
  "schema_version": tstr,
  "display": DisplayInfo,
  "aggregator_info": DisplayInfo  ; Optional: include to show your branding alongside the RP
}

DisplayInfo = {
  "display_name": tstr,
  "logo_uri": tstr,             ; See brand guidelines link in following paragraph
  "privacy_policy_uri": tstr
}

logo_uriGoogle ウォレットのブランド ガイドライン に準拠する必要があります。

C. OpenID4VP の統合

署名付き OpenID4VP 認証情報リクエストの形式を設定する場合は、base64url でエンコードされたメタデータを client_metadata オブジェクト内の gw_rp_metadata_bytes フィールドに含めます(次のセクションのサンプル リクエスト コードを参照)。

コンプライアンスと失効

  • 不正使用のモニタリング: Google は悪意のある RP アクティビティをモニタリングし、不正使用が検出された場合は通知します。
  • 迅速な失効: 不正使用の RP の証明書を速やかに失効させ、更新された証明書失効リスト(CRL)を公開する必要があります。
  • 監査: Google は、RP リクエストが登録されたユースケースと一致するように、匿名化されたログを保持します。

次のステップ

検証機関登録機関としてオンボーディングを開始するには、検証機関登録機関のオンボーディング インテーク フォームに記入して送信します。後続のダウンストリーム クライアントのオンボーディングには、検証機関登録機関クライアント オンボーディング フォームを使用します。

オンボーディングと統合に関するよくある質問については、デジタル ID と認証情報に関するよくある質問をご覧ください。

検証機関登録事業者の統合の詳細

次のセクションでは、デジタル認証情報 API と統合する検証機関登録機関の技術的な統合の詳細について説明します(リクエストの形式設定、リクエストの暗号化、API のトリガー、レスポンスの検証、ゼロ知識証明の実装など)。

サポートされている形式と機能

Google ウォレットは、**ISO mdoc** ベースのデジタル ID をサポートしています。

リクエストの形式を設定する

ウォレットから認証情報をリクエストするには、OpenID4VP を使用してリクエストの形式を設定する必要があります。特定の認証情報または複数の認証情報を 1 つの dcql_query オブジェクトでリクエストできます。

JSON リクエストの例

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

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

リクエストの暗号化

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

統合された OpenID4VP メタデータ

認証情報リクエストの形式を設定する場合は、client_metadata オブジェクト内に gw_rp_metadata_bytes フィールドを含める必要があります(次のサンプル リクエスト コードを参照)。このフィールドには、Google ウォレットが ID を確認し、ユーザーにブランディングを表示するために必要な、Base64URL でエンコードされたリライング パーティ メタデータが含まれています。

requestJsoncredential_request パラメータには、次のフィールドが含まれています。

特定の認証情報

{
  "response_type": "vp_token",
  "response_mode": "dc_api.jwt", // change this to dc_api if you want to demo with a non encrypted response.
  "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"
            ],
            "intent_to_retain": false // set this to true if you are saving the value of the field
          },
          {
            "path": [
              "org.iso.18013.5.1",
              "given_name"
            ],
            "intent_to_retain": false
          },
          {
            "path": [
              "org.iso.18013.5.1",
              "age_over_18"
            ],
            "intent_to_retain": false
          }
        ]
      }
    ]
  },
  "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",  // This is required
          "alg" : "ECDH-ES",  // This is required
        }
      ]
    },
    "vp_formats_supported": {
      "mso_mdoc": {
        "deviceauth_alg_values": [
          -7
        ],
        "issuerauth_alg_values": [
          -7
        ]
      }
    },
    "gw_rp_metadata_bytes": "<base64url encoded metadata string>"
  }
}

対象となる認定資格

mDL と ID パスの両方のリクエストの例を次に示します。ユーザーはどちらか一方を選択できます。

{
  "response_type": "vp_token",
  "response_mode": "dc_api.jwt", // change this to dc_api if you want to demo with a non encrypted response.
  "nonce": "1234",
  "dcql_query": {
    "credentials": [
      {
        "id": "mdl-request",
        "format": "mso_mdoc",
        "meta": {
          "doctype_value": "org.iso.18013.5.1.mDL"
        },
        "claims": [
          {
            "path": [
              "org.iso.18013.5.1",
              "family_name"
            ],
            "intent_to_retain": false // set this to true if you are saving the value of the field
          },
          {
            "path": [
              "org.iso.18013.5.1",
              "given_name"
            ],
            "intent_to_retain": false
          },
          {
            "path": [
              "org.iso.18013.5.1",
              "age_over_18"
            ],
            "intent_to_retain": false
          }
        ]
      },
      {  // Credential type 2
        "id": "id_pass-request",
        "format": "mso_mdoc",
        "meta": {
          "doctype_value": "com.google.wallet.idcard.1"
        },
        "claims": [
          {
            "path": [
              "org.iso.18013.5.1",
              "family_name"
            ],
            "intent_to_retain": false // set this to true if you are saving the value of the field
          },
          {
            "path": [
              "org.iso.18013.5.1",
              "given_name"
            ],
            "intent_to_retain": false
          },
          {
            "path": [
              "org.iso.18013.5.1",
              "age_over_18"
            ],
            "intent_to_retain": false
          }
        ]
      }
    ]
    credential_sets : [
      {
        "options": [
          [ "mdl-request" ],
          [ "id_pass-request" ]
        ]
      }
    ]
  },
  "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",  // This is required
          "alg" : "ECDH-ES",  // This is required
        }
      ]
    },
    "vp_formats_supported": {
      "mso_mdoc": {
        "deviceauth_alg_values": [
          -7
        ],
        "issuerauth_alg_values": [
          -7
        ]
      }
    },
    "gw_rp_metadata_bytes": "<base64url encoded metadata string>"
  }
}

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

署名付きリクエスト

署名付きリクエスト (JWT で保護された承認リクエスト)は、 PKI インフラストラクチャを使用して暗号で署名された JSON ウェブトークン (JWT)内に検証可能なプレゼンテーション リクエストをカプセル化し、リクエストの整合性を確保して、 Google ウォレットに ID を証明します。

前提条件

署名付きリクエストのコード変更を実装する前に、次のことを確認してください。

  • 秘密鍵: リクエストに署名するには、サーバーで管理される秘密鍵(楕円曲線 ES256 など)が必要です。
  • 証明書: 鍵ペアから派生した標準の X.509 証明書が必要です。
  • 登録: 公開証明書が Google ウォレットに登録されていることを確認します。

リクエスト作成ロジック

リクエストを作成するには、秘密鍵を使用してペイロードを JWS でラップする必要があります。

def construct_openid4vp_request(
    doctypes: list[str],
    requested_fields: list[dict],
    nonce_base64: str,
    jwe_encryption_public_jwk: jwk.JWK,
    is_zkp_request: bool,
    is_signed_request: bool,
    state: dict,
    origin: str
) -> dict:

    # ... [Existing logic to build 'presentation_definition' and basic 'request_payload'] ...

    # ------------------------------------------------------------------
    # SIGNED REQUEST IMPLEMENTATION (JAR)
    # ------------------------------------------------------------------
    if is_signed_request:
        try:
            # 1. Load the Verifier's Certificate
            # We must load the PEM string into a cryptography x509 object
            verifier_cert_obj = x509.load_pem_x509_certificate(
                CERTIFICATE.encode('utf-8'),
                backend=default_backend()
            )

            # 2. Calculate Client ID (x509_hash)
            # We calculate the SHA-256 hash of the DER-encoded certificate.
            cert_der = verifier_cert_obj.public_bytes(serialization.Encoding.DER)
            verifier_fingerprint_bytes = hashlib.sha256(cert_der).digest()

            # Create a URL-safe Base64 hash (removing padding '=')
            verifier_fingerprint_b64 = base64.urlsafe_b64encode(verifier_fingerprint_bytes).decode('utf-8').rstrip("=")

            # Format the client_id as required by the spec
            client_id = f'x509_hash:{verifier_fingerprint_b64}'

            # 3. Update Request Payload with JAR specific fields
            request_payload["client_id"] = client_id

            # Explicitly set expected origins to prevent relay attacks
            # Format for android origin: origin = android:apk-key-hash:<base64SHA256_ofAppSigningCert>
            # Format for web origin: origin = <origin_url>
            if origin:
                request_payload["expected_origins"] = [origin]

            # 4. Create Signed JWT (JWS)
            # Load the signing private key
            signing_key = jwk.JWK.from_pem(PRIVATE_KEY.encode('utf-8'))

            # Initialize JWS with the JSON payload
            jws_token = jws.JWS(json.dumps(request_payload).encode('utf-8'))

            # Construct the JOSE Header
            # 'x5c' (X.509 Certificate Chain) is critical: it allows the wallet
            # to validate your key against the one registered in the console.
            x5c_value = base64.b64encode(cert_der).decode('utf-8')

            protected_header = {
                "alg": "ES256",                 # Algorithm (e.g., ES256 or RS256)
                "typ": "oauth-authz-req+jwt",   # Standard type for JAR
                "kid": "1",                     # Key ID
                "x5c": [x5c_value]              # Embed the certificate
            }

            # Sign the token
            jws_token.add_signature(
                key=signing_key,
                alg=None,
                protected=json_encode(protected_header)
            )

            # 5. Return the Request Object
            # Instead of returning the raw JSON, we return the signed JWT string
            # under the 'request' key.
            return {"request": jws_token.serialize(compact=True)}

        except Exception as e:
            print(f"Error signing OpenID4VP request: {e}")
            return None

    # ... [Fallback for unsigned requests] ...
    return request_payload

API をトリガーする

API リクエスト全体をサーバーサイドで生成する必要があります。プラットフォームに応じて、生成された JSON をプラットフォーム API に渡します。

アプリ内(Android)

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

依存関係を更新する

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

dependencies {
    implementation("androidx.credentials:credentials:1.5.0-beta01")
    implementation("androidx.credentials:credentials-play-services-auth:1.5.0-beta01")
}

認証情報マネージャーを構成する

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

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

ID 属性をリクエストする

ID リクエストに個別のパラメータを指定する代わりに、アプリは CredentialOption 内の JSON 文字列としてすべてのパラメータをまとめて提供します。 認証情報マネージャーは、この JSON 文字列の内容を調べずに、利用可能なデジタル ウォレットに渡します。各ウォレットは、次の処理を行います。 - JSON 文字列を解析して、ID リクエストを理解する。 - 保存されている認証情報のうち、リクエストを満たすものを判断する(該当する場合)。

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

GetDigitalCredentialOption() 関数呼び出しでは、リクエストの形式requestJsonrequest として使用します。

// 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-v1-signed",
  "data" : {
    <encrpted_response>
  }
}

このレスポンスをサーバーに返して、その信頼性を検証します。 認証情報レスポンスを検証する手順については

ウェブ

Chrome またはその他のサポートされているブラウザでデジタル認証情報 API を使用して ID 認証情報をリクエストするには、次のリクエストを行います。

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

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

レスポンスを検証する

ウォレットから暗号化された identityToken(JWT)が返されたら、データを信頼する前に厳密なサーバーサイド検証を行う必要があります。

レスポンスを復号する

リクエストの client_metadata で送信された公開鍵に対応する秘密鍵を使用して、JWE を復号します。これにより、vp_token が生成されます。

Python の例:

  from jwcrypto import jwe, jwk

  # Retrieve the Private Key from Datastore
  reader_private_jwk = jwk.JWK.from_json(jwe_private_key_json_str)
  # Save public key thumbprint for session transcript
  encryption_public_jwk_thumbprint = reader_private_jwk.thumbprint()


  # 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": ["<base64UrlNoPadding_encoded_credential>"] // This applies to OpenID4VP 1.0 spec.
    }
  }
  1. セッションの文字起こしを作成する

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

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

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

    Android ハンドオーバー

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

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

        BrowserHandoverData =[
          origin,               // Origin URL
          nonce,               //  nonce that was used to generate credential request
          encryption_public_jwk_thumbprint,  // Encryption public key (JWK) Thumbprint
        ]
    
        BrowserHandoverDataBytes = hashlib.sha256(cbor2.dumps(BrowserHandoverData)).digest()
        

    SessionTranscript を使用して、ISO/IEC 18013-5:2021 の条項 9 に従ってデバイス レスポンスを検証する必要があります。

    この検証には、次の手順が含まれます。

  2. 発行者証明書を確認する: issuerAuth から発行者の署名証明書チェーンを抽出し、信頼できる IACA ルート証明書に対して検証します。サポートされている発行者の IACA 証明書を参照してください。

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

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

  5. 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
}

プライバシー保護年齢確認(ZKP)

ゼロ知識証明(正確な生年月日を確認せずにユーザーが 18 歳以上であることを確認するなど)をサポートするには、リクエスト形式を mso_mdoc_zk に変更し、必要な zk_system_type 構成を指定します。

ZKP とその機能の概要については、よくある質問をご覧ください。

  ...
  "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": "f88a39e561ec0be02bb3dfe38fb609ad154e98decbbe632887d850fc612fea6f", // This will differ if you need more than 1 attribute.
          "num_attributes": 1, // number of attributes (in claims) this has can support
          "version": 5,
          "block_enc_hash": 4096,
          "block_enc_sig": 2945,
        }
        {
          "system": "longfellow-libzk-v1",
          "circuit_hash": "137e5a75ce72735a37c8a72da1a8a0a5df8d13365c2ae3d2c2bd6a0e7197c7c6", // This will differ if you need more than 1 attribute.
          "num_attributes": 1, // number of attributes (in claims) this has can support
          "version": 6,
          "block_enc_hash": 4096,
          "block_enc_sig": 2945,
        }
       ],
       "verifier_message": "challenge"
      },
     "claims": [{
         ...
      "client_metadata": {
        "jwks": {
          "keys": [ // sample request encryption key
            {
              ...

ウォレットから暗号化されたゼロ知識証明が返されます。Google の longfellow-zk ライブラリを使用して、発行者の IACA 証明書に対してこの証明を検証できます。

verifier-service には、特定の発行者の IACA 証明書に対して レスポンスを検証できる、デプロイ可能な Docker ベースのサーバーが含まれています。

certs.pem を変更して、信頼する IACA 発行者証明書 を管理できます。

リソースとサポート