在线接受数字凭据

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

  1. 按照提供的说明,使用应用或网站进行集成。
  2. 使用测试 ID 通过 Google 钱包沙盒测试您的流程。
  3. 如需启用直播功能,请填写此表单以申请访问权限,并接受 Google Wallet 凭据服务条款。您需要为每个企业实体填写此表单。您填写完表单后,我们的团队会与您联系。
  4. 如果您有任何疑问,可以与wallet-identity-rp-support@google.com联系。

支持的凭据格式

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

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

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

支持的凭据

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

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

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

用户体验

本部分介绍了建议的线上演示流程。 此流程展示了向酒精饮料配送应用呈现年龄的流程,但对于网站和其他类型的呈现方式,用户体验类似。

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

重要说明

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

添加数字身份证件流程

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

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

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

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

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

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

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

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

{
      "requests" : [
        {
          "protocol": "openid4vp-v1-unsigned", // openid4vp-v1-signed for signed request.
          "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 和 ID 卡的示例请求。用户可以继续使用其中任一选项。

{
  "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 安全授权请求或 JAR)。如需了解更多详情,请参阅 openID4VP 文档

与以原始 JSON 格式发送参数的未签名请求不同,签名请求会将您的可验证演示请求封装在经过加密签名的 JSON Web 令牌 (JWT) 中。这有两大优势:

  1. 完整性:确保请求未被篡改。
  2. 替代身份验证机制 (x509 PKI):向 Google 钱包证明请求来自拥有已向 Google 钱包注册的特定公钥的验证方。

前提条件

在为已签名的请求实现代码更改之前,请确保您已:

  • 私钥:您需要一个私钥(例如,椭圆曲线 ES256 或 RSA)来对服务器中管理的请求进行签名。
  • 证书:您需要从密钥对派生的标准 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
            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

应用内

如需从 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 中提供。凭据管理器会将此 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 后,您需要执行多项验证,然后才能信任响应。

  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)
    # 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.
      }
    }
    
  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>",
          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。这包括多个步骤,例如:

  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
}

构建解决方案

如需构建解决方案,您可以参阅 GitHub 上的身份验证器参考实现

基于零知识证明 (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": "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 签发者证书

如需了解详情,您可以发送电子邮件与支持团队联系: wallet-identity-rp-support@google.com