Aceitação on-line de credenciais digitais

Este guia explica como as partes confiáveis (RPs, na sigla em inglês) podem integrar tecnicamente a API Digital Credentials para solicitar e validar credenciais de identidade digital em apps Android e na Web.

Processo de inscrição e pré-requisitos

Antes de entrar em produção, você precisa registrar formalmente seu aplicativo Relying Party no Google. Usamos um processo de assinatura de certificado em que o Google assina seu certificado para estabelecer confiança.

  1. Teste no sandbox:você pode começar o desenvolvimento imediatamente sem enviar um formulário de admissão. Você pode testar diretamente usando as chaves de teste pré-confiáveis e os metadados de amostra publicados na página do Modo sandbox.
  2. Grave o fluxo E2E:depois de concluir os testes e ter uma integração funcional no sandbox, grave um vídeo de ponta a ponta mostrando todo o fluxo de integração.
  3. Envie o formulário de entrada e aceite os Termos de Serviço:preencha e envie o formulário de integração de partes confiáveis.

O formulário exige as seguintes informações:

  • Sua solicitação de assinatura de certificado (CSR) de produção.
  • Seus recursos de exibição: URL do logotipo, nome de exibição, URL da Política de Privacidade e URL dos Termos de Serviço.
  • O vídeo completo da integração do sandbox.

Depois da aprovação, o Google vai fornecer um certificado assinado e seus metadados exclusivos codificados em Base64URL (gw_rp_metadata_bytes).

Detalhes da integração técnica

As seções a seguir abordam os detalhes técnicos da integração para partes confiáveis que se integram diretamente à API Digital Credentials, incluindo formatação e criptografia de solicitações, acionamento da API, validação de respostas e implementação de provas de conhecimento zero.

Formatos e recursos compatíveis

A Carteira do Google aceita documentos de identificação digitais baseados no mdoc ISO.

Formatar a solicitação

Para solicitar credenciais de qualquer carteira, formate sua solicitação usando OpenID4VP. É possível solicitar credenciais específicas ou várias credenciais em um único objeto dcql_query.

Exemplo de solicitação JSON

Confira um exemplo de solicitação de mdoc requestJson para receber credenciais de identidade de qualquer carteira em um dispositivo Android ou na Web.

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

Solicitar criptografia

O client_metadata contém a chave pública de criptografia para cada solicitação. Você precisa armazenar chaves privadas para cada solicitação e usá-las para autenticar e autorizar o token recebido do app carteira.

Metadados integrados do OpenID4VP

Ao formatar a solicitação de credencial, inclua o campo gw_rp_metadata_bytes no objeto client_metadata, conforme mostrado no código de solicitação de exemplo abaixo. Esse campo contém os metadados da parte confiável codificados em Base64URL necessários para a Carteira do Google verificar sua identidade e mostrar sua marca ao usuário.

O parâmetro credential_request em requestJson contém os seguintes campos.

Qualificações específicas

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

Qualquer credencial qualificada

Confira um exemplo de solicitação para mDL e passe de identificação. O usuário pode continuar com qualquer uma das opções.

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

Você pode solicitar qualquer número de atributos compatíveis de qualquer credencial de identidade armazenada na Carteira do Google.

Solicitações assinadas

Os pedidos assinados (solicitações de autorização protegidas por JWT) encapsulam sua solicitação de apresentação verificável em um JSON Web Token (JWT) assinado criptograficamente usando sua infraestrutura de PKI, garantindo a integridade da solicitação e comprovando sua identidade para a Carteira do Google.

.

Pré-requisitos

Antes de implementar as mudanças de código para solicitação assinada, verifique se você tem:

  • Chave privada:você precisa de uma chave privada (por exemplo, curva elíptica ES256) para assinar a solicitação gerenciada no seu servidor.
  • Certificado:você precisa de um certificado X.509 padrão derivado do seu par de chaves.
  • Registro:verifique se o certificado público está registrado na Carteira do Google.

Lógica de construção de solicitações

Para construir uma solicitação, use sua chave privada e encapsule o payload em uma 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

Acionar a API

Toda a solicitação de API precisa ser gerada do lado do servidor. Dependendo da plataforma, você vai transmitir o JSON gerado para as APIs da plataforma.

No app (Android)

Para solicitar credenciais de identidade dos seus apps Android, siga estas etapas:

Atualizar dependências

No build.gradle do projeto, atualize as dependências para usar o Credential Manager (beta):

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

Configurar o Gerenciador de credenciais

Para configurar e inicializar um objeto CredentialManager, adicione uma lógica parecida com esta:

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

Solicitar atributos de identidade

Em vez de especificar parâmetros individuais para solicitações de identidade, o app fornece todos eles juntos como uma string JSON dentro do CredentialOption. O Credential Manager transmite essa string JSON para as carteiras digitais disponíveis sem examinar o conteúdo dela. Cada carteira é responsável por: - Analisar a string JSON para entender a solicitação de identidade. - Determinar quais das credenciais armazenadas, se houver, atendem à solicitação.

Recomendamos que os parceiros criem as solicitações no servidor, mesmo para integrações de apps Android.

Você vai usar o requestJson do Formato da solicitação como o request na chamada da função 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)
    }
}

Processar a resposta de credencial

Depois de receber uma resposta da carteira, verifique se ela foi bem-sucedida e contém a resposta 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}")
    }
}

A resposta credentialJson contém um identityToken (JWT) criptografado, definido pelo W3C. O app Carteira é responsável por criar essa resposta.

Exemplo:

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

Você vai transmitir essa resposta de volta ao servidor para validar a autenticidade dela. Confira as etapas para validar a resposta de credencial.

Web

Para solicitar credenciais de identidade usando a API Digital Credentials no Chrome ou em outros navegadores compatíveis, faça a seguinte solicitação.

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

Envie a resposta dessa API de volta ao servidor para validar a resposta de credencial.

Validar a resposta

Depois que a carteira retornar o identityToken (JWT) criptografado, você precisará realizar uma validação estrita do lado do servidor antes de confiar nos dados.

Descriptografar a resposta

Use a chave privada correspondente à chave pública enviada no client_metadata da solicitação para descriptografar o JWE. Isso gera um vp_token.

Exemplo em 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 vai resultar em um JSON vp_token que contém a credencial

  {
    "vp_token":
    {
      "cred1": ["<base64UrlNoPadding_encoded_credential>"] // This applies to OpenID4VP 1.0 spec.
    }
  }
  1. Criar a transcrição da sessão

    A próxima etapa é criar o SessionTranscript do ISO/IEC 18013-5:2021 com uma estrutura de transferência específica para Android ou Web:

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

    Para transferências no Android e na Web, use o mesmo nonce que você usou para gerar credential_request.

    Transferência do 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()
        

    Transferência de 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()
        

    Usando o SessionTranscript, a resposta do dispositivo precisa ser validada de acordo com a cláusula 9 da norma ISO/IEC 18013-5:2021.

    Essa validação inclui várias etapas:

  2. Verifique o certificado do emissor:extraia a cadeia de certificados de assinatura do emissor de issuerAuth e valide-a em relação aos certificados raiz confiáveis da IACA. Consulte os certificados IACA do emissor compatível.

  3. Verificar assinatura da MSO (seção 9.1.2 da 18013-5)

  4. Calcular e verificar ValueDigests para elementos de dados (18013-5, seção 9.1.2)

  5. Verificar a assinatura deviceSignature (seção 9.1.3 da norma 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
}

Verificação de idade com preservação da privacidade (ZKP)

Para oferecer suporte a provas de conhecimento zero (por exemplo, verificar se um usuário tem mais de 18 anos sem ver a data de nascimento exata), mude o formato da solicitação para mso_mdoc_zk e forneça a configuração zk_system_type necessária.

Para uma visão geral de alto nível do que é ZKP e das funcionalidades dele, consulte as perguntas frequentes.

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

Você vai receber uma prova de conhecimento zero criptografada da carteira. É possível validar essa prova em relação aos certificados IACA dos emissores usando a biblioteca longfellow-zk do Google.

O verifier-service contém um servidor baseado em Docker pronto para implantação que permite validar a resposta em relação a determinados certificados IACA do emissor.

Você pode modificar o certs.pem para gerenciar os certificados do emissor da IACA em que você quer confiar.

Recursos e suporte