このガイドでは、証明書利用者(RP)がデジタル認証情報 API を技術的に統合して、Android アプリとウェブ全体で Google ウォレットからモバイル運転免許証(mDL)と ID パスをリクエストして検証する方法について説明します。
登録プロセスと前提条件
本番環境で公開する前に、証明書利用者アプリケーションを Google に正式に登録する必要があります。
- サンドボックスでテストする: サンドボックス環境とテスト ID の作成を使用して、すぐに開発を開始できます。テストでは利用規約への同意は必要ありません。
- 登録フォームを送信する: RP オンボーディング フォームに入力します。通常、オンボーディングには 3 ~ 5 営業日かかります。プロダクト名とロゴは、ユーザー向けの同意画面に表示され、ユーザーがデータをリクエストしているユーザーを特定するのに役立ちます。
- 利用規約に同意する: ライブ配信を開始する前に、利用規約に署名する必要があります。
サポートされている形式と機能
Google ウォレットは、ISO mdoc ベースのデジタル ID をサポートしています。
- サポートされている認証情報: サポートされている認証情報と属性を確認できます。
- サポートされているプロトコル: OpenID4VP(バージョン 1.0)。
- 最小 Android SDK: Android 9(API レベル 28)以降。
- ブラウザのサポート: Digital Credentials API をサポートするブラウザの包括的なリストについては、エコシステムのサポートページを参照してください。
リクエストの形式
ウォレットから認証情報をリクエストするには、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 には、リクエストごとの暗号化公開鍵が含まれます。リクエストごとに秘密鍵を保存し、ウォレット アプリから受け取ったトークンの認証と認可に使用する必要があります。
requestJson の credential_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
],
"isserauth_alg_values": [
-7
]
}
}
}
}
対象となる認証情報
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
],
"isserauth_alg_values": [
-7
]
}
}
}
}
Google ウォレットに保存されている任意の ID 認証情報から、任意の数のサポートされている属性をリクエストできます。
署名付きリクエスト
署名付きリクエスト(JWT で保護された認証リクエスト)は、PKI インフラストラクチャを使用して、検証可能なプレゼンテーション リクエストを暗号で署名された JSON ウェブトークン(JWT)内にカプセル化し、リクエストの完全性を確保して、Google ウォレットに ID を証明します。
前提条件
署名付きリクエストのコード変更を実装する前に、次のことを確認してください。
- 秘密鍵: サーバーで管理されているリクエストに署名するには、秘密鍵(楕円曲線
ES256など)が必要です。 - 証明書: 鍵ペアから派生した標準の X.509 証明書が必要です。
- 登録: 公開証明書が Google ウォレットに登録されていることを確認します。サポートチーム(
wallet-identity-rp-support@google.com)にお問い合わせください
リクエスト構築ロジック
リクエストを作成するには、秘密鍵を使用してペイロードを 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")
}
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() 関数呼び出しでは、リクエスト形式の requestJson を request として使用します。
// 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 またはその他のサポートされているブラウザで Digital Credentials API を使用して Identity Credentials をリクエストするには、次のリクエストを行います。
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.
}
}
セッションの文字起こしを作成する
次のステップでは、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>", 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 に従ってデバイス レスポンスを検証する必要があります。これには、次のような複数のステップが含まれます。State Issuer Cert を確認します。サポートされている発行者の IACA 証明書を参照してください。
MSO 署名を検証する(18013-5 セクション 9.1.2)
データ要素の
ValueDigestsを計算して確認する(18013-5 セクション 9.1.2)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 構成を指定します。
...
"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 発行者証明書を管理できます。
リソースとサポート
- リファレンス実装: GitHub で Identity Verifiers リファレンス実装をご確認ください。
- テスト ウェブサイト: verifier.multipaz.org でエンドツーエンドのフローを試してください。
- OpenID4VP 仕様: openID4VP の技術仕様をご覧ください。
- サポート: 統合時のデバッグのサポートやご質問については、
wallet-identity-rp-support@google.comにお問い合わせください。