온라인 디지털 사용자 인증 정보 수락

이 가이드에서는 신뢰 당사자 (RP)가 디지털 사용자 인증 정보 API를 기술적으로 통합하여 Android 앱과 웹 전반에서 Google 월렛의 모바일 운전면허증 (mDL)과 ID 패스를 요청하고 검증하는 방법을 설명합니다.

등록 절차 및 기본 요건

프로덕션에서 게시하기 전에 신뢰 당사자 애플리케이션을 Google에 정식으로 등록해야 합니다.

  1. 샌드박스에서 테스트: 샌드박스 환경테스트 ID 만들기를 사용하여 즉시 개발을 시작할 수 있습니다. 테스트에는 서비스 약관에 동의하지 않아도 됩니다.
  2. 접수 양식 제출: RP 온보딩 양식을 작성합니다. 온보딩에는 일반적으로 영업일 기준 3~5일이 소요됩니다. 제품 이름과 로고는 사용자에게 표시되는 동의 화면에 표시되어 사용자가 데이터를 요청하는 주체를 식별할 수 있도록 지원합니다.
  3. 서비스 약관에 동의: 라이브로 전환하기 전에 서비스 약관에 서명해야 합니다.

지원되는 형식 및 기능

Google 월렛은 ISO mdoc 기반 디지털 신분증을 지원합니다.

요청 형식 지정

지갑에서 사용자 인증 정보를 요청하려면 OpenID4VP를 사용하여 요청의 형식을 지정해야 합니다. 단일 dcql_query 객체에서 특정 사용자 인증 정보 또는 여러 사용자 인증 정보를 요청할 수 있습니다.

JSON 요청 예

다음은 Android 기기 또는 웹의 지갑에서 ID 사용자 인증 정보를 가져오는 mdoc requestJson 요청의 샘플입니다.

{
      "requests" : [
        {
          "protocol": "openid4vp-v1-signed",
          "data": {<signed_credential_request>} // This is an object, shouldn't be a string.
        }
      ]
}

암호화 요청

client_metadata에는 각 요청의 암호화 공개 키가 포함됩니다. 각 요청의 비공개 키를 저장하고 이를 사용하여 지갑 앱에서 수신한 토큰을 인증하고 승인해야 합니다.

requestJsoncredential_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 보안 승인 요청)은 PKI 인프라를 사용하여 암호화 서명된 JSON 웹 토큰(JWT) 내에 확인 가능한 프레젠테이션 요청을 캡슐화하여 요청 무결성을 보장하고 Google 월렛에 신원을 증명합니다.

기본 요건

서명된 요청을 위한 코드 변경사항을 구현하기 전에 다음 사항을 확인하세요.

  • 비공개 키: 서버에서 관리되는 요청에 서명하려면 비공개 키 (예: 타원 곡선 ES256)가 필요합니다.
  • 인증서: 키 쌍에서 파생된 표준 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
            # 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 앱에서 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 앱 통합의 경우에도 서버에서 요청을 생성하는 것이 좋습니다.

요청 형식requestJsonGetDigitalCredentialOption() 함수 호출에서 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-signed",
  "data" : {
    <encrpted_response>
  }
}

이 응답을 서버로 다시 전달하여 진위 여부를 확인합니다. 사용자 인증 정보 응답을 검증하는 단계

Chrome 또는 기타 지원되는 브라우저에서 디지털 사용자 인증 정보 API를 사용하여 ID 사용자 인증 정보를 요청하려면 다음 요청을 실행하세요.

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은 사용자 인증 정보가 포함된 vp_token JSON을 생성합니다.

  {
    "vp_token":
    {
      "cred1": ["<base64UrlNoPadding_encoded_credential>"] // This applies to OpenID4VP 1.0 spec.
    }
  }
  1. 세션 스크립트 만들기

    다음 단계는 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를 사용하여 기기 응답은 ISO/IEC 18013-5:2021 조항 9에 따라 검증해야 합니다. 여기에는 다음과 같은 여러 단계가 포함됩니다.

  2. 주 발급자 인증서를 확인합니다. 지원되는 발급기관의 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 구성을 제공하세요.

  ...
  "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을 수정할 수 있습니다.

리소스 및 지원

  • 참조 구현: GitHub에서 ID 확인 도구 참조 구현을 확인하세요.
  • 테스트 웹사이트: verifier.multipaz.org에서 엔드 투 엔드 흐름을 테스트합니다.
  • OpenID4VP 사양: openID4VP의 기술 사양을 확인하세요.
  • 지원: 통합 중에 디버깅 지원이 필요하거나 궁금한 점이 있으면 wallet-identity-rp-support@google.com에 문의하세요.