应用内和网页流程均可接受数字身份证件。 如需接受 Google 钱包中的凭据,您需要:
- 按照提供的说明,使用应用或网站进行集成。
- 使用测试 ID 通过 Google 钱包沙盒测试您的流程。
- 如需启用直播功能,请填写此表单以申请访问权限,并接受 Google Wallet 凭据服务条款。您需要为每个企业实体填写此表单。您填写完表单后,我们的团队会与您联系。
- 如果您有任何疑问,可以与wallet-identity-rp-support@google.com联系。
支持的凭据格式
目前有多种提议的标准定义了数字身份文档的数据格式,其中两种标准在行业内获得了广泛认可:
虽然 Android Credential Manager 支持这两种格式,但 Google 钱包目前仅支持基于 mdoc 的数字身份证件。
支持的凭据
Google 钱包支持 2 种凭据类型:
- 数字驾照 (mDL)
- 身份证件
您只需更改一个参数,即可在流程中请求任一凭据。
用户体验
本部分将介绍建议的线上演示流程。此流程显示了向酒精饮料配送应用提供年龄信息的流程,但对于网站和其他类型的演示,用户体验也类似。
|  |  |  |  |  | 
| 系统提示用户在应用或网站中验证年龄 | 用户看到符合条件的可用凭证 | 用户在 Google 钱包中看到确认页面 | 用户进行身份验证以确认共享 | 发送到应用或网站的数据 | 
重要说明
- 应用或网站可以灵活地创建 API 的入口点。如第 1 步所示,我们建议显示“使用数字身份证件验证”等通用按钮,因为我们预计随着时间的推移,通过该 API 可使用的选项将不止 Google 钱包。
- 步骤 2 中的选择器界面由 Android 呈现。符合条件的凭据由每个钱包提供的注册逻辑与信赖方发送的请求之间的匹配情况决定
- 第 3 步由 Google 钱包呈现。Google 钱包将在此界面上显示开发者提供的名称、徽标和隐私权政策。
添加数字身份证件流程
如果用户没有凭据,我们建议在“使用数字身份证件进行验证”按钮旁边提供一个链接,该链接将深层链接到 Google 钱包,以便用户添加数字身份证件。
|  |  | 
| 系统提示用户在应用或网站中验证年龄 | 用户前往 Google 钱包获取数字身份证件 | 
没有可用的数字身份证件
如果用户选择“使用数字 ID 进行验证”选项,但没有数字 ID,系统会显示此错误消息。
|  |  | 
| 系统提示用户在应用或网站中验证年龄 | 如果用户没有数字身份证件,系统会向其显示错误消息 | 
该 API 不支持以静默方式了解用户是否有任何可用的数字 ID,以保护用户隐私。因此,我们建议您添加注册链接选项,如上所示。
从钱包请求身份证件凭据的请求格式
以下示例展示了如何通过 mdoc requestJson 请求从 Android 设备或网络上的任何钱包获取身份凭据。
{
      "requests" : [
        {
          "protocol": "openid4vp-v1-unsigned",
          "data": {<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 和 idpass,用户可以继续使用其中任一凭证。
{
  "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 钱包中存储的任何身份凭证请求任意数量的受支持属性。
应用内
如需从 Android 应用请求身份凭据,请按以下步骤操作:
更新依赖项
在项目的 build.gradle 中,更新您的依赖项以使用 Credential Manager(Beta 版):
dependencies {
    implementation("androidx.credentials:credentials:1.5.0-beta01")
    // optional - needed for credentials support from play services, for devices running Android 13 and below.
    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 中提供。凭据管理器会将此 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-unsigned",
  "data" : {
    <encrpted_response>
  }
}
您需要将此响应传递回服务器,以验证其真实性。 您可以查看验证凭据响应的步骤。
Web
如需在 Chrome 或其他受支持的浏览器上使用 Digital Credentials API 请求身份凭据,请触发以下请求。
const credentialResponse = await navigator.credentials.get({
          digital : {
          requests : [
            {
              protocol: "openid4vp-v1-unsigned",
              data: {<credential_request>} // This is an object, shouldn't be a string.
            }
          ]
        }
      })
将此 API 的响应发送回您的服务器,以验证凭据响应
验证凭据响应的步骤
从应用或网站收到加密的 identityToken 后,您需要执行多项验证,然后才能信任响应。
- 使用私钥解密响应 - 第一步是使用保存的私钥解密令牌,并获取响应 JSON。 - 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_tokenJSON- { "vp_token": { "cred1": "<credential_token>" } }
- 创建会话转录内容 - 下一步是使用 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 条验证 DeviceResponse。这包括多个步骤,例如:
- 检查州颁发机构证书。查看受支持的发卡机构的 IACA 证书 
- 验证 MSO 签名 (18013-5 第 9.1.2 节) 
- 计算并检查数据元素的值摘要(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
}
测试您的解决方案
如需测试您的解决方案,请构建并运行我们的开源参考持有者 Android 应用。 以下是构建和运行参考持有者应用的步骤:
- 克隆参考应用代码库
- 在 Android Studio 中打开项目
- 在 Android 设备或模拟器上构建并运行 appholder目标。
基于零知识证明 (ZKP) 的验证
零知识证明 (ZKP) 是一种加密方法,可让个人(证明者)向验证者证明自己拥有某种身份信息或符合特定条件(例如年满 18 周岁、持有有效凭据),而无需透露实际的底层数据本身。从本质上讲,这是一种在保护敏感细节隐私的同时确认有关个人身份的陈述是否属实的方法。
依赖于直接共享身份数据的数字身份系统通常要求用户共享过多的个人信息,从而增加了数据泄露和身份盗用的风险。零知识证明 (ZKP) 带来了范式转变,实现了以最少的披露信息进行验证。
数字身份中的 ZKP 的关键概念:
- 证明者:试图证明自己身份的某个人。
- 验证方:请求身份属性证明的实体。
- 证明:一种加密协议,可让证明者在不泄露机密信息的情况下,让验证者相信其声明的真实性。
零知识证明的核心属性:
- 完整性:如果陈述为真,并且证明者和验证者都是诚实的,则验证者会被说服。
- 可靠性:如果陈述为假,不诚实的证明者无法(以极高的概率)说服诚实的验证者相信该陈述为真。
- 零知识:验证者除了知道陈述为真之外,不会学到任何其他信息。不会泄露证明者的任何实际身份数据。
如需从 Google 钱包获取零知识证明,您需要将请求格式更改为 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": "bd3168ea0a9096b4f7b9b61d1c210dac1b7126a9ec40b8bc770d4d485efce4e9", // This will differ if you need more than 1 attribute.
          "num_attributes": 1, // number of attributes (in claims) this has can support
          "version": 3
        },
        {
          "system": "longfellow-libzk-v1",
          "circuit_hash": "89288b9aa69d2120d211618fcca8345deb4f85d2e710c220cc9c059bbee4c91f", // This will differ if you need more than 1 attribute.
          "num_attributes": 1, // number of attributes (in claims) this has can support
          "version": 4
        }
        {
          "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
        }
        {
          "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
        }
       ],
       "verifier_message": "challenge"
      },
     "claims": [{
         ...
      "client_metadata": {
        "jwks": {
          "keys": [ // sample request encryption key
            {
              ...
您将从钱包中获得加密的零知识证明。您可以使用 Google 的 longfellow-zk 库针对签发者的 IACA 证书验证此证明。
验证器服务包含一个可部署的基于 Docker 的服务器,可让您根据某些颁发者 IACA 证书验证响应。
您可以修改 certs.pem,以管理您要信任的 IACA 签发者证书。
如需了解详情,您可以发送电子邮件与支持团队联系
wallet-identity-rp-support@google.com
