本指南介绍了信赖方 (RP) 如何从技术上集成 Digital Credentials API,以在 Android 应用和 Web 中从 Google 钱包请求和验证移动驾驶执照 (mDL) 和身份证件。
注册流程和前提条件
在正式发布到生产环境之前,您必须向 Google 正式注册您的信赖方应用。
- 在沙盒中进行测试:您可以立即开始使用我们的沙盒环境和创建测试 ID 进行开发。 测试不需要接受服务条款。
- 提交登记表:填写 RP 新用户引导表单。新商家加入流程通常需要 3-5 个工作日。您的产品名称和徽标将显示在面向用户的权限请求页面上,以帮助用户识别是谁在请求其数据。
- 接受《服务条款》:您必须先签署《服务条款》,然后才能开始直播。
支持的格式和功能
Google 钱包支持基于 ISO mdoc 的数字身份证件。
- 支持的凭据:您可以查看支持的凭据和属性。
- 支持的协议: OpenID4VP(版本 1.0)。
- 最低 Android SDK:Android 9(API 级别 28)及更高版本。
- 浏览器支持:如需查看支持数字凭证 API 的浏览器的完整列表,请参阅生态系统支持页面。
设置请求格式
如需从任何钱包请求凭据,您必须使用 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 包含每个请求的加密公钥。您需要存储每个请求的私钥,并使用这些私钥对从钱包应用收到的令牌进行身份验证和授权。
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 和身份证件的请求示例。用户可以继续使用其中任一账号。
{
"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 钱包中存储的任何身份凭据请求任意数量的受支持属性。
已签名的请求
已签名的请求(JWT 安全授权请求)使用您的公钥基础设施 (PKI) 将可验证的演示请求封装在经过加密签名的 JSON Web 令牌 (JWT) 中,从而确保请求完整性并向 Google 钱包证明您的身份。
前提条件
在为已签名的请求实现代码更改之前,请确保您已:
- 私钥:您需要使用私钥(例如,椭圆曲线
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 应用请求身份凭据,请按以下步骤操作:
更新依赖项
在项目的 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" : {
<encrpted_response>
}
}
您需要将此响应传递回服务器,以验证其真实性。 您可以找到验证凭据响应的步骤
Web
如需在 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 的响应发送回您的服务器,以验证凭据响应
验证回答
钱包返回加密的 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 或 Web 特定的切换结构,根据 ISO/IEC 18013-5:2021 创建 SessionTranscript:
SessionTranscript = [ null, // DeviceEngagementBytes not available null, // EReaderKeyBytes not available [ "OpenID4VPDCAPIHandover", AndroidHandoverDataBytes // BrowserHandoverDataBytes for Web ] ]对于 Android 和 Web 切换,您都需要使用用于生成
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 子句验证设备响应。这包括多个步骤,例如:检查州签发者证书。请参阅支持的发卡机构的 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 证书验证此证明。
验证器服务包含一个基于 Docker 的部署就绪型服务器,可让您根据某些颁发者 IACA 证书验证响应。
您可以修改 certs.pem,以管理您想要信任的 IACA 签发者证书。
资源与支持
- 参考实现:请在 GitHub 上查看我们的身份验证器参考实现。
- 测试网站:请访问 verifier.multipaz.org 尝试端到端流程。
- OpenID4VP 规范:查看 openID4VP 的技术规范。
- 支持:如果您在集成过程中需要调试方面的帮助或有任何疑问,请与
wallet-identity-rp-support@google.com联系。