在线接受数字凭据

数字身份证件可在应用内网页流程中接受。 如需接受 Google 钱包中的凭证,您需要:

  1. 按照提供的说明,使用应用或网站进行集成。
  2. 使用测试 ID 在 Google 钱包沙盒中测试您的流程。
  3. 准备好正式发布后,请填写此表单,申请并同意接受 Google 钱包凭据的服务条款。

前提条件

如需测试数字身份证件的显示,您必须先使用预期测试账号(必须是 Gmail 账号)加入公开 Beta 版计划。随后,请向您的指定 Google 联系人提供以下详细信息。

  • 服务条款链接
  • 徽标
  • 网站
  • 应用软件包 ID(适用于 Android 应用集成)
    • 包括开发 / 调试 build
  • 应用签名
    • $ $ANDROID_SDK/build-tools/$BUILD_TOOLS_VERSION/apksigner verify --print-certs -v $APK
  • 用于加入公开 Beta 版的 Gmail ID

支持的凭据格式

目前有多种提议的标准定义了数字身份文档的数据格式,其中两种标准在行业内获得了广泛认可:

  1. mdocs - 由 ISO 定义。
  2. W3C 可验证凭据 - 由 W3C 定义。

虽然 Android Credential Manager 支持这两种格式,但 Google 钱包目前仅支持基于 mdoc 的数字身份证件。

支持的凭据

Google 钱包支持 2 种凭据类型:

  1. 数字驾照 (mDL)
  2. 身份证件

您只需更改一个参数,即可在流程中请求任一凭据。

用户体验

本部分将介绍建议的线上演示流程。此流程显示了向酒精饮料配送应用提供年龄信息的流程,但对于网站和其他类型的演示,用户体验也类似。

系统提示用户在应用或网站中验证年龄 用户看到可用的符合条件的凭据 用户在 Google 钱包中看到确认页面 用户进行身份验证以确认共享 发送到应用或网站的数据
系统提示用户在应用或网站中验证年龄 用户看到可用的符合条件的凭据 用户在 Google 钱包中看到确认页面 用户进行身份验证以确认共享 发送到应用或网站的数据

重要说明

  1. 应用或网站可以灵活地创建 API 的入口点。如第 1 步所示,我们建议显示“使用数字身份证件验证”等通用按钮,因为我们预计随着时间的推移,除了 Google 钱包之外,还会有更多选项通过该 API 提供。
  2. 步骤 2 中的选择器界面由 Android 呈现。符合条件的凭据由每个钱包提供的注册逻辑与信赖方发送的请求之间的匹配情况决定
  3. 第 3 步由 Google 钱包呈现。Google 钱包将在此界面上显示开发者提供的名称、徽标和隐私权政策。

添加数字身份证件流程

如果用户没有凭据,我们建议在“使用数字身份证件进行验证”按钮旁边提供一个链接,该链接将深层链接到 Google 钱包,以便用户添加数字身份证件。

系统提示用户在应用或网站中验证年龄 用户前往 Google 钱包获取数字身份证件
系统提示用户在应用或网站中验证年龄 用户前往 Google 钱包获取数字身份证件

没有可用的数字版身份证件

如果用户在没有数字身份证件的情况下选择“使用数字身份证件进行验证”选项,系统会显示此错误消息。

系统提示用户在应用或网站中验证年龄 如果用户没有数字身份证件,系统会向其显示错误消息
系统提示用户在应用或网站中验证年龄 如果用户没有数字身份证件,系统会向其显示错误消息

该 API 不支持以静默方式了解用户是否有任何可用的数字 ID,以保护用户隐私。因此,我们建议您添加注册链接选项,如图所示。

从钱包请求身份证件凭据的请求格式

以下示例展示了如何通过 mdoc requestJson 请求从 Android 设备或网络上的任何钱包获取身份凭据。

{
      "requests" : [
        {
          "protocol": "openid4vp",
          "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",
  "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"
            ]
          },
          {
            "path": [
              "org.iso.18013.5.1",
              "given_name"
            ]
          },
          {
            "path": [
              "org.iso.18013.5.1",
              "age_over_18"
            ]
          }
        ]
      }
    ]
  },
  "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",
          "alg" : "ECDH-ES",
        }
      ]
    },
    "authorization_encrypted_response_alg": "ECDH-ES",
    "authorization_encrypted_response_enc": "A128GCM"
  }
}

您可以从 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",
  "data" : {
    <encrpted_response>
  }
}

您需要将此响应传递回服务器,以验证其真实性。 您可以查看验证凭据响应的步骤

Web

如需在 Chrome 上使用 Digital Credentials API 请求身份凭据,您需要注册 Digital Credentials API 源试用

const credentialResponse = await navigator.credentials.get({
          digital : {
          requests : [
            {
              protocol: "openid4vp",
              data: {<credential_request>} // This is an object, shouldn't be a string.
            }
          ]
        }
      })

将此 API 的响应发送回您的服务器,以验证凭据响应

验证凭据响应的步骤

从应用或网站收到加密的 identityToken 后,您需要执行多项验证,然后才能信任响应。

  1. 使用私钥解密响应

    第一步是使用保存的私钥解密令牌,并获取响应 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)
    
    # 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": "<credential_token>"
      }
    }
    
  2. 创建会话转录内容

    下一步是根据 ISO/IEC 18013-5:2021 创建 SessionTranscript,其中包含 Android 或 Web 特定的切换结构:

    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>",
          clientId,           // "android-origin:<app_package_name>",
          nonce,              // nonce that was used to generate credential request
        ]
    
        AndroidHandoverDataBytes = hashlib.sha256(cbor2.dumps(AndroidHandoverData)).digest()
        

    浏览器切换

        BrowserHandoverData =[
          origin,               // Origin URL
          clientId,             // "web-origin:<origin>"
          nonce,               //  nonce that was used to generate credential request
        ]
    
        BrowserHandoverDataBytes = hashlib.sha256(cbor2.dumps(BrowserHandoverData)).digest()
        

    使用 SessionTranscript 时,必须根据 ISO/IEC 18013-5:2021 第 9 条验证 DeviceResponse。这包括多个步骤,例如:

  3. 检查州颁发机构证书。查看受支持的发卡机构的 IACA 证书

  4. 验证 MSO 签名 (18013-5 第 9.1.2 节)

  5. 计算并检查数据元素的值摘要(18013-5 第 9.1.2 节)

  6. 验证 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": "2093f64f54c81fb2f7f96a46593951d04005784da3d479e4543e2190dcf205d6", //This will differ if you need more than 1 attribute.
            "num_attributes": 1, // number of attributes (in claims) this has can support
            "version": 2
        },
        {
            "system": "longfellow-libzk-v1",
            "circuit_hash": "2836f0df5b7c2c431be21411831f8b3d2b7694b025a9d56a25086276161f7a93", // This will differ if you need more than 1 attribute.
            "num_attributes": 1, // number of attributes (in claims) this has can support
            "version": 1
        }
       ],
       "verifier_message": "challenge"
      },
     "claims": [{
         ...