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

يوضّح هذا الدليل كيف يمكن للجهات المعتمِدة (RP) دمج واجهة Digital Credentials API من الناحية الفنية لطلب والتحقّق من صحة رخص القيادة على الأجهزة الجوّالة (mDL) وبطاقات التعريف من محفظة Google على مستوى تطبيقات Android والويب.

.

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

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

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

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

تتيح "محفظة 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 على مفتاح التشفير العام لكل طلب. عليك تخزين المفاتيح الخاصة لكل طلب واستخدامها للمصادقة على الرمز المميّز الذي تتلقّاه من تطبيق المحفظة ومنحه الإذن.

تحتوي المَعلمة 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) وهويّة رقمية. ويمكن للمستخدم المتابعة باستخدام أي منهما.

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

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

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

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

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

  • المفتاح الخاص: تحتاج إلى مفتاح خاص (مثل ES256 Elliptic Curve) لتوقيع الطلب الذي تتم إدارته في الخادم.
  • الشهادة: تحتاج إلى شهادة 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

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

يجب إنشاء طلب بيانات من واجهة برمجة التطبيقات بالكامل من جهة الخادم. استنادًا إلى النظام الأساسي، ستمرِّر ملف 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

        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. التحقّق من شهادة جهة إصدار البطاقة في الولاية راجِع شهادات IACA الصادرة عن الجهات المعتمدة.

  3. التحقّق من توقيع مشغّل شبكة الجوّال (18013-5 الفقرة 9.1.2)

  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)

لإتاحة استخدام ميزة "إثبات صحة المعلومات بدون الكشف عنها" (مثل إثبات أنّ عمر المستخدم يتجاوز 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 التي تريد الوثوق بها.

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

  • التنفيذ المرجعي: يمكنك الاطّلاع على التنفيذ المرجعي لخدمات التحقّق من الهوية على GitHub.
  • الموقع الإلكتروني التجريبي: جرِّب مسار العمل المتكامل على verifier.multipaz.org.
  • مواصفات OpenID4VP: يمكنك الاطّلاع على المواصفات الفنية openID4VP.
  • الدعم: للحصول على مساعدة في تصحيح الأخطاء أو طرح الأسئلة أثناء عملية الدمج، يُرجى التواصل مع wallet-identity-rp-support@google.com.