線上接受數位憑證

數位 ID 可用於應用程式內網站流程。如要接受 Google 錢包的憑證,您必須:

  1. 按照提供的指示,使用應用程式或網頁進行整合,
  2. 請填寫這份表單,申請並同意接受 Google 錢包憑證的服務條款。

必要條件

如要測試數位身分證件,您必須先使用指定的測試帳戶 (必須是 Gmail 帳戶) 註冊公開 Beta 版計畫。接著,請將後續詳細資料提供給指定的 Google 聯絡窗口。

  • 服務條款連結
  • 標誌
  • 網站
  • 應用程式套件 ID (適用於 Android 應用程式整合)
    • 包含開發 / 偵錯版本
  • 應用程式簽名
    • $ $ANDROID_SDK/build-tools/$BUILD_TOOLS_VERSION/apksigner verify --print-certs -v $APK
  • 用於加入公開測試版的 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 算繪。系統會根據每個 Wallet 提供的註冊邏輯與信賴方傳送的要求進行比對,判斷是否符合資格的憑證
  3. 步驟 3 是由 Google 錢包算繪。Google 錢包會在這個畫面上顯示開發人員提供的名稱、標誌和隱私權政策。

新增數位身分證件流程

如果使用者沒有憑證,建議您在「使用數位身分證件驗證」按鈕旁邊提供連結,讓使用者可透過 Google 錢包深層連結新增數位身分證件。

系統提示使用者在應用程式或網站中驗證年齡 使用者前往 Google 錢包取得數位身分證件
系統提示使用者在應用程式或網站中驗證年齡 使用者前往 Google 錢包取得數位身分證件

沒有可用的數位身分證件

如果使用者選取「使用數位身分證件驗證」選項,但沒有數位身分證件,系統會顯示這則錯誤訊息。

系統提示使用者在應用程式或網站中驗證年齡 使用者沒有數位身分證件時,系統會顯示錯誤訊息
系統提示使用者在應用程式或網站中驗證年齡 使用者沒有數位身分證件時,系統會顯示錯誤訊息

為了保護使用者隱私,這個 API 不支援可讓您靜默瞭解使用者是否有任何可用的數位 ID 的功能。因此,建議您加入如上所示的 onboarding 連結選項。

從錢包要求身分證件的要求格式

以下是 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)。錢包應用程式會負責建立此回應。

範例:

"response" : {
  <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. 建立工作階段轉錄稿

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

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

    對於 Android / 網頁轉移,您必須使用產生 credential_request 時使用的同一個 Nonce。

    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. 計算及檢查資料元素的 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,並將 zk_system_type 新增至「Request」

  ...
  "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": "2b8e0c49b08eb1801b9bd7a82aa9eb3736a7519fc2b409asdhj1237034", // This will differ if you need more than 1 attribute.
           "num_attributes": 1,
           "version": 1
         }
       ],
       "verifier_message": "challenge"
      },
     "claims": [{
         ...