Dijital Kimlik Bilgilerinin Online Olarak Kabul Edilmesi

Bu kılavuzda, güvenen tarafların (RP'ler) Android uygulamaları ve web'de Google Cüzdan'dan mobil sürücü belgeleri (mDL) ve kimlik kartları istemek ve doğrulamak için Digital Credentials API'yi teknik olarak nasıl entegre edebileceği açıklanmaktadır.

Kayıt Süreci ve Ön Koşullar

Üretimde yayınlanmadan önce, Relying Party uygulamanızı Google'a resmi olarak kaydetmeniz gerekir.

  1. Korumalı alanda test etme: Korumalı alan ortamımızı ve test kimliği oluşturma özelliğimizi kullanarak geliştirmeye hemen başlayabilirsiniz. Test için Hizmet Şartları'nı kabul etmeniz gerekmez.
  2. Kabul Formunu Gönderin: RP İlk Katılım Formu'nu doldurun. İlk katılım süreci genellikle 3-5 iş günü sürer. Ürün Adınız ve Logonuz, kullanıcıların verilerini kimin istediğini belirlemelerine yardımcı olmak için kullanıcı rızası ekranında gösterilir.
  3. Hizmet Şartları'nı kabul edin: Yayına başlamadan önce Hizmet Şartları'nı imzalamanız gerekir.

Desteklenen Biçimler ve Özellikler

Google Cüzdan, ISO mdoc tabanlı dijital kimlikleri destekler.

İsteği Biçimlendirme

Herhangi bir cüzdandan kimlik bilgisi istemek için isteğinizi OpenID4VP kullanarak biçimlendirmeniz gerekir. Tek bir dcql_query nesnesinde belirli kimlik bilgileri veya birden fazla kimlik bilgisi isteyebilirsiniz.

JSON İsteği Örneği

Android cihazda veya web'de herhangi bir cüzdandan kimlik bilgisi almak için mdoc requestJson isteğinin örneğini aşağıda bulabilirsiniz.

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

Şifreleme İsteği

client_metadata, her istek için şifreleme ortak anahtarını içerir. Her istek için özel anahtarları saklamanız ve bunları, cüzdan uygulamasından aldığınız jetonu kimlik doğrulamak ve yetkilendirmek için kullanmanız gerekir.

requestJson içindeki credential_request parametresi aşağıdaki alanları içerir.

Belirli kimlik bilgisi

{
  "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
        ]
      }
    }
  }
}

Uygun Kimlik Bilgileri

Hem mDL hem de kimlik kartı için örnek isteği aşağıda bulabilirsiniz. Kullanıcı bunlardan biriyle devam edebilir.

{
  "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 Cüzdan'da saklanan kimlik bilgilerinden istediğiniz sayıda desteklenen özelliği isteyebilirsiniz.

İmzalı İstekler

İmzalı İstekler (JWT ile Güvenli Yetkilendirme İstekleri), PKI altyapınızı kullanarak doğrulanabilir sunum isteğinizi kriptografik olarak imzalanmış bir JSON Web Jetonu (JWT) içinde kapsar. Böylece, isteğin bütünlüğü sağlanır ve Google Cüzdan'a kimliğiniz kanıtlanır.

Ön koşullar

İmzalı istek için kod değişikliklerini uygulamadan önce şunlardan emin olun:

  • Özel anahtar: Sunucunuzda yönetilen isteği imzalamak için özel bir anahtara (ör. Eliptik Eğri ES256) ihtiyacınız vardır.
  • Sertifika: Anahtar çiftinizden türetilmiş standart bir X.509 sertifikasına ihtiyacınız vardır.
  • Kayıt: Genel sertifikanızın Google Cüzdan'a kaydedildiğinden emin olun. wallet-identity-rp-support@google.com numarası üzerinden destek ekibimizle iletişime geçin.

İstek oluşturma mantığı

İstek oluşturmak için özel anahtarınızı kullanmanız ve yükü JWS'ye sarmalamanız gerekir.

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'yi tetikleme

API isteğinin tamamı sunucu tarafında oluşturulmalıdır. Platforma bağlı olarak, oluşturulan JSON'u yerel API'lere iletirsiniz.

Uygulama İçi (Android)

Android uygulamalarınızdan kimlik bilgisi istemek için aşağıdaki adımları uygulayın:

Bağımlılıkları güncelleme

Projenizin build.gradle dosyasında, Kimlik Bilgisi Yöneticisi'ni (beta) kullanmak için bağımlılıklarınızı güncelleyin:

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

Kimlik Bilgisi Yöneticisi'ni yapılandırma

Bir CredentialManager nesnesini yapılandırmak ve başlatmak için aşağıdakine benzer bir mantık ekleyin:

// Use your app or activity context to instantiate a client instance of CredentialManager.
val credentialManager = CredentialManager.create(context)

İstek kimliği özellikleri

Uygulama, kimlik istekleri için bağımsız parametreler belirtmek yerine bunların hepsini CredentialOption içinde bir JSON dizesi olarak birlikte sağlar. Kimlik bilgisi yöneticisi, bu JSON dizesini içeriğini incelemeden kullanılabilir dijital cüzdanlara iletir. Her cüzdan daha sonra şunlardan sorumludur: - Kimlik isteğini anlamak için JSON dizesini ayrıştırma. - Kayıtlı kimlik bilgilerinden hangilerinin (varsa) isteği karşıladığını belirleme

İş ortaklarının, Android uygulaması entegrasyonları için bile isteklerini sunucuda oluşturmalarını öneririz.

İstek Biçimi'ndeki requestJson öğesini GetDigitalCredentialOption() işlev çağrısında request olarak kullanacaksınız.

// 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)
    }
}

Kimlik bilgisi yanıtını işleme

Cüzdandan yanıt aldıktan sonra yanıtın başarılı olup olmadığını ve credentialJson yanıtını içerip içermediğini doğrulayın.

// 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 yanıtı, W3C tarafından tanımlanan şifrelenmiş bir identityToken (JWT) içerir. Bu yanıtı oluşturmaktan Cüzdan uygulaması sorumludur.

Örnek:

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

Bu yanıtı, geçerliliğini doğrulamak için sunucuya geri iletirsiniz. Kimlik bilgisi yanıtını doğrulama adımlarını

Web

Chrome'da veya desteklenen diğer tarayıcılarda Digital Credentials API'yi kullanarak kimlik bilgisi istemek için aşağıdaki isteği gönderin.

const credentialResponse = await navigator.credentials.get({
          digital : {
          requests : [
            {
              protocol: "openid4vp-v1-signed",
              data: {<credential_request>} // This is an object, shouldn't be a string.
            }
          ]
        }
      })

Kimlik bilgisi yanıtını doğrulamak için bu API'den gelen yanıtı sunucunuza geri gönderin.

Yanıtı Doğrulama

Cüzdan, şifrelenmiş identityToken (JWT) döndürdüğünde verilere güvenmeden önce sıkı bir sunucu tarafı doğrulama işlemi gerçekleştirmeniz gerekir.

Yanıtın şifresini çözme

JWE'nin şifresini çözmek için isteğin client_metadata bölümünde gönderilen ortak anahtara karşılık gelen özel anahtarı kullanın. Bu işlem vp_token ile sonuçlanır.

Python örneği:

  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, kimlik bilgisini içeren vp_token JSON'a neden olur.

  {
    "vp_token":
    {
      "cred1": ["<base64UrlNoPadding_encoded_credential>"] // This applies to OpenID4VP 1.0 spec.
    }
  }
  1. Oturum transkriptini oluşturma

    Bir sonraki adım, Android veya web'e özel bir devretme yapısıyla ISO/IEC 18013-5:2021'den SessionTranscript oluşturmaktır:

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

    Hem Android hem de web devirlerinde, credential_request oluşturmak için kullandığınız aynı tek seferlik rastgele sayıyı kullanmanız gerekir.

    Android Handover

        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()
        

    Tarayıcı Aktarımı

        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 kullanılarak Cihaz Yanıtı, ISO/IEC 18013-5:2021 madde 9'a göre doğrulanmalıdır. Bu süreçte aşağıdaki gibi çeşitli adımlar yer alır:

  2. Eyalet tarafından verilen sertifikayı kontrol edin. Desteklenen kuruluşun IACA sertifikalarına bakın.

  3. MSO imzasını doğrulama (18013-5 Bölüm 9.1.2)

  4. Veri Öğeleri (18013-5 Bölüm 9.1.2) için ValueDigests değerini hesaplayın ve kontrol edin.

  5. deviceSignature imzasını doğrulama (18013-5 Bölüm 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
}

Gizliliği korumaya yönelik yaş doğrulama (ZKP)

Sıfır bilgi kanıtlarını (ör. kullanıcının tam doğum tarihini görmeden 18 yaşından büyük olduğunu doğrulama) desteklemek için istek biçiminizi mso_mdoc_zk olarak değiştirin ve gerekli zk_system_type yapılandırmasını sağlayın.

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

Cüzdandan şifrelenmiş bir sıfır bilgi kanıtı alırsınız. Google'ın longfellow-zk kitaplığını kullanarak bu kanıtı IACA sertifikası veren kuruluşlara karşı doğrulayabilirsiniz.

Doğrulayıcı hizmeti, yanıtı belirli kart sağlayıcı IACA sertifikalarına göre doğrulamanıza olanak tanıyan, dağıtıma hazır ve Docker tabanlı bir sunucu içerir.

Güvenmek istediğiniz IACA veren sertifikalarını yönetmek için certs.pem dosyasını değiştirebilirsiniz.

Kaynaklar ve Destek

  • Referans Uygulama: GitHub'daki Kimlik Doğrulayıcı Referans Uygulamamıza göz atın.
  • Test Web Sitesi: verifier.multipaz.org adresinde uçtan uca akışı deneyin.
  • OpenID4VP Spesifikasyonu: OpenID4VP'nin teknik özelliklerine göz atın.
  • Destek: Entegrasyon sırasında hata ayıklama yardımı veya sorularınız için wallet-identity-rp-support@google.com ile iletişime geçin.