驗證機構註冊機構新手上路指南

總覽

本節將逐步說明驗證者註冊機構如何加入 Google 錢包身分識別服務。

驗證機構註冊商 (例如,代表其他實體驗證的 IDV 公司) 會擔任自己的憑證授權單位 (CA),為您管理的下游 End Relying Parties (RP) 簽署身分要求。

新手上路流程

步驟 1:提交申請表單、根憑證及接受服務條款

填寫並提交驗證者註冊機構上線登記表單。在這份表單中,您將提供沙箱和正式環境的根憑證。提交這份新手上路表單,即表示您正式接受《Google 錢包驗證者註冊機構服務條款》。

步驟 2:沙箱信任和測試

提交申請表後,Google 會將沙箱根憑證新增至 Google 錢包沙箱信任儲存庫,並通知您。接著,您可以使用沙箱根目錄簽署的憑證,在沙箱中開始測試整合。

步驟 3:錄製端對端示範影片

完成沙箱測試後,請錄製初始 (第 1 個) Relying Party 的驗證流程端對端影片,並提交給 Google。

  • 影片規定:
    • 視情況錄製驗證方代管 (自行代管)商家代管 (RP 代管) 流程的示範影片。
    • 在影片中使用實際的商家顯示素材資源 (名稱、標誌、服務條款網址) 和匯總器顯示素材資源。
    • 清楚展示啟動驗證流程的使用者介面和畫面。

步驟 4:核准及生產根信任

收到端對端影片示範後,Google 會啟動影片審查和核准程序,同時開始製作根憑證信任程序。這兩項程序都完成並獲得核准後,您就能開始為下游的 End RP 推出服務。

步驟 5:持續完成 RP 前置作業

您必須為簽署的每個 End RP 執行下列操作:

  • 通知 Google:使用驗證者註冊機構用戶端入門表單,向 Google 告知新的 RP 及其預期用途。
  • 設定中繼資料:填入 RP 的顯示資訊 (名稱、標誌、隱私權政策網址),並在憑證中設定全域不重複的識別名稱 (主體)

技術規格

A. 憑證設定檔

要求必須由使用 P-256 / ECDSA 產生的標準 X.509 v3 憑證簽署,且包含自訂 Google 擴充功能:

  • 自訂擴充功能 OID: 1.3.6.1.4.1.11129.10.1
  • 重要性:非重要。
  • 內容:RelyingPartyMetadataBytes 的 SHA256 雜湊,以 ASN.1 OCTET 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 認證要求時,請在 client_metadata 物件內的 gw_rp_metadata_bytes 欄位中加入 base64url 編碼的中繼資料 (如下一節的範例要求程式碼所示)。

合規和撤銷

  • 濫用監控:Google 會監控惡意 RP 活動,並在偵測到任何濫用行為時通知您。
  • 立即撤銷:您必須立即撤銷濫用 RP 的憑證,並發布更新的憑證撤銷清單 (CRL)。
  • 稽核:Google 會維護匿名記錄,確保 RP 要求符合註冊用途。

後續步驟

如要開始以驗證機構註冊機構的身分加入,請填寫並提交驗證機構註冊機構上線申請表。如要加入後續下游用戶端,請使用驗證者註冊用戶端入門表單

如要查看有關新手上路和整合的常見問題,請參閱「數位身分和憑證常見問題」。

驗證者註冊商整合詳細資料

下一節將說明驗證者註冊機構整合 Digital Credentials API 的技術細節,包括要求格式、要求加密、觸發 API、驗證回應,以及實作零知識證明。

支援的格式和功能

Google 錢包支援以 ISO mdoc 為基礎的數位身分證件。

格式化要求

如要向任何錢包要求憑證,必須使用 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 錢包驗證身分和向使用者顯示品牌資訊時所需的 Base64URL 編碼信賴方中繼資料。

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

要求身分屬性

應用程式會將身分要求的所有參數一併提供為 CredentialOption 中的 JSON 字串,而非個別指定。Credential Manager 會將這個 JSON 字串傳遞給可用的數位錢包,不會檢查其內容。每個錢包隨後會負責: - 剖析 JSON 字串,瞭解身分識別要求。 - 判斷儲存的憑證是否符合要求。

即使是 Android 應用程式整合,我們也建議合作夥伴在伺服器上建立請求。

您會在 GetDigitalCredentialOption() 函式呼叫中,使用「要求格式」requestJson 做為 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)。Google 錢包應用程式會負責製作這則回覆。

範例:

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

您會將這項回應傳回伺服器,驗證其真實性。您可以參閱這篇文章,瞭解如何驗證憑證回應

網頁

如要在 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.
    }
  }
  1. 建立工作階段轉錄稿

    下一個步驟是從 ISO/IEC 18013-5:2021 建立 SessionTranscript,並使用 Android 或網頁專屬的交接結構:

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

    無論是 Android 或網頁交接,您都必須使用產生 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()
        

    瀏覽器交接

        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 條款驗證裝置回應。

    這項驗證包含下列步驟:

  2. 檢查簽發者憑證:issuerAuth 擷取簽發者的簽署憑證鏈結,並根據受信任的 IACA 根憑證驗證該鏈結。請參閱支援的發卡機構 IACA 憑證

  3. 驗證 MSO 簽章 (18013-5 第 9.1.2 節)

  4. 計算及檢查ValueDigests資料元素 (18013-5 第 9.1.2 節)

  5. 驗證 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 簽發者憑證

資源與支援