پذیرش آنلاین مدارک دیجیتال

این راهنما توضیح می‌دهد که چگونه طرفین اعتماد (RP) می‌توانند از نظر فنی API اعتبارنامه‌های دیجیتال را برای درخواست و اعتبارسنجی گواهینامه‌های رانندگی موبایل (mDL) و کارت‌های شناسایی از Google Wallet در برنامه‌های اندروید و وب ادغام کنند.

مراحل ثبت نام و پیش نیازها

قبل از شروع به کار، باید برنامه Relying Party خود را رسماً در گوگل ثبت کنید.

  1. تست در محیط سندباکس: شما می‌توانید بلافاصله با استفاده از محیط سندباکس ما و ایجاد یک شناسه تست، توسعه را آغاز کنید. پذیرش شرایط خدمات برای تست الزامی نیست.
  2. ارسال فرم دریافت: فرم پذیرش RP را پر کنید. پذیرش معمولاً ۳ تا ۵ روز کاری طول می‌کشد. نام و لوگوی محصول شما در صفحه رضایت‌نامه روبروی کاربر نمایش داده می‌شود تا به کاربران کمک کند تشخیص دهند چه کسی درخواست‌کننده داده‌های آنهاست.
  3. پذیرش شرایط خدمات: قبل از شروع به کار، باید شرایط خدمات را امضا کنید.

فرمت‌ها و قابلیت‌های پشتیبانی‌شده

گوگل والت از شناسه‌های دیجیتال مبتنی بر ISO mdoc پشتیبانی می‌کند.

قالب‌بندی درخواست

برای درخواست اعتبارنامه از هر کیف پولی، باید درخواست خود را با استفاده از OpenID4VP قالب‌بندی کنید. می‌توانید اعتبارنامه‌های خاص یا چندین اعتبارنامه را در یک شیء dcql_query درخواست کنید.

مثال درخواست JSON

در اینجا نمونه‌ای از یک درخواست mdoc requestJson برای دریافت اطلاعات هویتی از هر کیف پولی روی دستگاه اندروید یا وب آورده شده است.

{
      "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 و 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 Wallet درخواست کنید.

درخواست‌های امضا شده

درخواست‌های امضا شده (درخواست‌های مجوز امن JWT) درخواست ارائه قابل تأیید شما را با استفاده از زیرساخت PKI شما، درون یک JSON Web Token (JWT) امضا شده رمزنگاری شده، کپسوله‌سازی می‌کند و یکپارچگی درخواست را تضمین کرده و هویت شما را به Google Wallet اثبات می‌کند.

پیش‌نیازها

قبل از اجرای تغییرات کد برای درخواست امضا شده، مطمئن شوید که موارد زیر را دارید:

  • کلید خصوصی: برای امضای درخواست که در سرور شما مدیریت می‌شود، به یک کلید خصوصی (مثلاً Elliptic Curve ES256 ) نیاز دارید.
  • گواهی: شما به یک گواهی استاندارد X.509 که از جفت کلید شما مشتق شده باشد، نیاز دارید.
  • ثبت نام: مطمئن شوید که گواهی عمومی شما در Google Wallet ثبت شده است. با تیم پشتیبانی ما از طریق wallet-identity-rp-support@google.com تماس بگیرید.

منطق ساخت درخواست

برای ساخت یک درخواست، باید از کلید خصوصی خود استفاده کنید و payload را در یک 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 های بومی ارسال خواهید کرد.

درون برنامه‌ای (اندروید)

برای درخواست اعتبارنامه هویت از برنامه‌های اندروید خود، این مراحل را دنبال کنید:

به‌روزرسانی وابستگی‌ها

در فایل build.gradle پروژه خود، وابستگی‌های خود را برای استفاده از Credential Manager (نسخه بتا) به‌روزرسانی کنید:

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)

درخواست ویژگی‌های هویت

به جای مشخص کردن پارامترهای تکی برای درخواست‌های هویت، برنامه همه آنها را به صورت یک رشته JSON در CredentialOption ارائه می‌دهد. مدیر اعتبارنامه این رشته JSON را بدون بررسی محتویات آن، به کیف پول‌های دیجیتال موجود ارسال می‌کند. سپس هر کیف پول مسئول موارد زیر است: - تجزیه رشته JSON برای درک درخواست هویت. - تعیین اینکه کدام یک از اعتبارنامه‌های ذخیره شده در آن، در صورت وجود، درخواست را برآورده می‌کند.

ما به شرکا توصیه می‌کنیم که درخواست‌های خود را حتی برای ادغام برنامه‌های اندروید، روی سرور ایجاد کنند.

شما از requestJson از Request Format به عنوان 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 تعریف شده است. برنامه Wallet مسئول ساخت این پاسخ است.

مثال:

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

شما این پاسخ را برای تأیید صحت آن به سرور ارسال خواهید کرد. می‌توانید مراحل تأیید اعتبار پاسخ را بیابید.

وب

برای درخواست اعتبارنامه‌های هویتی با استفاده از API اعتبارنامه‌های دیجیتال در کروم یا سایر مرورگرهای پشتیبانی‌شده، درخواست زیر را ارسال کنید.

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 تولید می‌کند.

مثال پایتون:

  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 مخصوص اندروید یا وب است:

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

    برای هر دو روش انتقال فایل اندروید و وب، باید از همان nonce که برای تولید credential_request استفاده کردید، استفاده کنید.

    تحویل اندروید

        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 اعتبارسنجی شود. این شامل چندین مرحله است، مانند:

  2. گواهی صادرکننده ایالتی را بررسی کنید. به گواهی‌های 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)

برای پشتیبانی از اثبات‌های دانش صفر (مثلاً تأیید بالای ۱۸ سال بودن کاربر بدون مشاهده تاریخ تولد دقیق او)، قالب درخواست خود را به 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
            {
              ...

شما یک اثبات دانش صفر رمزگذاری شده از کیف پول دریافت خواهید کرد. می‌توانید این اثبات را با استفاده از کتابخانه longfellow-zk گوگل در برابر گواهی‌های IACA صادرکنندگان اعتبارسنجی کنید.

سرویس تأییدکننده شامل یک سرور مبتنی بر داکر و آماده برای استقرار است که به شما امکان می‌دهد پاسخ را در برابر برخی از گواهی‌های IACA صادرکننده اعتبارسنجی کنید.

شما می‌توانید certs.pem را برای مدیریت گواهی‌های صادرکننده IACA که می‌خواهید به آنها اعتماد کنید، تغییر دهید.

منابع و پشتیبانی

  • پیاده‌سازی مرجع: پیاده‌سازی مرجع تأیید هویت ما را در گیت‌هاب بررسی کنید.
  • وب‌سایت آزمایشی: جریان سرتاسری را در verifier.multipaz.org امتحان کنید.
  • مشخصات OpenID4VP: مشخصات فنی openID4VP را بررسی کنید.
  • پشتیبانی: برای کمک در رفع اشکال یا سوالات در طول ادغام، با wallet-identity-rp-support@google.com تماس بگیرید.