線上接受數位憑證

數位身分證件可透過應用程式內網站流程接受。 如要接受 Google 錢包中的憑證,您必須:

  1. 按照提供的操作說明,透過應用程式或網站整合。
  2. 使用測試 ID 在 Google 錢包沙箱中測試流程。
  3. 準備好上線後,請填寫這份表單,要求並同意接受 Google 錢包憑證的服務條款。

必要條件

如要測試數位身分證件的顯示方式,請先使用預計的測試帳戶 (必須是 Gmail 帳戶) 註冊公開 Beta 版計畫。接著,請將下列詳細資料提供給指定的 Google 聯絡人。

  • 服務條款連結
  • 標誌
  • 網站
  • 應用程式套件 ID (適用於 Android 應用程式整合)
    • 包括開發 / 偵錯版本
  • 應用程式簽章
    • $ $ANDROID_SDK/build-tools/$BUILD_TOOLS_VERSION/apksigner verify --print-certs -v $APK
  • 用於加入公開 Beta 版的 Gmail ID

支援的憑證格式

目前有幾項提議的標準定義了數位身分證件的資料格式,其中兩項已在業界獲得廣泛採用:

  1. mdocs - 由 ISO 定義。
  2. w3c 可驗證憑證 - 由 w3c 定義。

Android 憑證管理工具支援這兩種格式,但 Google 錢包目前僅支援以 mdoc 為基礎的數位身分證件。

支援的憑證

Google 錢包支援 2 種憑證類型:

  1. 行動駕照 (mDL)
  2. 身分證件票證

您可以在流程中要求任一憑證,只要變更單一參數即可。

使用者體驗

本節將說明建議的線上簡報流程。流程顯示向酒類外送應用程式呈現年齡的畫面,但網頁和其他類型的呈現方式也類似。

系統提示使用者在應用程式或網站中驗證年齡 使用者會看到可用的合格憑證 使用者在 Google 錢包中看到確認頁面 使用者驗證以確認共用 傳送至應用程式或網站的資料
系統提示使用者在應用程式或網站中驗證年齡 使用者會看到可用的合格憑證 使用者在 Google 錢包中看到確認頁面 使用者驗證以確認共用 傳送至應用程式或網站的資料

重要附註

  1. 應用程式或網站可彈性建立 API 的進入點。如步驟 1 所示,我們建議顯示「使用數位身分證件驗證」等一般按鈕,因為我們預期 API 會提供 Google 錢包以外的選項。
  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)

要求身分屬性

應用程式不會為身分要求指定個別參數,而是將所有參數一併提供為 CredentialOption 中的 JSON 字串。憑證管理工具會將這個 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)。Google 錢包應用程式會負責製作這項回覆。

範例:

{
  "protocol" : "openid4vp",
  "data" : {
    <encrpted_response>
  }
}

您會將此回應傳回伺服器,驗證其真實性。您可以參閱驗證憑證回應的步驟

網頁

如要在 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 或網頁專屬的交接結構:

    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>",
          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. 計算及檢查資料元素的 ValueDigest (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 重要概念:

  • 驗證者:嘗試證明自己身分某個面向的個人。
  • 驗證者:要求身分屬性證明的實體。
  • 證明:一種加密通訊協定,可讓證明者向驗證者證明其聲明屬實,但不會揭露私密資訊。

零知識證明的核心屬性:

  • 完備性:如果陳述內容為真,且證明者和驗證者都誠實,驗證者就會相信。
  • 健全性:如果陳述內容為假,誠實的驗證者 (極有可能) 不會被不誠實的證明者說服,相信陳述內容為真。
  • 零知識:驗證者除了知道陳述內容為真,不會獲得任何其他資訊。不會洩漏驗證者身分的實際資料。

如要從 Google 錢包取得零知識證明,請將要求格式變更為 mso_mdoc_zk,並在 Request 中新增 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": [{
         ...