Руководство по вводу в должность регистратора-верификатора

Обзор

В этом разделе описан пошаговый процесс подключения регистраторов-верификаторов к сервису Google Wallet Identity.

В качестве регистратора-верификатора (например, компании, занимающейся проверкой данных от имени других организаций) вы выступаете в роли собственного центра сертификации (ЦС), подписывая запросы на идентификацию для конечных заинтересованных сторон (КП), которыми вы управляете.

Процесс адаптации

Шаг 1: Заполните регистрационную форму, предоставьте корневые сертификаты и примите условия использования.

Заполните и отправьте форму заявки на подключение к регистратору верификаторов . В этой форме вы должны указать корневые сертификаты для песочницы и производственной среды . Отправляя эту форму, вы также официально принимаете Условия предоставления услуг регистратора верификаторов Google Wallet .

Шаг 2: Доверие и тестирование в песочнице

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

Шаг 3: Запись видеодемонстрации E2E.

После завершения тестирования в тестовой среде запишите видеодемонстрацию процесса проверки для вашей первой (1-й) проверяющей стороны и отправьте их в Google.

  • Требования к видео:
    • При необходимости запишите демонстрационные сценарии как для потоков , размещенных верификатором (самостоятельно размещенных) , так и для потоков, размещенных продавцом (размещенных RP) .
    • Используйте в видеороликах реальные рекламные материалы продавцов (название, логотип, URL-адрес условий обслуживания) и рекламные материалы агрегаторов.
    • Чётко продемонстрируйте пользовательский интерфейс и экраны, запускающие процесс проверки.

Шаг 4: Утверждение и установление доверия к корневому компоненту в производственной среде.

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

Шаг 5: Продолжение процесса адаптации в рамках программы End RP.

Для каждого подписанного вами соглашения о завершении ролевой игры вы должны:

  • Уведомите Google: используйте форму регистрации клиента регистратора верификаторов , чтобы сообщить Google о новом регистраторе верификаторов и предполагаемом сценарии его использования.
  • Настройка метаданных: заполните отображаемую информацию о поставщике услуг (имя, логотип, URL-адрес политики конфиденциальности) и установите глобально уникальное отличительное имя (субъект) в его сертификате.

Технические характеристики

А. Профиль сертификата

Запросы должны быть подписаны стандартными сертификатами X.509 v3, сгенерированными с использованием P-256 / ECDSA и содержащими пользовательское расширение Google:

  • Пользовательский OID расширения: 1.3.6.1.4.1.11129.10.1
  • Критичность: Некритическая.
  • Содержимое: Хэш SHA256 объекта RelyingPartyMetadataBytes , закодированный в 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 Wallet по использованию фирменной символики .

Интеграция C. OpenID4VP

При форматировании подписанного запроса на учетные данные OpenID4VP включите закодированные в base64url метаданные в поле gw_rp_metadata_bytes внутри объекта client_metadata (как показано в примере кода запроса в следующем разделе).

Соблюдение требований и аннулирование

  • Мониторинг злоупотреблений: Google отслеживает вредоносную активность в RP-сетях и уведомит вас о любых обнаруженных нарушениях.
  • Оперативное аннулирование: Вы обязаны незамедлительно аннулировать сертификаты, используемые злоупотребляющими механизмами повторного использования, и опубликовать обновленный список аннулированных сертификатов (CRL).
  • Аудит: Google ведет анонимизированные журналы, чтобы гарантировать соответствие запросов RP зарегистрированным сценариям использования.

Следующие шаги

Для начала процесса адаптации в качестве регистратора-верификатора заполните и отправьте форму заявки на адаптацию регистратора-верификатора . Для адаптации последующих клиентов используйте форму заявки на адаптацию регистратора-верификатора .

Часто задаваемые вопросы о процессе регистрации и интеграции см. в разделе «Часто задаваемые вопросы о цифровой идентификации и учетных данных» .

Подробности интеграции с регистратором верификаторов

В следующем разделе рассматриваются технические детали интеграции регистраторов-верификаторов с API цифровых учетных данных (включая форматирование запросов, шифрование запросов, запуск API, проверку ответов и реализацию доказательств с нулевым разглашением).

Поддерживаемые форматы и возможности

Google Wallet поддерживает цифровые удостоверения личности на основе 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 Wallet для проверки вашей личности и отображения вашего бренда пользователю.

Параметр 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": {
          "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 и ID. Пользователь может выбрать любой из них.

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

Подписанные запросы

Подписанные запросы (запросы авторизации, защищенные JWT) инкапсулируют ваш проверяемый запрос на представление данных в криптографически подписанный JSON Web Token (JWT) с использованием вашей инфраструктуры PKI, обеспечивая целостность запроса и подтверждая вашу личность для Google Wallet.

Предварительные требования

Перед внесением изменений в код для подписанного запроса убедитесь, что у вас есть:

  • Закрытый ключ: Для подписи запроса, обрабатываемого на вашем сервере, вам потребуется закрытый ключ (например, эллиптическая кривая ES256 ).
  • Сертификат: Вам потребуется стандартный сертификат X.509, полученный на основе вашей пары ключей.
  • Регистрация: Убедитесь, что ваш публичный сертификат зарегистрирован в Google Wallet.

Логика построения запроса

Для формирования запроса вам необходимо использовать свой закрытый ключ и обернуть полезную нагрузку в 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 (бета-версия):

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-приложениями.

В вызове функции GetDigitalCredentialOption() вы будете использовать requestJson из Request Format в качестве 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 содержится зашифрованный токен идентификации (JWT), определенный W3C. За формирование этого ответа отвечает приложение Wallet.

Пример:

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

Вы передадите этот ответ обратно на сервер для проверки его подлинности. Инструкции по проверке ответа с учетными данными можно найти здесь.

Веб

Для запроса учетных данных с помощью 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 обратно на свой сервер для проверки учетных данных.

Проверьте ответ

После того как кошелек вернет зашифрованный 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, так и через веб-интерфейс вам потребуется использовать тот же nonce, который вы использовали для генерации 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 для элементов данных (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 и каковы его возможности, см. раздел часто задаваемых вопросов (FAQ) .

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

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

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

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