디지털 신분증은 인앱 및 웹 흐름 모두에서 허용될 수 있습니다. Google 월렛의 사용자 인증 정보를 수락하려면 다음 단계를 따르세요.
- 제공된 안내에 따라 앱 또는 웹을 사용하여 통합합니다.
- 테스트 ID를 사용하여 Google 월렛 샌드박스를 통해 흐름을 테스트합니다.
- 라이브로 전환하려면 이 양식을 작성하여 액세스를 요청하고 Google 월렛 사용자 인증 정보 서비스 약관에 동의하세요. 각 비즈니스 법인에 대해 작성해야 합니다. 양식을 작성해 주시면 Google팀에서 연락을 드립니다.
- 궁금한 점이 있으면
wallet-identity-rp-support@google.com에 문의하세요.
지원되는 사용자 인증 정보 형식
디지털 신원 문서의 데이터 형식을 정의하는 여러 제안된 표준이 있으며, 그중 두 가지가 업계에서 상당한 관심을 받고 있습니다.
- mdocs - ISO에 의해 정의됩니다.
- w3c Verifiable Credentials - w3c에서 정의합니다.
Android 인증 관리자는 두 형식을 모두 지원하지만, 현재 Google 월렛은 mdoc 기반 디지털 ID만 지원합니다.
지원되는 사용자 인증 정보
Google 월렛은 다음 두 가지 사용자 인증 정보 유형을 지원합니다.
- 모바일 운전면허증 (mDL)
- 신분증 패스
단일 매개변수 변경으로 흐름에서 두 사용자 인증 정보를 모두 요청할 수 있습니다.
사용자 환경
이 섹션에서는 권장되는 온라인 프레젠테이션 흐름을 설명합니다. 흐름은 주류 배송 앱에 연령이 표시되는 방식을 보여주지만 웹 및 기타 유형의 프레젠테이션의 UX도 유사합니다.
![]() |
![]() |
![]() |
![]() |
![]() |
| 사용자에게 앱 또는 웹사이트에서 연령을 확인하라는 메시지가 표시됨 | 사용자에게 사용 가능한 인증 정보가 표시됨 | Google 월렛에 확인 페이지가 표시됨 | 사용자가 인증하여 공유 확인 | 앱 또는 웹사이트로 전송된 데이터 |
주요 참고사항
- 앱 또는 웹사이트는 API 진입점을 만드는 방식에 유연성이 있습니다. 1단계에 표시된 대로 시간이 지남에 따라 Google 월렛 이외의 옵션이 API를 통해 제공될 것으로 예상되므로 '디지털 신분증으로 인증'과 같은 일반 버튼을 표시하는 것이 좋습니다.
- 2단계의 선택기 화면은 Android에서 렌더링합니다. 사용 가능한 사용자 인증 정보는 각 월렛에서 제공하는 등록 로직과 신뢰 당사자가 전송한 요청 간의 일치 여부에 따라 결정됩니다.
- 3단계는 Google 월렛에서 렌더링됩니다. Google 월렛에는 개발자가 이 화면에 제공하는 이름, 로고, 개인정보처리방침이 표시됩니다.
디지털 ID 흐름 추가
사용자에게 사용자 인증 정보가 없는 경우 사용자가 디지털 신분증을 추가할 수 있도록 Google 월렛으로 연결되는 딥 링크를 '디지털 신분증으로 인증' 버튼 옆에 제공하는 것이 좋습니다.
![]() |
![]() |
| 사용자에게 앱 또는 웹사이트에서 연령을 확인하라는 메시지가 표시됨 | 사용자가 Google 월렛으로 이동하여 디지털 신분증을 받음 |
사용 가능한 디지털 ID 없음
사용자가 디지털 ID가 없는 상태에서 '디지털 ID로 인증' 옵션을 선택하면 이 오류 메시지가 표시됩니다.
![]() |
![]() |
| 사용자에게 앱 또는 웹사이트에서 연령을 확인하라는 메시지가 표시됨 | 디지털 ID가 없는 사용자에게 오류가 표시됨 |
API는 사용자의 개인 정보를 보호하기 위해 사용자가 사용할 수 있는 디지털 ID가 있는지 여부를 자동으로 학습하는 기능을 지원하지 않습니다. 따라서 표시된 것처럼 온보딩 링크 옵션을 포함하는 것이 좋습니다.
지갑에서 ID 사용자 인증 정보를 요청하는 요청 형식
다음은 Android 기기나 웹의 지갑에서 신원 사용자 인증 정보를 가져오는 mdoc requestJson 요청의 샘플입니다.
{
"requests" : [
{
"protocol": "openid4vp-v1-unsigned", // openid4vp-v1-signed for signed request.
"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", // 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 월렛에 저장된 모든 ID 인증 정보에서 원하는 수의 지원되는 속성을 요청할 수 있습니다.
서명된 요청
보안을 강화하고 인증 요청의 무결성을 보장하려면 서명된 요청 (JWT 보안 승인 요청 또는 JAR라고도 함)을 구현해야 합니다. 자세한 내용은 openID4VP 문서를 참고하세요.
매개변수가 원시 JSON으로 전송되는 서명되지 않은 요청과 달리 서명된 요청은 암호화 서명된 JSON 웹 토큰 (JWT) 내에 확인 가능한 프레젠테이션 요청을 캡슐화합니다. 이렇게 하면 두 가지 주요 이점이 있습니다.
- 무결성: 요청이 조작되지 않았는지 확인합니다.
- 대체 인증 메커니즘 (x509 PKI): 요청이 Google 월렛에 등록된 특정 공개 키를 소유한 검증 기관에서 시작되었음을 Google 월렛에 증명합니다.
기본 요건
서명된 요청을 위한 코드 변경사항을 구현하기 전에 다음 사항을 확인하세요.
- 비공개 키: 비공개 키 (예: 타원 곡선
ES256또는 RSA)을 사용하여 서버에서 관리되는 요청에 서명합니다. - 인증서: 키 쌍에서 파생된 표준 X.509 인증서가 필요합니다.
- 등록: 공개 인증서가 Google 월렛에 등록되어 있는지 확인합니다. 지원팀(
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
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
인앱
Android 앱에서 ID 사용자 인증 정보를 요청하려면 다음 단계를 따르세요.
종속 항목 업데이트
프로젝트의 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)
요청 ID 속성
ID 요청에 개별 매개변수를 지정하는 대신 앱은 CredentialOption 내에서 모든 매개변수를 JSON 문자열로 함께 제공합니다. 사용자 인증 정보 관리자는 콘텐츠를 검사하지 않고 이 JSON 문자열을 사용 가능한 디지털 지갑에 전달합니다. 그러면 각 지갑은 다음을 담당합니다. - JSON 문자열을 파싱하여 ID 요청을 이해합니다. - 저장된 사용자 인증 정보 중 요청을 충족하는 사용자 인증 정보가 있는지 확인합니다.
파트너는 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)이 포함됩니다. 이 응답을 작성하는 것은 Wallet 앱의 책임입니다.
예:
{
"protocol" : "openid4vp-v1-unsigned",
"data" : {
<encrpted_response>
}
}
이 응답을 서버로 다시 전달하여 진위 여부를 확인합니다. 사용자 인증 정보 응답을 검증하는 단계를 확인할 수 있습니다.
웹
Chrome 또는 기타 지원되는 브라우저에서 디지털 사용자 인증 정보 API를 사용하여 ID 사용자 인증 정보를 요청하려면 다음 요청을 실행하세요.
const credentialResponse = await navigator.credentials.get({
digital : {
requests : [
{
protocol: "openid4vp-v1-unsigned",
data: {<credential_request>} // This is an object, shouldn't be a string.
}
]
}
})
이 API의 응답을 서버로 다시 보내 사용자 인증 정보 응답을 검증합니다.
인증 정보 응답 유효성 검사 단계
앱 또는 웹사이트에서 암호화된 identityToken을 수신하면 응답을 신뢰하기 전에 여러 유효성 검사를 실행해야 합니다.
비공개 키를 사용하여 응답 복호화
첫 번째 단계는 저장된 비공개 키를 사용하여 토큰을 복호화하고 응답 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) # 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는 사용자 인증 정보를 포함하는vp_tokenJSON을 생성합니다.{ "vp_token": { "cred1": ["<base64UrlNoPadding_encoded_credential>"] // This applies to OpenID4VP 1.0 spec. } }세션 스크립트 만들기
다음 단계는 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>", 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를 사용하여 DeviceResponse는 ISO/IEC 18013-5:2021 조항 9에 따라 검증되어야 합니다. 여기에는 다음과 같은 여러 단계가 포함됩니다.주 발급자 인증서를 확인합니다. 지원되는 발급기관의 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
}
솔루션 빌드
솔루션을 빌드하려면 GitHub의 신원 확인 도구 참조 구현을 참고하세요.
영지식 증명 (ZKP) 기반 확인
영지식 증명 (ZKP)은 개인(증명자)이 실제 기본 데이터를 공개하지 않고도 특정 신원 정보를 보유하거나 특정 기준 (예: 18세 이상, 유효한 사용자 인증 정보 보유)을 충족함을 검증자에게 증명할 수 있는 암호화 방법입니다. 기본적으로 민감한 세부정보를 비공개로 유지하면서 본인에 관한 진술의 진실성을 확인하는 방법입니다.
신원 데이터의 직접 공유에 의존하는 디지털 신원 시스템은 사용자가 과도한 개인 정보를 공유하도록 요구하는 경우가 많아 데이터 유출 및 신원 도용의 위험이 증가합니다. 영지식 증명은 최소한의 공개로 검증을 지원하는 패러다임 전환을 제공합니다.
디지털 ID의 ZKP 주요 개념:
- 증명자: 신원의 특정 측면을 증명하려는 개인입니다.
- 인증자: ID 속성의 증명을 요청하는 항목입니다.
- 증명: 증명자가 비밀 정보를 공개하지 않고 검증자에게 주장의 진실성을 확신시킬 수 있는 암호화 프로토콜입니다.
영지식 증명의 핵심 속성:
- 완전성: 명제가 참이고 증명자와 검증자가 모두 정직하다면 검증자는 확신하게 됩니다.
- 건전성: 명제가 거짓인 경우 부정직한 증명자는 정직한 검증자에게 명제가 참이라고 확신시킬 수 없습니다 (매우 높은 확률로).
- 영지식: 검증자는 문장이 참이라는 사실 외에는 아무것도 알 수 없습니다. 증명자의 신원에 관한 실제 데이터는 노출되지 않습니다.
Google 월렛에서 제로 지식 증명을 다시 받으려면 요청 형식을 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
{
...
지갑에서 암호화된 영지식 증명을 다시 받게 됩니다. Google의 longfellow-zk 라이브러리를 사용하여 발급자 IACA 인증서에 대해 이 증명을 검증할 수 있습니다.
verifier-service에는 특정 발급기관 IACA 인증서에 대해 응답을 검증할 수 있는 배포 준비가 완료된 Docker 기반 서버가 포함되어 있습니다.
신뢰할 IACA 발급자 인증서를 관리하도록 certs.pem을 수정할 수 있습니다.
자세한 내용은 지원 이메일로 문의하세요.
wallet-identity-rp-support@google.com







