В этом руководстве объясняется, как проверяющие стороны (RP) могут технически интегрировать API цифровых учетных данных для запроса и проверки мобильных водительских удостоверений (mDL) и пропусков Google Wallet в приложениях Android и в веб-версии.
Процесс регистрации и предварительные условия
Перед запуском в рабочую среду необходимо официально зарегистрировать приложение-доверяющую сторону в Google.
- Тестирование в песочнице: Вы можете немедленно приступить к разработке, используя нашу песочницу и создав идентификатор теста . Принятие условий предоставления услуг для тестирования не требуется.
- Заполните форму заявки: Заполните форму регистрации RP . Регистрация обычно занимает 3-5 рабочих дней. Название и логотип вашего продукта будут отображаться на экране согласия пользователя, чтобы помочь пользователям определить, кто запрашивает их данные.
- Примите условия предоставления услуг: Вы должны подписать условия предоставления услуг, прежде чем они начнут действовать.
Поддерживаемые форматы и возможности
Google Wallet поддерживает цифровые удостоверения личности на основе ISO mdoc .
- Поддерживаемые учетные данные: Вы можете ознакомиться со списком поддерживаемых учетных данных и их атрибутов .
- Поддерживаемые протоколы: OpenID4VP (версия 1.0) .
- Минимальные требования к Android SDK: Android 9 (уровень API 28) и выше.
- Поддержка браузеров: Полный список браузеров, поддерживающих API цифровых учетных данных, см. на странице поддержки экосистемы .
Отформатируйте запрос
Для запроса учетных данных из любого кошелька необходимо отформатировать запрос с использованием 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 содержит открытый ключ шифрования для каждого запроса. Вам потребуется хранить закрытые ключи для каждого запроса и использовать их для аутентификации и авторизации токена, полученного от приложения-кошелька.
Параметр 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
],
"isserauth_alg_values": [
-7
]
}
}
}
}
Любой соответствующий требованиям сертификат
Вот пример запроса для передачи 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
],
"isserauth_alg_values": [
-7
]
}
}
}
}
Вы можете запросить любое количество поддерживаемых атрибутов из любых учетных данных, хранящихся в Google Wallet.
Подписанные запросы
Подписанные запросы (запросы авторизации, защищенные JWT) инкапсулируют ваш проверяемый запрос на представление данных в криптографически подписанный JSON Web Token (JWT) с использованием вашей инфраструктуры PKI, обеспечивая целостность запроса и подтверждая вашу личность для Google Wallet.
Предварительные требования
Before implementing the code changes for signed request, ensure you have:
- Закрытый ключ: Для подписи запроса, обрабатываемого на вашем сервере, вам потребуется закрытый ключ (например, эллиптическая кривая
ES256). - Сертификат: Вам потребуется стандартный сертификат X.509, полученный на основе вашей пары ключей.
- Регистрация: Убедитесь, что ваш публичный сертификат зарегистрирован в Google Wallet. Обратитесь в нашу службу поддержки по адресу
wallet-identity-rp-support@google.com
Логика построения запроса
Для формирования запроса вам необходимо использовать свой закрытый ключ и обернуть полезную нагрузку в 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.
}
}
Создайте стенограмму сессии.
Следующий шаг — создание записи сеанса (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. Это включает в себя несколько этапов, таких как:Проверьте сертификат эмитента в соответствующем штате. Обратитесь к сертификатам IACA поддерживаемого эмитента .
Проверьте подпись MSO (18013-5 Раздел 9.1.2)
Вычислите и проверьте
ValueDigestsдля элементов данных (18013-5 Раздел 9.1.2)Проверка подписи
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.
Сервис проверки содержит готовый к развертыванию сервер на основе Docker, который позволяет проверять ответ на соответствие определенным сертификатам IACA от эмитента.
Вы можете изменить файл certs.pem , чтобы управлять сертификатами эмитентов IACA , которым вы хотите доверять.
Ресурсы и поддержка
- Reference Implementation: Check out our Identity Verifiers Reference Implementation on GitHub.
- Тестовый сайт: Проверьте весь процесс от начала до конца на verifier.multipaz.org .
- Спецификация OpenID4VP: Ознакомьтесь с технической спецификацией OpenID4VP .
- Поддержка: Для получения помощи в отладке или по вопросам, возникающим в процессе интеграции, обращайтесь по адресу
wallet-identity-rp-support@google.com.