Chấp nhận thông tin xác thực kỹ thuật số trực tuyến

Hướng dẫn này giải thích cách Bên tin cậy (RP) có thể tích hợp về mặt kỹ thuật Digital Credentials API để yêu cầu và xác thực Giấy phép lái xe (mDL) và Thẻ nhận dạng trên thiết bị di động từ Google Wallet trên các ứng dụng Android và web.

Quy trình đăng ký và điều kiện tiên quyết

Trước khi phát hành công khai, bạn phải chính thức đăng ký ứng dụng Bên thứ ba đáng tin cậy với Google.

  1. Kiểm thử trong Hộp cát: Bạn có thể bắt đầu phát triển ngay lập tức bằng cách sử dụng Môi trường hộp cátTạo mã nhận dạng kiểm thử. Bạn không bắt buộc phải chấp nhận Điều khoản dịch vụ để kiểm thử.
  2. Gửi biểu mẫu nhập thông tin: Điền thông tin vào Biểu mẫu tham gia chương trình RP. Quá trình tham gia thường mất từ 3 đến 5 ngày làm việc. Tên và biểu trưng sản phẩm của bạn sẽ xuất hiện trên màn hình xin phép dành cho người dùng để giúp người dùng xác định bên đang yêu cầu dữ liệu của họ.
  3. Chấp nhận Điều khoản dịch vụ: Bạn phải ký Điều khoản dịch vụ trước khi phát sóng trực tiếp.

Định dạng và chức năng được hỗ trợ

Google Wallet hỗ trợ Giấy tờ tuỳ thân điện tử dựa trên mdoc theo tiêu chuẩn ISO.

Định dạng yêu cầu

Để yêu cầu thông tin đăng nhập từ bất kỳ ví nào, bạn phải định dạng yêu cầu bằng OpenID4VP. Bạn có thể yêu cầu thông tin đăng nhập cụ thể hoặc nhiều thông tin đăng nhập trong một đối tượng dcql_query duy nhất.

Ví dụ về yêu cầu JSON

Sau đây là mẫu yêu cầu mdoc requestJson để lấy thông tin nhận dạng từ bất kỳ ví nào trên thiết bị Android hoặc web.

{
      "requests" : [
        {
          "protocol": "openid4vp-v1-signed",
          "data": {<signed_credential_request>} // This is an object, shouldn't be a string.
        }
      ]
}

Yêu cầu mã hoá

client_metadata chứa khoá công khai mã hoá cho từng yêu cầu. Bạn sẽ cần lưu trữ khoá riêng tư cho từng yêu cầu và sử dụng các khoá này để xác thực và uỷ quyền mã thông báo mà bạn nhận được từ ứng dụng ví.

Tham số credential_request trong requestJson chứa các trường sau.

Thông tin xác thực cụ thể

{
  "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
        ]
      }
    }
  }
}

Mọi thông tin xác thực đủ điều kiện

Sau đây là yêu cầu mẫu cho cả mDL và thẻ giấy tờ tuỳ thân. Người dùng có thể tiếp tục với một trong hai lựa chọn.

{
  "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
        ]
      }
    }
  }
}

Bạn có thể yêu cầu bất kỳ số lượng thuộc tính được hỗ trợ nào từ mọi thông tin nhận dạng được lưu trữ trong Google Wallet.

Yêu cầu đã ký

Yêu cầu đã ký (Yêu cầu uỷ quyền bảo mật JWT) đóng gói yêu cầu trình bày có thể xác minh của bạn bên trong Mã thông báo web JSON (JWT) được ký bằng mật mã bằng cách sử dụng cơ sở hạ tầng PKI, đảm bảo tính toàn vẹn của yêu cầu và chứng minh danh tính của bạn với Google Wallet.

Điều kiện tiên quyết

Trước khi triển khai các thay đổi về mã cho yêu cầu đã ký, hãy đảm bảo rằng bạn đã:

  • Khoá riêng tư: Bạn cần có khoá riêng tư (ví dụ: Đường cong Elliptic ES256) để ký yêu cầu được quản lý trong máy chủ của bạn.
  • Chứng chỉ: Bạn cần có chứng chỉ X.509 tiêu chuẩn bắt nguồn từ cặp khoá của bạn.
  • Đăng ký: Đảm bảo rằng chứng chỉ công khai của bạn đã được đăng ký với Google Wallet. Hãy liên hệ với nhóm hỗ trợ của chúng tôi theo số wallet-identity-rp-support@google.com

Logic tạo yêu cầu

Để tạo một yêu cầu, bạn cần sử dụng khoá riêng tư và gói tải trọng trong 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

Kích hoạt API

Toàn bộ yêu cầu API phải được tạo ở phía máy chủ. Tuỳ thuộc vào nền tảng, bạn sẽ truyền JSON đã tạo vào các API gốc.

Trong ứng dụng (Android)

Để yêu cầu thông tin xác thực danh tính từ các ứng dụng Android, hãy làm theo các bước sau:

Cập nhật phần phụ thuộc

Trong build.gradle của dự án, hãy cập nhật các phần phụ thuộc để sử dụng Trình quản lý thông tin xác thực (bản thử nghiệm):

dependencies {
    implementation("androidx.credentials:credentials:1.5.0-beta01")
    implementation("androidx.credentials:credentials-play-services-auth:1.5.0-beta01")
}

Định cấu hình Trình quản lý thông tin xác thực

Để định cấu hình và khởi tạo một đối tượng CredentialManager, hãy thêm logic tương tự như sau:

// Use your app or activity context to instantiate a client instance of CredentialManager.
val credentialManager = CredentialManager.create(context)

Thuộc tính nhận dạng yêu cầu

Thay vì chỉ định các tham số riêng lẻ cho yêu cầu về danh tính, ứng dụng sẽ cung cấp tất cả các tham số đó cùng nhau dưới dạng một chuỗi JSON trong CredentialOption. Trình quản lý thông tin xác thực sẽ chuyển chuỗi JSON này đến các ví điện tử hiện có mà không cần kiểm tra nội dung của chuỗi. Sau đó, mỗi ví sẽ chịu trách nhiệm: – Phân tích cú pháp chuỗi JSON để hiểu yêu cầu về danh tính. – Xác định xem có thông tin đăng nhập nào đã lưu trữ đáp ứng yêu cầu hay không.

Đối tác nên tạo yêu cầu trên máy chủ ngay cả đối với các hoạt động tích hợp ứng dụng Android.

Bạn sẽ sử dụng requestJson từ Định dạng yêu cầu làm request trong lệnh gọi hàm GetDigitalCredentialOption().

// 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)
    }
}

Xử lý phản hồi về thông tin đăng nhập

Sau khi nhận được phản hồi từ ví, bạn sẽ xác minh xem phản hồi đó có thành công hay không và có chứa phản hồi credentialJson hay không.

// 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}")
    }
}

Phản hồi credentialJson chứa một identityToken (JWT) đã mã hoá, do W3C xác định. Ứng dụng Wallet chịu trách nhiệm tạo phản hồi này.

Ví dụ:

{
  "protocol" : "openid4vp-v1-signed",
  "data" : {
    <encrpted_response>
  }
}

Bạn sẽ chuyển phản hồi này trở lại máy chủ để xác thực tính xác thực của phản hồi. Bạn có thể xem các bước xác thực phản hồi thông tin xác thực

Web

Để yêu cầu Thông tin xác thực danh tính bằng Digital Credentials API trên Chrome hoặc các trình duyệt được hỗ trợ khác, hãy đưa ra yêu cầu sau.

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

Gửi phản hồi từ API này trở lại máy chủ của bạn để xác thực phản hồi về thông tin đăng nhập

Xác thực câu trả lời

Sau khi ví trả về identityToken (JWT) đã mã hoá, bạn phải thực hiện quy trình xác thực phía máy chủ nghiêm ngặt trước khi tin tưởng dữ liệu.

Giải mã câu trả lời

Sử dụng khoá riêng tư tương ứng với khoá công khai được gửi trong client_metadata của yêu cầu để giải mã JWE. Thao tác này sẽ tạo ra một vp_token.

Ví dụ về 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 sẽ tạo ra một JSON vp_token chứa thông tin đăng nhập

  {
    "vp_token":
    {
      "cred1": ["<base64UrlNoPadding_encoded_credential>"] // This applies to OpenID4VP 1.0 spec.
    }
  }
  1. Tạo bản chép lời của phiên

    Bước tiếp theo là tạo SessionTranscript từ ISO/IEC 18013-5:2021 bằng cấu trúc Bàn giao dành riêng cho Android hoặc Web:

    SessionTranscript = [
      null,                // DeviceEngagementBytes not available
      null,                // EReaderKeyBytes not available
      [
        "OpenID4VPDCAPIHandover",
        AndroidHandoverDataBytes   // BrowserHandoverDataBytes for Web
      ]
    ]
    

    Đối với cả hoạt động bàn giao trên Android và web, bạn sẽ cần sử dụng cùng một số chỉ dùng một lần mà bạn đã dùng để tạo credential_request.

    Android Handover

        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()
        

    Chuyển giao trình duyệt

        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()
        

    Khi sử dụng SessionTranscript, Phản hồi của thiết bị phải được xác thực theo điều khoản 9 của tiêu chuẩn ISO/IEC 18013-5:2021. Quá trình này bao gồm một số bước, chẳng hạn như:

  2. Kiểm tra Giấy chứng nhận của tổ chức phát hành tiểu bang. Tham khảo chứng chỉ IACA của tổ chức phát hành được hỗ trợ.

  3. Xác minh chữ ký MSO (18013-5 Phần 9.1.2)

  4. Tính toán và kiểm tra ValueDigests cho các Phần tử dữ liệu (18013-5 Phần 9.1.2)

  5. Xác minh chữ ký deviceSignature (18013-5 Phần 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
}

Xác minh độ tuổi đảm bảo quyền riêng tư (ZKP)

Để hỗ trợ Chứng minh không tiết lộ (ví dụ: xác minh rằng người dùng trên 18 tuổi mà không cần xem ngày sinh chính xác của họ), hãy thay đổi định dạng yêu cầu thành mso_mdoc_zk và cung cấp cấu hình zk_system_type bắt buộc.

  ...
  "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
            {
              ...

Bạn sẽ nhận được một bằng chứng không tiết lộ thông tin đã mã hoá từ ví. Bạn có thể xác thực bằng chứng này dựa trên các chứng chỉ IACA của tổ chức phát hành bằng cách sử dụng thư viện longfellow-zk của Google.

verifier-service chứa một máy chủ dựa trên Docker, sẵn sàng triển khai, cho phép bạn xác thực phản hồi dựa trên một số chứng chỉ IACA của tổ chức phát hành.

Bạn có thể sửa đổi certs.pem để quản lý các chứng chỉ của tổ chức phát hành IACA mà bạn muốn tin cậy.

Tài nguyên và dịch vụ hỗ trợ

  • Triển khai tham chiếu: Hãy xem Triển khai tham chiếu về trình xác minh danh tính của chúng tôi trên GitHub.
  • Trang web thử nghiệm: Thử quy trình từ đầu đến cuối tại verifier.multipaz.org.
  • Quy cách OpenID4VP: Xem quy cách kỹ thuật cho openID4VP.
  • Hỗ trợ: Để được trợ giúp gỡ lỗi hoặc giải đáp thắc mắc trong quá trình tích hợp, hãy liên hệ với wallet-identity-rp-support@google.com.