Aceptación en línea de credenciales digitales

En esta guía, se explica cómo las partes que confían (RP) pueden integrar técnicamente la API de Digital Credentials para solicitar y validar licencias de conducir (mDL) y pases de ID móviles desde la Billetera de Google en apps para Android y la Web.

Proceso de registro y requisitos previos

Antes de publicar tu aplicación de entidad de confianza en producción, debes registrarla formalmente en Google.

  1. Prueba en la zona de pruebas: Puedes comenzar el desarrollo de inmediato con nuestro entorno de zona de pruebas y cómo crear un ID de prueba. No es necesario aceptar las Condiciones del Servicio para realizar pruebas.
  2. Envía el formulario de admisión: Completa el formulario de incorporación de RP. Por lo general, la incorporación demora entre 3 y 5 días hábiles. El nombre y el logotipo de tu producto se mostrarán en la pantalla de consentimiento visible para el usuario para ayudarlo a identificar quién solicita sus datos.
  3. Acepta las Condiciones del Servicio: Debes firmar las Condiciones del Servicio antes de publicar tu canal.

Formatos y capacidades compatibles

La Billetera de Google admite documentos de identidad digitales basados en mdoc según la norma ISO.

Cómo dar formato a la solicitud

Para solicitar credenciales de cualquier billetera, debes darle formato a tu solicitud con OpenID4VP. Puedes solicitar credenciales específicas o varias credenciales en un solo objeto dcql_query.

Ejemplo de solicitud en JSON

A continuación, se muestra un ejemplo de una solicitud de mdoc requestJson para obtener credenciales de identidad de cualquier billetera en un dispositivo Android o en la Web.

{
      "requests" : [
        {
          "protocol": "openid4vp-v1-signed",
          "data": {<signed_credential_request>} // This is an object, shouldn't be a string.
        }
      ]
}

Solicitar encriptación

El objeto client_metadata contiene la clave pública de encriptación para cada solicitud. Deberás almacenar claves privadas para cada solicitud y usarlas para autenticar y autorizar el token que recibas de la app de la billetera.

El parámetro credential_request en requestJson contiene los siguientes campos.

Credencial específica

{
  "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
        ]
      }
    }
  }
}

Cualquier credencial apta

A continuación, se muestra la solicitud de ejemplo para el pase de mDL y de ID. El usuario puede continuar con cualquiera de las dos opciones.

{
  "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
        ]
      }
    }
  }
}

Puedes solicitar la cantidad de atributos admitidos que desees de cualquier credencial de identidad almacenada en la Billetera de Google.

Solicitudes firmadas

Las solicitudes firmadas (solicitudes de autorización protegidas con JWT) encapsulan tu solicitud de presentación verificable dentro de un token web JSON (JWT) firmado de forma criptográfica con tu infraestructura de PKI, lo que garantiza la integridad de la solicitud y demuestra tu identidad ante la Billetera de Google.

Requisitos previos

Antes de implementar los cambios de código para la solicitud firmada, asegúrate de tener lo siguiente:

  • Clave privada: Necesitas una clave privada (p.ej., curva elíptica ES256) para firmar la solicitud que se administra en tu servidor.
  • Certificado: Necesitas un certificado X.509 estándar derivado de tu par de claves.
  • Registro: Asegúrate de que tu certificado público esté registrado en la Billetera de Google. Comunícate con nuestro equipo de asistencia al cliente al wallet-identity-rp-support@google.com

Lógica de construcción de solicitudes

Para construir una solicitud, debes usar tu clave privada y envolver la carga útil en un 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

Activa la API

Toda la solicitud a la API se debe generar del lado del servidor. Según la plataforma, pasarás el JSON generado a las APIs nativas.

En la aplicación (Android)

Para solicitar credenciales de identidad desde tus apps para Android, sigue estos pasos:

Actualiza las dependencias

En el archivo build.gradle de tu proyecto, actualiza las dependencias para usar el Administrador de credenciales (beta):

dependencies {
    implementation("androidx.credentials:credentials:1.5.0-beta01")
    implementation("androidx.credentials:credentials-play-services-auth:1.5.0-beta01")
}

Cómo configurar el Administrador de credenciales

Para configurar e inicializar un objeto CredentialManager, agrega una lógica similar a la siguiente:

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

Solicita atributos de identidad

En lugar de especificar parámetros individuales para las solicitudes de identidad, la app los proporciona todos juntos como una cadena JSON dentro de CredentialOption. El Administrador de credenciales pasa esta cadena JSON a las billeteras digitales disponibles sin examinar su contenido. Luego, cada billetera es responsable de lo siguiente: - Analizar la cadena JSON para comprender la solicitud de identidad - Determinar cuáles de sus credenciales almacenadas, si las hay, satisfacen la solicitud

Recomendamos a los socios que creen sus solicitudes en el servidor, incluso para las integraciones de apps para Android.

Usarás el requestJson de Formato de la solicitud como el request en la llamada a la función 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)
    }
}

Cómo controlar la respuesta de credenciales

Una vez que recibas una respuesta de la billetera, verificarás si la respuesta es correcta y contiene la respuesta 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}")
    }
}

La respuesta credentialJson contiene un identityToken (JWT) encriptado, definido por el W3C. La app de Wallet es responsable de elaborar esta respuesta.

Ejemplo:

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

Pasarás esta respuesta al servidor para validar su autenticidad. Puedes encontrar los pasos para validar la respuesta de credenciales

Web

Para solicitar credenciales de identidad con la API de Digital Credentials en Chrome o en otros navegadores compatibles, realiza la siguiente solicitud.

const credentialResponse = await navigator.credentials.get({
          digital : {
          requests : [
            {
              protocol: "openid4vp-v1-signed",
              data: {<credential_request>} // This is an object, shouldn't be a string.
            }
          ]
        }
      })

Envía la respuesta de esta API a tu servidor para validar la respuesta de credenciales.

Valida la respuesta

Una vez que la billetera devuelva el identityToken (JWT) encriptado, debes realizar una validación estricta del lado del servidor antes de confiar en los datos.

Cómo desencriptar la respuesta

Usa la clave privada correspondiente a la clave pública enviada en el client_metadata de la solicitud para desencriptar el JWE. Esto genera un vp_token.

Ejemplo de 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 generará un JSON de vp_token que contendrá la credencial.

  {
    "vp_token":
    {
      "cred1": ["<base64UrlNoPadding_encoded_credential>"] // This applies to OpenID4VP 1.0 spec.
    }
  }
  1. Crea la transcripción de la sesión

    El siguiente paso es crear el objeto SessionTranscript a partir de ISO/IEC 18013-5:2021 con una estructura de transferencia específica para Android o la Web:

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

    Para las transferencias tanto en Android como en la Web, deberás usar el mismo nonce que usaste para generar credential_request.

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

    Transferencia del navegador

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

    Con el uso de SessionTranscript, la respuesta del dispositivo se debe validar según la cláusula 9 de ISO/IEC 18013-5:2021. Esto incluye varios pasos, como los siguientes:

  2. Verifica el certificado de la entidad emisora del estado. Consulta los certificados de IACA de la entidad emisora admitida.

  3. Verifica la firma del MSO (sección 9.1.2 de 18013-5)

  4. Cómo calcular y verificar ValueDigests para los elementos de datos (sección 9.1.2 de ISO 18013-5)

  5. Verifica la firma de deviceSignature (sección 9.1.3 de 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
}

Verificación de edad que preserva la privacidad (ZKP)

Para admitir pruebas de conocimiento cero (p.ej., verificar que un usuario sea mayor de 18 años sin ver su fecha de nacimiento exacta), cambia el formato de tu solicitud a mso_mdoc_zk y proporciona la configuración zk_system_type requerida.

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

Recibirás una prueba de conocimiento cero encriptada de la billetera. Puedes validar esta prueba con los certificados de IACA de las entidades emisoras a través de la biblioteca longfellow-zk de Google.

El verifier-service contiene un servidor basado en Docker y listo para la implementación que te permite validar la respuesta con ciertos certificados de IACA de la entidad emisora.

Puedes modificar certs.pem para administrar los certificados de la entidad emisora de la IACA en los que deseas confiar.

Recursos y asistencia