本指南說明直接依賴方 (RP) (個別商家、企業和應用程式開發人員) 如何整合 Digital Credentials API,在 Android 應用程式和網路上要求及驗證數位身分憑證。
信賴方整合的運作方式
每個依賴方整合程序都包含四個步驟。步驟 A 和 C 在伺服器上執行;步驟 B 在 Android 應用程式或瀏覽器中執行。選取任一步驟,即可跳至導入詳細資料:

支援的格式、憑證和功能
Google 錢包支援 Android 9 (API 級別 28) 以上版本和支援的瀏覽器,可透過 OpenID4VP (1.0 版) 支援ISO mdoc 數位身分證件。
支援的憑證和屬性
查詢可向 Google 錢包要求的 doctype_value 識別資訊 (mDL、身分憑證、附相片的身分證件) 和屬性聲明路徑。
支援的核發機構和 IACA 憑證
下載美國各州、國際核發機構和 Sandbox Utopia 核發機構的官方 IACA 根憑證,以驗證 issuerAuth。
步驟 A:設定要求格式並簽署 (伺服器端)
如要向任何錢包要求憑證,您必須在伺服器上使用 OpenID4VP 設定要求格式。您可以在單一 dcql_query 物件中要求特定憑證或多個憑證。
JSON 要求範例
以下是 mdoc requestJson 要求的範例,用於從 Android 裝置或網頁上的任何錢包取得身分憑證。
{
"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 欄位 (如下方的要求程式碼範例所示)。這個欄位包含 Base64URL 編碼的依賴方中繼資料,Google 錢包會使用這項資料驗證您的身分,並向使用者顯示您的品牌資訊。
憑證要求酬載
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": {
// Use org.iso.18013.5.1.mDL for mDL,
// com.google.wallet.idcard.1 for ID pass, or
// org.iso.23220.photoid.1 for ID pass (using photo ID document type)
"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
}
]
}
]
},
"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、身分憑證和身分憑證 (使用附相片的身分證件類型) 的要求範例。使用者可以選擇其中一種方式繼續操作。
{
"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 pass
"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 type 3: ID pass (using photo ID document type)
"id": "photo_id-request",
"format": "mso_mdoc",
"meta": {
"doctype_value": "org.iso.23220.photoid.1"
},
"claims": [
{
"path": [
"org.iso.23220.1",
"family_name"
],
"intent_to_retain": false // set this to true if you are saving the value of the field
},
{
"path": [
"org.iso.23220.1",
"given_name"
],
"intent_to_retain": false
},
{
"path": [
"org.iso.23220.1",
"age_over_18"
],
"intent_to_retain": false
}
]
}
]
credential_sets : [
{
"options": [
[ "mdl-request" ],
[ "id_pass-request" ],
[ "photo_id-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 錢包儲存的任何身分憑證中,要求任意數量的支援屬性。
經簽署的要求
經簽署的要求 (JWT 安全授權要求) 會使用 PKI 基礎架構,將可驗證的憑證要求封裝在加密簽署的 JSON Web Token (JWT) 中,確保要求完整性,並向 Google 錢包證明您的身分。
必要條件
為經簽署的要求導入程式碼變更前,請確認您已完成下列事項:
- 私密金鑰:您需要私密金鑰 (例如橢圓曲線
ES256) 才能簽署要求,並在伺服器上管理。如要以依賴方身分進行沙箱測試,請使用沙箱測試私密金鑰。 - 憑證:您需要從金鑰組衍生的標準 X.509 憑證。
- 註冊:確認公開憑證 (或驗證者註冊機構的根憑證授權單位) 受到 Google 錢包信任。
要求建構邏輯
建構及簽署 OpenID4VP 要求時,請特別注意每個欄位預期的確切編碼格式,混用 base64 和 base64url 或填補 (=) 會導致簽章或來源驗證失敗:
| 欄位 | 必要編碼 / 格式 | 字元和填補 |
|---|---|---|
client_id (x509_hash:<hash>) |
base64url (不含填補) | 使用網址安全字母 (-、_);移除結尾的 = |
expected_origins (android:apk-key-hash:<hash>) |
標準 Base64 (不含填充) | 使用標準字母 (+、/);移除結尾的 = |
expected_origins (https://...) |
小寫網址,結尾不含斜線 | 範例:https://example.com (結尾不含 /) |
x5c JWS 標頭 |
標準 Base64 (含填補) | 使用標準字母 (+、/);保留結尾的 = 填補 |
如要建構要求,請使用私密金鑰,並將酬載包裝在 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()
# Important: Use URL-safe Base64 (base64url: '-', '_') without padding ('=') for client_id
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
# Important: Use standard Base64 (not URL-safe) without padding for android:apk-key-hash:
# origin = f"android:apk-key-hash:{base64.b64encode(apk_cert_hash).decode('utf-8').rstrip('=')}"
# Format for web origin (lowercase URL without trailing slash): origin = "https://example.com"
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.
# Important: Use standard Base64 ('+', '/') WITH padding ('=') for x5c
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
步驟 B:觸發 API (用戶端)
整個 API 要求應在伺服器端產生。視平台而定,將產生的 JSON 傳遞至平台 API。
應用程式內 (Android)
如要從 Android 應用程式要求身分憑證,請按照下列步驟操作:
更新依附元件
在專案的 build.gradle 中更新依附元件,以便使用 Credential Manager (Beta 版):
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)
要求身分屬性
應用程式不會為身分要求指定個別參數,而是將所有參數以 JSON 字串的形式,一併提供給 CredentialOption。Credential Manager 會將這個 JSON 字串傳遞至可用的數位錢包,而不會檢查其內容。每個錢包都必須負責:
- 剖析 JSON 字串,瞭解身分要求。
- 判斷哪些儲存的憑證 (如有) 符合要求。
建議合作夥伴在伺服器上建立要求,即使是 Android 應用程式整合也一樣。
您將使用設定要求格式並簽署要求中的 requestJson,做為 GetDigitalCredentialOption() 函式呼叫中的 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" : {
<encrypted_response>
}
}
請將這項回應傳回伺服器,驗證其真實性。請參閱「解密及驗證回應」。
網頁
如要在 Chrome 或其他支援的瀏覽器上,使用 Digital Credentials API 要求身分憑證,請提出下列要求:
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 的回應傳回伺服器,驗證憑證回應。
步驟 C:解密及驗證回應 (伺服器端)
錢包傳回加密的 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.
}
}
重建 SessionTranscript 並驗證 DeviceResponse
- 使用 Android 或網頁專屬的 Handover 結構,從 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 Handover
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 條驗證裝置回應。
這項驗證包含四項檢查:
- 檢查核發者憑證:從
issuerAuth擷取核發者的簽署憑證鏈結,並根據信任的 IACA 根憑證進行驗證。請參閱「支援的核發機構和 IACA 憑證」(使用沙箱測試 ID 測試時,請新增 Utopia 沙箱憑證)。 - 驗證 MSO 簽章 (ISO/IEC 18013-5 第 9.1.2 節)。
- 計算並檢查資料元素的
ValueDigests(ISO/IEC 18013-5 第 9.1.2 節)。 - 驗證
deviceSignature簽章 (ISO/IEC 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
}
步驟 D (選用):隱私權保護年齡驗證 (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 包含可立即部署、以 Docker 為基礎的伺服器,可讓您根據核發者 IACA 憑證驗證回應。
您可以修改 certs.pem,管理要信任的 IACA 核發者憑證。
SDK、參考實作和支援
使用我們的開放原始碼程式庫、參考伺服器和網頁驗證工具,加快導入速度:
| 資源 / 程式庫 | 這項工具的作用 | 連結 |
|---|---|---|
| 身分驗證工具參考實作 | 伺服器和用戶端範例,示範如何產生 OpenID4VP 要求、簽署 JWS、解密 JWE,以及驗證 mdoc 回應。 | GitHub 存放區 |
| Multipaz SDK | 開放原始碼程式庫 (OpenWallet Foundation),包括用於 CBOR 解碼、SessionTranscript 重建和 ISO/IEC 18013-5/7 簽章驗證的 DeviceResponseParser。 |
openwallet-foundation/multipaz |
| Longfellow ZKP 程式庫和驗證器服務 | 開放原始碼程式庫和 Docker 伺服器,用於驗證零知識證明 (mso_mdoc_zk) 年齡和屬性斷言。 |
google/longfellow-zk |
| 互動式網站驗證工具 | 託管網頁驗證工具,可針對 Google 錢包測試 W3C Digital Credentials API / OpenID4VP 要求。 | verifier.multipaz.org |
| OpenID4VP 1.0 規格 | 可驗證簡報 1.0 通訊協定規格的官方 OpenID。 | OpenID4VP 規格 |
| 常見問題與開發人員支援 | 技術整合常見問題的解答,或傳送電子郵件至 wallet-identity-rp-support@google.com 尋求偵錯協助。 |
技術整合常見問題 |