במדריך הזה מוסבר איך צדדים מסתמכים (Relying Parties, RP) יכולים לשלב טכנית את Digital Credentials API כדי לבקש ולאמת פרטי זהות דיגיטליים באפליקציות ל-Android ובאינטרנט.
תהליך ההרשמה ודרישות מוקדמות
לפני שמעבירים את האפליקציה לסביבת הייצור, צריך לרשום אותה באופן רשמי ב-Google כצד נסמך. אנחנו משתמשים בתהליך חתימה על אישור שבו Google חותמת על האישור שלכם כדי לבסס אמון.
- בדיקה בסביבת Sandbox: אפשר להתחיל בפיתוח באופן מיידי בלי לשלוח טופס בקשה. אפשר לבדוק ישירות באמצעות מפתחות הבדיקה המהימנים מראש ומטא נתונים לדוגמה שפורסמו בדף מצב Sandbox.
- תיעוד של זרימת העבודה מקצה לקצה: אחרי שסיימתם את הבדיקות ויש לכם שילוב תקין בסביבת Sandbox, צריך לתעד סרטון מקצה לקצה שמציג את כל זרימת העבודה של השילוב.
- שליחת טופס קליטה ואישור התנאים וההגבלות: ממלאים את טופס ההצטרפות של צד מסתמך ושולחים אותו.
כדי למלא את הטופס, צריך לציין את הפרטים הבאים:
- בקשת החתימה על אישור (CSR) של סביבת הייצור.
- נכסי התצוגה שלכם: כתובת ה-URL של הלוגו, שם התצוגה, כתובת ה-URL של מדיניות הפרטיות וכתובת ה-URL של התנאים וההגבלות.
- סרטון מקצה לקצה של השילוב בארגז החול.
אחרי האישור, Google תספק לכם אישור חתום ואת המטא-נתונים הייחודיים שלכם בקידוד Base64URL (gw_rp_metadata_bytes).
פרטים טכניים של השילוב
בקטעים הבאים מפורטים פרטים טכניים על שילוב של Relying Parties ישירות עם Digital Credentials API (כולל פורמט הבקשה, הצפנת הבקשה, הפעלת ה-API, אימות התשובות והטמעה של Zero-Knowledge Proofs).
פורמטים ויכולות נתמכים
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.
מטא-נתונים משולבים של OpenID4VP
כשמגדירים את הבקשה לאישור פרטי הכניסה, צריך לכלול את השדה gw_rp_metadata_bytes בתוך האובייקט client_metadata (כפי שמוצג בקוד הבקשה לדוגמה שלמטה). השדה הזה מכיל את המטא-נתונים של הצד המסתמך בקידוד Base64URL, שנדרשים ל-Google 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
],
"issuerauth_alg_values": [
-7
]
}
},
"gw_rp_metadata_bytes": "<base64url encoded metadata string>"
}
}
כל פרטי כניסה שעומדים בדרישות
הנה דוגמה לבקשה עבור 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
],
"issuerauth_alg_values": [
-7
]
}
},
"gw_rp_metadata_bytes": "<base64url encoded metadata string>"
}
}
אתם יכולים לבקש כל מספר של מאפיינים נתמכים מכל מסמך מזהה ששמור ב-Google Wallet.
בקשות חתומות
בקשות חתומות (בקשות הרשאה מאובטחות באמצעות JWT) כוללות את בקשת ההצגה הניתנת לאימות בתוך אסימון אינטרנט מסוג JSON (JWT) שחתום קריפטוגרפית באמצעות תשתית ה-PKI שלכם. כך מובטחת שלמות הבקשה ומוכח הזיהוי שלכם ב-Google Wallet.
דרישות מוקדמות
לפני שמטמיעים את שינויי הקוד לבקשה חתומה, צריך לוודא שיש לכם:
- מפתח פרטי: צריך מפתח פרטי (למשל, Elliptic Curve
ES256) כדי לחתום על הבקשה שמנוהלת בשרת. - אישור: נדרש אישור X.509 רגיל שנגזר מזוג המפתחות שלכם.
- הרשמה: מוודאים שהאישור הציבורי שלכם רשום ב-Google Wallet.
לוגיקה של יצירת בקשות
כדי ליצור בקשה, צריך להשתמש במפתח הפרטי ולעטוף את מטען הייעודי (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 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. אפליקציית Google 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.תהליך האימות הזה כולל כמה שלבים:
בדיקת אישור המנפיק: מחלצים את שרשרת אישורי החתימה של המנפיק מ-
issuerAuthומאמתים אותה מול אישורי הבסיס המהימנים של IACA. כאן אפשר לראות את רשימת המנפיקים הנתמכים של אישורי 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.
סקירה כללית של ZKP והיכולות שלה מופיעה בשאלות הנפוצות.
...
"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.