Принятие цифровых удостоверений онлайн

В этом руководстве рассказывается, как проверяющие стороны могут технически интегрировать Digital Credentials API, чтобы запрашивать и проверять цифровые учетные данные в приложениях Android и интернете.

Процесс регистрации и требования

Прежде чем запускать приложение в рабочей среде, необходимо официально зарегистрировать его в Google. Мы используем процесс подписания сертификата, в котором Google подписывает ваш сертификат, чтобы установить доверие.

  1. Тестирование в песочнице. Вы можете начать разработку сразу же, не отправляя форму. Вы можете протестировать контент, используя предварительно доверенные тестовые ключи и примеры метаданных, опубликованные на странице Тестовый режим.
  2. Запишите видео с демонстрацией сквозного процесса. После того как вы завершите тестирование и убедитесь, что интеграция работает в песочнице, запишите видео, на котором будет показан весь процесс интеграции.
  3. Отправьте форму регистрации и примите Условия использования. Заполните и отправьте форму регистрации Relying Party.

В форме необходимо указать следующую информацию:

  • Запрос на подпись сертификата для рабочей версии.
  • Объекты для медийной рекламы: URL логотипа, отображаемое название, URL политики конфиденциальности и URL условий использования.
  • Видео, на котором показана интеграция в изолированной среде.

После одобрения Google предоставит вам подписанный сертификат и уникальные метаданные, закодированные в формате Base64URL (gw_rp_metadata_bytes).

Технические сведения об интеграции

В следующих разделах приведена техническая информация об интеграции проверяющих сторон непосредственно с 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

При форматировании запроса учетных данных необходимо включить поле gw_rp_metadata_bytes в объект client_metadata (как показано в приведенном ниже примере кода запроса). Это поле содержит метаданные проверяющей стороны, закодированные в формате Base64URL. Они необходимы Google Кошельку, чтобы подтвердить вашу личность и показать пользователю ваш бренд.

Параметр credential_request в requestJson содержит следующие поля:

Определенные учетные данные

{
  "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": {
          // Use org.iso.18013.5.1.mDL for mDL,
          // com.google.wallet.idcard.1 for ID pass, or
          // org.iso.23220.photoid.1 for ID pass (using photo ID document type)
          "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
          }
        ]
      }
    ]
  },
  "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>"
  }
}

Любые подходящие учетные данные

Ниже приведен пример запроса для ЦВУ, цифрового удостоверения и цифрового удостоверения (с использованием типа документа "удостоверение личности с фотографией"). Пользователь может выбрать любой из них.

{
  "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 pass
        "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 type 3: ID pass (using photo ID document type)
        "id": "photo_id-request",
        "format": "mso_mdoc",
        "meta": {
          "doctype_value": "org.iso.23220.photoid.1"
        },
        "claims": [
          {
            "path": [
              "org.iso.23220.1",
              "family_name"
            ],
            "intent_to_retain": false // set this to true if you are saving the value of the field
          },
          {
            "path": [
              "org.iso.23220.1",
              "given_name"
            ],
            "intent_to_retain": false
          },
          {
            "path": [
              "org.iso.23220.1",
              "age_over_18"
            ],
            "intent_to_retain": false
          }
        ]
      }
    ]
    credential_sets : [
      {
        "options": [
          [ "mdl-request" ],
          [ "id_pass-request" ],
          [ "photo_id-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) позволяют поместить запрос на проверяемое представление в криптографически подписанный веб-токен JSON (JWT) с использованием инфраструктуры открытых ключей (PKI). Это обеспечивает целостность запроса и подтверждает вашу личность для 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 проекта обновите зависимости, чтобы использовать Менеджер учетных данных (бета-версию):

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

Как настроить менеджер учетных данных

Чтобы настроить и инициализировать объект 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 из раздела Формат запроса в качестве request в вызове функции 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)
    }
}

Как обрабатывать ответ с учетными данными

Когда вы получите ответ от кошелька, проверьте, успешно ли выполнен запрос и содержит ли ответ 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 содержит зашифрованный identityToken (JWT), определенный W3C. Ответ формируется в приложении "Кошелек".

Пример:

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

Этот ответ нужно передать обратно на сервер, чтобы подтвердить его подлинность. Как проверить ответ на запрос учетных данных

Веб

Чтобы запросить учетные данные с помощью Digital Credentials API в Chrome или другом поддерживаемом браузере, отправьте следующий запрос.

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 обратно на свой сервер, чтобы проверить credentialResponse.

Проверка ответа

После того как кошелек вернет зашифрованный токен 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 приведет к созданию JSON-файла vp_token, содержащего учетные данные.

  {
    "vp_token":
    {
      "cred1": ["<base64UrlNoPadding_encoded_credential>"] // This applies to OpenID4VP 1.0 spec.
    }
  }
  1. Как создать расшифровку сеанса

    На следующем шаге нужно создать объект SessionTranscript из стандарта ISO/IEC 18013-5:2021 со структурой передачи, предназначенной для Android или интернета:

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

    При передаче данных как в Android, так и в веб-приложениях необходимо использовать тот же однократно используемый номер, который вы использовали для создания 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 ответ устройства должен быть проверен в соответствии с пунктом 9 стандарта ISO/IEC 18013-5:2021.

    Проверка включает несколько этапов:

  2. Проверка сертификата издателя. Извлеките цепочку сертификатов подписи издателя из issuerAuth и проверьте ее на соответствие доверенным корневым сертификатам IACA. Ознакомьтесь со списком поддерживаемых сертификатов IACA эмитента.

  3. Проверка подписи MSO (18013-5, раздел 9.1.2)

  4. Рассчитывать и проверять ValueDigests для элементов данных (раздел 9.1.2 стандарта ISO 18013-5)

  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.

Общие сведения о доказательстве с нулевым разглашением и его возможностях можно найти в разделе Часто задаваемые вопросы.

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

Вы получите от кошелька зашифрованное доказательство с нулевым разглашением. Вы можете проверить это доказательство с помощью сертификатов IACA, используя библиотеку longfellow-zk от Google.

verifier-service содержит готовый к развертыванию сервер на основе Docker, который позволяет проверять ответ на соответствие определенным сертификатам IACA эмитента.

Вы можете изменить файл certs.pem, чтобы управлять сертификатами издателей IACA, которым хотите доверять.

Ресурсы и поддержка