قبول بيانات الاعتماد الرقمية على الإنترنت

يوضّح هذا الدليل كيف يمكن للأطراف المعتمدة دمج واجهة برمجة التطبيقات Digital Credentials API من الناحية الفنية لطلب بيانات اعتماد الهوية الرقمية والتحقّق من صحتها على تطبيقات Android والويب.

عملية التسجيل والمتطلبات الأساسية

قبل نشر تطبيقك في مرحلة الإنتاج، يجب تسجيله رسميًا لدى Google. نستخدم عملية توقيع الشهادات حيث توقّع Google شهادتك لإثبات صحتها.

  1. الاختبار في Sandbox: يمكنك بدء عملية التطوير على الفور بدون إرسال نموذج طلب. يمكنك إجراء الاختبار مباشرةً باستخدام مفاتيح الاختبار الموثوق بها مسبقًا والبيانات الوصفية النموذجية المنشورة على صفحة وضع الحماية.
  2. تسجيل مسار العمل المتكامل: بعد الانتهاء من الاختبار والحصول على عملية دمج تعمل في Sandbox، سجِّل فيديو متكاملاً يعرض مسار الدمج بأكمله.
  3. إرسال نموذج طلب الانضمام وقبول بنود الخدمة: املأ نموذج منح إمكانيّة الوصول إلى الطرف المعتمِد وأرسِله.

يتطلّب النموذج المعلومات التالية:

  • طلب توقيع الشهادة (CSR) الخاص بإصدارك العلني
  • مواد العرض على الشبكة الإعلانية: عنوان URL الخاص بالشعار، والاسم المعروض، وعنوان URL الخاص بسياسة الخصوصية، وعنوان URL الخاص ببنود الخدمة
  • فيديو شامل لعملية دمج بيئة الاختبار المعزولة

بعد الموافقة، ستزوّدك Google بشهادة موقَّعة وبياناتك الوصفية الفريدة المرمّزة بتنسيق Base64URL (gw_rp_metadata_bytes).

تفاصيل الدمج الفني

تتضمّن الأقسام التالية تفاصيل الدمج الفني لجهات الاعتماد التي يتم دمجها مباشرةً مع Digital Credentials API (بما في ذلك تنسيق الطلبات وتشفيرها وتفعيل واجهة برمجة التطبيقات والتحقّق من صحة الردود وتنفيذ إثباتات صحة المعلومات بدون الكشف عنها).

التنسيقات والإمكانات المتوافقة

تتيح "محفظة Google" استخدام مستندات التعريف الرقمية المستندة إلى معيار ISO mdoc.

تنسيق الطلب

لطلب بيانات اعتماد من أي محفظة، يجب تنسيق طلبك باستخدام OpenID4VP. يمكنك طلب بيانات اعتماد محدّدة أو بيانات اعتماد متعدّدة في عنصر dcql_query واحد.

مثال على طلب JSON

في ما يلي نموذج لطلب مستند رقمي 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" لإثبات هويتك وعرض علامتك التجارية للمستخدم.

تحتوي المَعلمة 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": {
          // Use org.iso.18013.5.1.mDL for mDL,
          // com.google.wallet.idcard.1 for ID pass, or
          // org.iso.23220.photoid.1 for ID pass (using photo ID document type)
          "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
          }
        ]
      }
    ]
  },
  "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>"
  }
}

أي بيانات اعتماد مؤهَّلة

في ما يلي نموذج طلب لرخصة القيادة الرقمية وبطاقة التعريف وبطاقة التعريف (باستخدام نوع مستند تعريف يحمل صورة). ويمكن للمستخدم المتابعة باستخدام أي منهما.

{
  "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 pass
        "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 type 3: ID pass (using photo ID document type)
        "id": "photo_id-request",
        "format": "mso_mdoc",
        "meta": {
          "doctype_value": "org.iso.23220.photoid.1"
        },
        "claims": [
          {
            "path": [
              "org.iso.23220.1",
              "family_name"
            ],
            "intent_to_retain": false // set this to true if you are saving the value of the field
          },
          {
            "path": [
              "org.iso.23220.1",
              "given_name"
            ],
            "intent_to_retain": false
          },
          {
            "path": [
              "org.iso.23220.1",
              "age_over_18"
            ],
            "intent_to_retain": false
          }
        ]
      }
    ]
    credential_sets : [
      {
        "options": [
          [ "mdl-request" ],
          [ "id_pass-request" ],
          [ "photo_id-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".

الطلبات الموقّعة

تتضمّن الطلبات الموقّعة (طلبات التفويض الآمنة باستخدام رموز JSON المميّزة للويب) طلب عرض بيانات يمكن إثبات صحتها داخل رمز JSON مميّز للويب (JWT) موقّع تشفيريًا باستخدام البنية الأساسية للمفتاح العام (PKI)، ما يضمن سلامة الطلب ويثبت هويتك لخدمة "محفظة Google".

.

المتطلبات الأساسية

قبل تنفيذ تغييرات الرمز لطلب موقّع، تأكَّد من توفّر ما يلي:

  • المفتاح الخاص: تحتاج إلى مفتاح خاص (مثل ES256 Elliptic Curve) لتوقيع الطلب الذي تتم إدارته في الخادم.
  • الشهادة: تحتاج إلى شهادة X.509 عادية مشتقة من زوج المفاتيح.
  • التسجيل: تأكَّد من تسجيل شهادتك العامة في "محفظة Google".

منطق إنشاء الطلبات

لإنشاء طلب، عليك استخدام مفتاحك الخاص وتضمين الحمولة في توقيع 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

تشغيل واجهة برمجة التطبيقات

يجب إنشاء طلب بيانات من واجهة برمجة التطبيقات بالكامل من جهة الخادم. استنادًا إلى النظام الأساسي، ستمرِّر ملف JSON الذي تم إنشاؤه إلى واجهات برمجة التطبيقات الخاصة بالنظام الأساسي.

داخل التطبيق (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")
}

ضبط Credential Manager

لضبط كائن CredentialManager وتهيئته، أضِف منطقًا مشابهًا لما يلي:

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

سمات هوية الطلب

بدلاً من تحديد مَعلمات فردية لطلبات تحديد الهوية، يقدّم التطبيق جميع المَعلمات معًا كسلسلة JSON ضمن CredentialOption. يرسل Credential Manager سلسلة JSON هذه إلى المحافظ الرقمية المتاحة بدون فحص محتوياتها. بعد ذلك، يكون كل تطبيق محفظة مسؤولاً عمّا يلي: - تحليل سلسلة JSON لفهم طلب تحديد الهوية. - تحديد بيانات الاعتماد المحفوظة التي تستوفي الطلب، إن وُجدت

ننصح الشركاء بإنشاء طلباتهم على الخادم حتى عند دمج تطبيقات Android.

ستستخدِم requestJson من تنسيق الطلب كـ request في استدعاء الدالة GetDigitalCredentialOption().

// 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 على identityToken مشفّر (JWT)، محدّد من قِبل W3C. تطبيق "محفظة Google" هو المسؤول عن صياغة هذا الردّ.

مثال:

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

ستعيد هذا الردّ إلى الخادم للتحقّق من صحته. يمكنك الاطّلاع على خطوات التحقّق من صحة استجابة بيانات الاعتماد

الويب

لطلب مستندات تعريف الهوية باستخدام Digital Credentials 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.
            }
          ]
        }
      })

أرسِل الردّ من واجهة برمجة التطبيقات هذه إلى خادمك للتحقّق من صحة الردّ على بيانات الاعتماد.

التحقّق من صحة الردّ

بعد أن تعرض المحفظة 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. إنشاء نص الجلسة

    الخطوة التالية هي إنشاء SessionTranscript من ISO/IEC 18013-5:2021 باستخدام بنية Handover خاصة بنظام التشغيل Android أو الويب:

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

    في كلتا الحالتين، سواء كان التسليم على Android أو على الويب، عليك استخدام قيمة nonce نفسها التي استخدمتها لإنشاء credential_request.

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

    تسليم المتصفّح

        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، يجب التحقّق من صحة &quot;رد الجهاز&quot; وفقًا للبند 9 من معيار ISO/IEC 18013-5:2021.

    تتضمّن عملية التحقّق من الصحة عدة خطوات:

  2. التحقّق من شهادة جهة الإصدار: استخرِج سلسلة شهادات التوقيع الخاصة بجهة الإصدار من issuerAuth وتحقّق من صحتها مقارنةً بشهادات الجذر الموثوق بها الصادرة عن هيئة اعتماد الإنترنت في الهند (IACA). راجِع شهادات IACA الصادرة عن جهات الإصدار المعتمَدة.

  3. التحقّق من توقيع MSO (القسم 9.1.2 من المعيار 18013-5)

  4. حساب ValueDigests والتحقّق منه لعناصر البيانات (القسم 9.1.2 من معيار 18013-5)

  5. التحقّق من توقيع deviceSignature (الفقرة 9.1.3 من المعيار 18013-5)

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

لإتاحة استخدام Zero-Knowledge Proofs (مثل التأكّد من أنّ عمر المستخدم يتخطّى 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.

يحتوي verifier-service على خادم جاهز للنشر يستند إلى Docker ويتيح لك التحقّق من صحة الردّ مقارنةً بشهادات IACA الخاصة بجهات إصدار معيّنة.

يمكنك تعديل ملف certs.pem لإدارة شهادات جهات إصدار IACA التي تريد الوثوق بها.

المراجع والدعم