概览
本部分概述了验证方注册机构加入 Google 钱包身份服务的逐步流程。
作为验证机构注册商(例如,代表其他实体进行验证的 IDV 公司),您充当自己的证书授权机构 (CA),为所管理的下游最终信赖方 (RP) 签署身份请求。
引导流程
第 1 步:提交初始信息收集表单、根证书并接受服务条款
填写并提交 Verifier Registrar Onboarding Intake Form。在此表单中,您将提供沙盒和生产环境根证书。提交此初始配置信息收集表单,即表示您正式接受 Google 钱包验证器注册机构服务条款。
第 2 步:沙盒信任和测试
您提交初始信息收集表单后,Google 会将您的沙盒根证书添加到 Google 钱包沙盒信任存储区,并通知您。然后,您就可以开始在沙盒中使用由沙盒根证书签名的证书来测试集成。
第 3 步:录制端到端演示视频
完成沙盒测试后,请录制初始(第 1 个)信赖方的验证流程的端到端视频演示,并将其提交给 Google。
- 视频要求:
- 根据需要,录制验证方托管(自行托管)和商家托管(RP 托管)流程的演示视频。
- 在视频中使用实际的商家展示素材资源(名称、徽标、服务条款网址)和汇总平台展示素材资源。
- 清晰展示启动验证流程的界面和屏幕。
第 4 步:审批和生产根信任
收到您的端到端视频演示后,Google 会触发视频审核和审批流程,同时开始生产根证书信任流程。这两个流程都完成并获得批准后,您就可以开始为下游最终 RP 推出服务了。
第 5 步:持续进行最终 RP 初始配置
对于您签署的每份 End RP,您都必须:
- 告知 Google:使用验证方注册机构客户导入表单告知 Google 新 RP 及其预期使用情形。
- 配置元数据:填充 RP 的显示信息(名称、徽标、隐私权政策网址),并在其证书中设置全局唯一的 Distinguished Name(主题)。
技术规范
A. 证书配置文件
请求必须使用通过 P-256 / ECDSA 生成的标准 X.509 v3 证书进行签名,并且包含自定义 Google 扩展:
- 自定义扩展 OID:
1.3.6.1.4.1.11129.10.1 - 严重程度:非严重。
- 内容:
RelyingPartyMetadataBytes的 SHA256 哈希值,以 ASN.1OCTET 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_uri 必须遵循 Google 钱包品牌推广指南。
C. OpenID4VP 集成
在设置签名 OpenID4VP 凭据请求的格式时,请将经过 base64url 编码的元数据包含在 client_metadata 对象内的 gw_rp_metadata_bytes 字段中(如下一部分中的示例请求代码所示)。
合规性和撤消
- 滥用行为监控:Google 会监控是否存在恶意 RP 活动,并会在检测到任何滥用行为时通知您。
- 及时撤消:您必须及时撤消滥用 RP 的证书,并发布更新的证书吊销列表 (CRL)。
- 审核:Google 会维护匿名化日志,以确保 RP 请求与其注册的用例相符。
后续步骤
如需开始以验证者注册机构的身份完成初始配置,请填写并提交验证者注册机构初始配置登记表单。如需为后续下游客户完成新手入门,请使用验证方注册机构客户新手入门表单。
如需查看有关初始配置和集成的常见问题解答,请参阅数字身份和凭据常见问题解答。
验证方注册商集成详情
以下部分介绍了与数字凭据 API 集成的验证方注册机构的技术集成详细信息(包括请求格式设置、请求加密、触发 API、验证响应和实现零知识证明)。
支持的格式和功能
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 包含每个请求的加密公钥。
您需要存储每个请求的私钥,并使用这些私钥对从钱包应用收到的令牌进行身份验证和授权。
集成 OpenID4VP 元数据
在设置凭据请求的格式时,您必须在 client_metadata 对象内添加 gw_rp_metadata_bytes 字段(如下面的示例请求代码所示)。此字段包含 Google 钱包所需的 Base64网址 编码的信赖方元数据,用于验证您的身份并向用户显示您的品牌信息。
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
],
"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": "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 钱包中存储的任何身份凭据请求任意数量的受支持属性。
已签名的请求
已签名的请求(JWT 安全授权请求)使用您的 PKI 基础架构将可验证的演示请求封装在经过加密签名的 JSON Web 令牌 (JWT) 中,从而确保请求完整性并向 Google 钱包证明您的身份。
前提条件
在为已签名的请求实现代码更改之前,请确保您已:
- 私钥:您需要一个私钥(例如,椭圆曲线
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 应用请求身份凭据,请按以下步骤操作:
更新依赖项
在项目的 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 条验证设备响应。此验证包括以下几个步骤:
检查签发者证书:从
issuerAuth中提取签发者的签名证书链,并根据受信任的 IACA 根证书对其进行验证。请参阅支持的发卡机构的 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 配置。
如需大致了解什么是 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 证书验证此证明。
验证器服务包含一个可随时部署的基于 Docker 的服务器,可让您根据某些颁发者 IACA 证书验证响应。
您可以修改 certs.pem,以管理您想要信任的 IACA 签发者证书。
资源与支持
- 常见问题解答:如需查看有关技术集成的常见问题解答,请参阅数字身份和凭据常见问题解答。
- 参考实现:请在 GitHub 上查看我们的身份验证器参考实现。
- 测试网站:请访问 verifier.multipaz.org 尝试端到端流程。
- OpenID4VP 规范:查看 openID4VP 的技术规范。
- 支持:如果您在集成过程中需要调试方面的帮助或有任何疑问,请与
wallet-identity-rp-support@google.com联系。