במדריך הזה מוסבר איך צדדים מסתמכים (RP) יכולים לשלב טכנית את Digital Credentials API כדי לבקש ולאמת רישיונות נהיגה לנייד (mDL) וכרטיסי תעודה מ-Google Wallet באפליקציות ל-Android ובאינטרנט.
תהליך ההרשמה ודרישות הסף
לפני שמעלים את האפליקציה לסביבת הייצור, צריך לרשום אותה באופן רשמי ב-Google כצד נסמך.
- בדיקה בארגז חול: אפשר להתחיל בפיתוח מיד באמצעות סביבת ארגז החול ויצירת מזהה בדיקה. לא נדרש אישור של התנאים וההגבלות לצורך בדיקה.
- שליחת טופס פרטים ראשוניים: ממלאים את טופס ההצטרפות ל-RP. תהליך ההצטרפות נמשך בדרך כלל 3-5 ימי עסקים. שם המוצר והלוגו שלכם יוצגו במסך ההסכמה שמוצג למשתמשים, כדי לעזור להם לזהות מי מבקש את הנתונים שלהם.
- אישור התנאים וההגבלות: לפני שמתחילים שידור חי צריך לחתום על התנאים וההגבלות.
פורמטים ויכולות נתמכים
Google Wallet תומך בתעודות מזהות דיגיטליות שמבוססות על ISO mdoc.
- פרטי כניסה נתמכים: אפשר לעיין בפרטי הכניסה והמאפיינים הנתמכים.
- פרוטוקולים נתמכים: OpenID4VP (גרסה 1.0).
- גרסת Android SDK מינימלית: Android 9 (רמת API 28) ואילך.
- תמיכה בדפדפנים: רשימה מקיפה של דפדפנים שתומכים ב-Digital Credentials API זמינה בדף התמיכה במערכת האקולוגית.
בחירת הפורמט של הבקשה
כדי לבקש פרטי כניסה מכל ארנק, צריך לעצב את הבקשה באמצעות OpenID4VP. אפשר לבקש פרטי כניסה ספציפיים או כמה פרטי כניסה באובייקט dcql_query אחד.
דוגמה לבקשת JSON
הנה דוגמה לבקשת mdoc requestJson לקבלת פרטי זהות מכל ארנק במכשיר Android או באינטרנט.
{
"requests" : [
{
"protocol": "openid4vp-v1-signed",
"data": {<signed_credential_request>} // This is an object, shouldn't be a string.
}
]
}
בקשת הצפנה
התג client_metadata מכיל את המפתח הציבורי להצפנה של כל בקשה.
תצטרכו לאחסן מפתחות פרטיים לכל בקשה ולהשתמש בהם כדי לאמת ולאשר את הטוקן שאתם מקבלים מאפליקציית Wallet.
הפרמטר 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 Wallet.
בקשות חתומות
בקשות חתומות (בקשות הרשאה מאובטחות באמצעות JWT) כוללות את בקשת ההצגה הניתנת לאימות בתוך אסימון JWT (JSON Web Token) חתום קריפטוגרפית באמצעות תשתית ה-PKI שלכם, כדי להבטיח את שלמות הבקשה ולאמת את הזהות שלכם ב-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 המקוריים.
באפליקציה (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. אפליקציית Wallet אחראית ליצירת התגובה הזו.
דוגמה:
{
"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.
}
]
}
})
שולחים את התשובה מה-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 יניב קובץ JSON vp_token שמכיל את פרטי הכניסה
{
"vp_token":
{
"cred1": ["<base64UrlNoPadding_encoded_credential>"] // This applies to OpenID4VP 1.0 spec.
}
}
יצירת תמליל של הסשן
השלב הבא הוא ליצור את SessionTranscript מ-ISO/IEC 18013-5:2021 עם מבנה העברה ספציפי ל-Android או לאינטרנט:
SessionTranscript = [ null, // DeviceEngagementBytes not available null, // EReaderKeyBytes not available [ "OpenID4VPDCAPIHandover", AndroidHandoverDataBytes // BrowserHandoverDataBytes for Web ] ]גם בהעברה לאפליקציית Android וגם בהעברה לאתר, צריך להשתמש באותו מספר חד-פעמי שבו השתמשתם כדי ליצור את
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, יש לאמת את תגובת המכשיר בהתאם לסעיף 9 בתקן ISO/IEC 18013-5:2021. התהליך כולל כמה שלבים, כמו:בודקים את האישור של המנפיק הממלכתי. כאן אפשר לראות את רשימת המנפיקים הנתמכים של אישורי IACA.
אימות חתימת MSO (18013-5 Section 9.1.2)
חישוב של
ValueDigestsובדיקה שלו עבור רכיבי נתונים (18013-5 סעיף 9.1.2)אימות החתימה
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
{
...
הארנק ישלח בחזרה הוכחה מוצפנת של אפס ידע. אפשר לאמת את ההוכחה הזו מול אישורי IACA של מנפיקים באמצעות הספרייה longfellow-zk של Google.
השירות לאימות מכיל שרת מבוסס-Docker שמוכן לפריסה, שמאפשר לכם לאמת את התגובה מול אישורים מסוימים של רשות הנפקה של IACA.
אפשר לשנות את certs.pem כדי לנהל אישורי הנפקה של IACA שרוצים לתת בהם אמון.
מקורות מידע ותמיכה
- הטמעה לדוגמה: אפשר לעיין בהטמעה לדוגמה של מאמתים של זהויות ב-GitHub.
- בדיקת אתר: אפשר לנסות את התהליך מקצה לקצה בכתובת verifier.multipaz.org.
- מפרט OpenID4VP: אפשר לעיין במפרט הטכני של openID4VP.
- תמיכה: לקבלת עזרה בניפוי באגים או אם יש שאלות במהלך השילוב, אפשר לפנות אל
wallet-identity-rp-support@google.com.