Trabalhar com vereditos de integridade

A API Play Integrity usa vereditos de integridade para comunicar informações sobre a validade de dispositivos, apps e usuários. O servidor do app pode usar o payload resultante em um veredito descriptografado e verificado para determinar a melhor forma de prosseguir com uma ação ou solicitação específica no app.

Gerar um valor de uso único

.

Um valor de uso único é um ID da solicitação de uso único usado para verificar a integridade da mensagem. O ideal é que esse ID da solicitação esteja vinculado ao contexto em que ele é gerado, como o hash do ID do usuário ou um carimbo de data/hora.

Os valores de uso único precisam ser exclusivos e impossíveis de prever. Quando um usuário recebe um veredito de integridade para uma determinada ação protegida, é importante que esse veredito não possa ser reutilizado em ações de proteção subsequentes. Essa ação é conhecida como ataque de repetição e é impedida pelo uso do valor de uso único.

Para criar um valor de uso único, siga um destes procedimentos:

Escolha a técnica de geração de valor de uso único com base no valor da ação que está sendo protegida e da ameaça potencial ao app. É possível que o app cliente esteja sob o controle de um invasor. Por isso, é muito mais seguro gerar o valor de uso único de modo aleatório no lado do servidor, e não no lado do cliente.

Valor de uso único gerado pelo servidor

As etapas abaixo explicam como gerar valores de uso único usando um gerador de números aleatórios criptograficamente seguro em um ambiente de servidor seguro para cada solicitação de cliente recebida:

  1. O app informa o back-end de uma ação, como o registro de um novo usuário.
  2. O back-end do app gera um valor de uso único aleatório e exclusivo.
  3. O back-end do app adiciona um valor de uso único ou solicitação de pareamento à tabela de solicitações pendentes.
  4. O back-end do app solicita um token de integridade, transmitindo o valor de uso único com a solicitação.
  5. Depois de se comunicar com a API Play Integrity, o app transmite um token de resposta ao back-end. Em seguida, o token, que representa o veredito de integridade, é descriptografado e verificado. Normalmente, o back-end do app transmite o token aos servidores do Google Play para descriptografar e verificar o veredito. Em seguida, ele transmite o payload do token de volta ao back-end do app. O back-end do app também pode executar as etapas de descriptografia e verificação localmente.
  6. O back-end do app extrai o valor de uso único do payload descriptografado.
  7. O back-end do app verifica se o valor de uso único aparece em uma entrada da tabela de solicitações pendentes e se essa solicitação é correspondente.
  8. O back-end do app remove a entrada da tabela de solicitações pendentes.
  9. O back-end do app extrai o carimbo de data/hora do payload descriptografado e verifica se ele foi criado recentemente, dentro do período máximo permitido entre o momento da criação e o atual.
  10. O back-end do app extrai o nome do pacote do payload descriptografado e verifica se o nome do pacote no payload corresponde ao nome do pacote real do app.

A Figura 1 apresenta um diagrama de sequência que ilustra essas etapas.

O valor de uso único é exclusivo e impossível de prever. Portanto, essa abordagem oferece uma forte garantia de que não é possível repetir a solicitação.

Figura 1. Diagrama de sequência que mostra como gerar um valor de uso único do lado do servidor para ser usado com a API Play Integrity.

Valor de uso único gerado pelo cliente

Alguns clientes podem não conseguir reter todas as solicitações de entrada no lado do servidor ou não tolerar o atraso de uma viagem de ida e volta extra. Para esses casos, existe outra solução que abre mão de algumas das garantias de repetição para simplificar. Essa solução tem como base o hash dos parâmetros da solicitação e inclui um carimbo de data/hora:

  1. O app conclui uma ação, como registrar um novo usuário.
  2. O app gera um valor de uso único com um hash de parâmetros, como o horário atual, a fim de calcular um valor exclusivo para essa solicitação específica.
  3. O app solicita um token de integridade da API Play Integrity, transmitindo esse valor.
  4. A API Play Integrity responde com um token que representa o veredito de integridade.
  5. O app transmite o token com os parâmetros usados para gerar o valor de uso único ao back-end.
  6. O token é descriptografado e verificado. Normalmente, o back-end do app transmite o token aos servidores do Google Play para descriptografar e verificar o veredito. Em seguida, ele transmite o payload do token de volta ao back-end do app. O back-end do app também pode executar as etapas de descriptografia e verificação localmente.
  7. O back-end do app extrai o valor de uso único do payload descriptografado.
  8. O back-end do app calcula o hash dos parâmetros do lado do cliente fornecidos pelo aplicativo e verifica se esse valor calculado corresponde ao valor de uso único do payload descriptografado.
  9. O back-end do app extrai o carimbo de data/hora do payload descriptografado e verifica se ele foi criado recentemente, dentro do período máximo permitido entre o momento da criação e o atual.
  10. O back-end do app extrai o nome do pacote do payload descriptografado e verifica se o nome do pacote no payload corresponde ao nome do pacote real do app.

A Figura 2 apresenta um diagrama de sequência que ilustra essas etapas.

O carimbo de data/hora precisa ser incluído nos parâmetros da solicitação, de modo que cada valor de uso único seja exclusivo. Ainda assim, como o carimbo de data/hora e os parâmetros de solicitação são gerados no cliente e você não sabe se o cliente está sob o controle de um invasor considere que comportamentos adversos são possíveis.

Figura 2. Diagrama de sequência que mostra como gerar um valor de uso único do lado do cliente para ser usado com a API Play Integrity.

Solicitar um veredito de integridade

Após gerar um valor de uso único, é possível solicitar um veredito de integridade do Google Play. Para isso, siga estes passos:

  1. Crie um IntegrityManager, conforme mostrado no snippet de código abaixo.
  2. Use o gerenciador para chamar requestIntegrityToken(), fornecendo o valor de uso único com o método setNonce() no builder IntegrityTokenRequest associado.

Kotlin

// Receive the nonce from the secure server.
val nonce: String = ...

// Create an instance of a manager.
val integrityManager =
    IntegrityManagerFactory.create(applicationContext)

// Request the integrity token by providing a nonce.
val integrityTokenResponse: Task<IntegrityTokenResponse> =
    integrityManager.requestIntegrityToken(
        IntegrityTokenRequest.builder()
            .setNonce(nonce)
            .build())

// Allows your app to interpret error codes from the API.
integrityTokenResponse.addOnFailureListener(error -> ...)

Java

// Receive the nonce from the secure server.
String nonce = ...

// Create an instance of a manager.
IntegrityManager integrityManager =
    IntegrityManagerFactory.create(getApplicationContext());

// Request the integrity token by providing a nonce.
Task<IntegrityTokenResponse> integrityTokenResponse =
    integrityManager
      .requestIntegrityToken(
          IntegrityTokenRequest.builder().setNonce(nonce).build());

// Allows your app to interpret error codes from the API.
integrityTokenResponse.addOnFailureListener(error -> ...);

Unity

IEnumerator RequestIntegrityTokenCoroutine()
{
    // Receive the nonce from the secure server.
    var nonce = ...

    // Create an instance of a manager.
    var integrityManager = new IntegrityManager();

    // Request the integrity token by providing a nonce.
    var tokenRequest = new IntegrityTokenRequest(nonce);
    var requestIntegrityTokenOperation =
        integrityManager.RequestIntegrityToken(tokenRequest);

    // Wait for PlayAsyncOperation to complete.
    yield return requestIntegrityTokenOperation;

    // Check the resulting error code.
    if (requestIntegrityTokenOperation.Error != IntegrityErrorCode.NoError)
    {
        AppendStatusLog("IntegrityAsyncOperation failed with error: " +
                requestIntegrityTokenOperation.Error);
        yield break;
    }

    // Get the response.
    var tokenResponse = requestIntegrityTokenOperation.GetResult();
}

Nativo

/// Create an IntegrityTokenRequest opaque object.
const char* nonce = RequestNonceFromServer();
IntegrityTokenRequest* request;
IntegrityTokenRequest_create(&request);
IntegrityTokenRequest_setNonce(request, nonce);

/// Prepare an IntegrityTokenResponse opaque type pointer before calling
/// IntegerityManager_requestIntegrityToken().
IntegrityTokenResponse* response;
IntegrityErrorCode error_code = IntegrityManager_requestIntegrityToken(request, &response);

/// Proceed to polling iff error_code == INTEGRITY_NO_ERROR
if (error_code != INTEGRITY_NO_ERROR) {
    /// Remember to call the *_destroy() functions.
    return;
}

/// Use polling to wait for the async operation to complete.
IntegrityResponseStatus response_status;

/// Check for error codes.
IntegrityErrorCode error_code = IntegrityTokenResponse_getStatus(response, &response_status);
if (error_code == INTEGRITY_NO_ERROR && response_status == INTEGRITY_RESPONSE_COMPLETED) {
        const char* integrity_token = IntegrityTokenResponse_getToken(response);
        SendTokenToServer(integrity_token);
}

/// Remember to free up resources.
IntegrityTokenRequest_destroy(request);
IntegrityTokenResponse_destroy(response);
IntegrityManager_destroy();

Descriptografar e verificar o veredito de integridade

Ao solicitar um veredito de integridade, a API Play Integrity fornece um token de resposta assinado. O valor de uso único incluído na solicitação se torna parte do token de resposta.

Formato do token

O token é um Token da Web JSON (JWT, na sigla em inglês) aninhado, ou seja, Criptografia JSON da Web (JWE, na sigla em inglês) de Assinatura JSON da Web (JWS, na sigla em inglês). Os componentes da JWE e da JWS são representados usando a serialização compacta (links em inglês).

Os algoritmos de criptografia e assinatura têm suporte em várias implementações de JWT:

  • A JWE usa A256KW para alg e A256GCM para enc {: .external} (links em inglês)
  • A JWS usa ES256.

Descriptografar e verificar nos servidores do Google

A API Play Integrity permite descriptografar e verificar o veredito de integridade nos servidores do Google, o que melhora a segurança do app. Para fazer isso, siga estas etapas:

  1. Crie uma conta de serviço no projeto do Google Cloud que está vinculado ao app.
  2. No servidor do app, faça esta solicitação:

    playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \
      '{ "integrity_token": "INTEGRITY_TOKEN", "nonce": "NONCE"}'
  3. Leia a resposta JSON.

Descriptografar e verificar localmente

Caso você prefira gerenciar as chaves de API, é possível descriptografar e verificar o token retornado no seu próprio ambiente de servidor seguro. Você pode extrair o token retornado usando o método IntegrityTokenResponse#token().

O exemplo abaixo mostra como decodificar a chave AES e a chave pública EC codificada em DER a fim de verificar assinaturas do Play Console para chaves específicas da linguagem no back-end do app. Observe que as chaves são codificadas em base64 usando sinalizações padrão.

Kotlin

// base64OfEncodedDecryptionKey is provided through Play Console.
var decryptionKeyBytes: ByteArray = Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT)

// Deserialized encryption (symmetric) key.
var decryptionKey: SecretKey = SecretKeySpec(
    decryptionKeyBytes,
    /* offset= */ 0,
    AES_KEY_SIZE_BYTES,
    AES_KEY_TYPE
)

// base64OfEncodedVerificationKey is provided through Play Console.
var encodedVerificationKey: ByteArray =
    Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT)

// Deserialized verification (public) key.
var verificationKey: PublicKey = KeyFactory.getInstance(EC_KEY_TYPE)
    .generatePublic(X509EncodedKeySpec(encodedVerificationKey))

Java

// base64OfEncodedDecryptionKey is provided through Play Console.
byte[] decryptionKeyBytes =
    Base64.decode(base64OfEncodedDecryptionKey, Base64.DEFAULT);

// Deserialized encryption (symmetric) key.
SecretKey decryptionKey =
    new SecretKeySpec(
        decryptionKeyBytes,
        /* offset= */ 0,
        AES_KEY_SIZE_BYTES,
        AES_KEY_TYPE);

// base64OfEncodedVerificationKey is provided through Play Console.
byte[] encodedVerificationKey =
    Base64.decode(base64OfEncodedVerificationKey, Base64.DEFAULT);
// Deserialized verification (public) key.
PublicKey verificationKey =
    KeyFactory.getInstance(EC_KEY_TYPE)
        .generatePublic(new X509EncodedKeySpec(encodedVerificationKey));

Em seguida, use essas chaves para descriptografar o token de integridade (parte JWE) e, em seguida, verificar e extrair a parte JWS aninhada.

Kotlin

val jwe: JsonWebEncryption =
    JsonWebStructure.fromCompactSerialization(integrityToken) as JsonWebEncryption
jwe.setKey(decryptionKey)

// This also decrypts the JWE token.
val compactJws: String = jwe.getPayload()

val jws: JsonWebSignature =
    JsonWebStructure.fromCompactSerialization(compactJws) as JsonWebSignature
jws.setKey(verificationKey)

// This also verifies the signature.
val payload: String = jws.getPayload()

Java

JsonWebEncryption jwe =
 (JsonWebEncryption)JsonWebStructure.fromCompactSerialization(integrityToken);
jwe.setKey(decryptionKey);

// This also decrypts the JWE token.
String compactJws = jwe.getPayload();

JsonWebSignature jws =
    (JsonWebSignature) JsonWebStructure.fromCompactSerialization(compactJws);
jws.setKey(verificationKey);

// This also verifies the signature.
String payload = jws.getPayload();

O payload resultante é um token de texto simples que contém sinais de integridade.

Você também precisa verificar a parte requestDetails do payload JSON, garantindo que o valor de uso único e o nome do pacote correspondam ao que foi enviado na solicitação original:

Kotlin

val requestDetails: JSONObject =
    JSONObject(payload).getJSONObject("requestDetails")
val requestPackageName: String =
    requestDetails.getString("requestPackageName");
val nonce: String = requestDetails.getString("nonce");
val timestampMillis: Long = requestDetails.getLong("timestampMillis");
val currentTimestampMillis: Long = ...;

if (!requestPackageName.equals(expectedPackageName)
    // See "Generate nonce" section of the doc on how to store/compute
    // the expected nonce.
    || !nonce.equals(expectedNonce)
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
  // The token is invalid!
  ...
}

Java

JSONObject requestDetails =
    new JSONObject(payload).getJSONObject("requestDetails");
String requestPackageName =
    requestDetails.getString("requestPackageName");
String nonce = requestDetails.getString("nonce");
long timestampMillis = requestDetails.getLong("timestampMillis");
long currentTimestampMillis = ...;

if (!requestPackageName.equals(expectedPackageName)
    // See "Generate nonce" section of the doc on how to store/compute
    // the expected nonce.
    || !nonce.equals(expectedNonce)
    || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) {
  // The token is invalid!
  ...
}

Formato do payload retornado

O payload é JSON em texto simples e contém sinais de integridade e informações fornecidas pelo desenvolvedor.

A estrutura geral do payload é a seguinte:

{
  requestDetails: { ... }
  appIntegrity: { ... }
  deviceIntegrity: { ... }
  accountDetails: { ... }
}

As seções a seguir descrevem cada campo em mais detalhes.

Campo de detalhes da solicitação

O campo requestDetails contém informações fornecidas na solicitação, incluindo o valor de uso único. Esses valores precisam corresponder aos da solicitação original.

requestDetails: {
  // Application package name this attestation was requested for.
  // Note that this field might be spoofed in the middle of the
  // request.
  requestPackageName: "com.package.name"
  // base64-encoded web-safe no-wrap nonce provided by the developer.
  nonce: "aGVsbG8gd29scmQgdGhlcmU"
  // The timestamp in milliseconds when the request was made
  // (computed on the server).
  timestampMillis: 1617893780
}

Campo de integridade do aplicativo

O campo appIntegrity contém informações relacionadas ao pacote.

appIntegrity: {
  // PLAY_RECOGNIZED, UNRECOGNIZED_VERSION, or UNEVALUATED.
  appRecognitionVerdict: "PLAY_RECOGNIZED"
  // The package name of the app.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  packageName: "com.package.name"
  // The sha256 digest of app certificates on device.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  certificateSha256Digest: ["6a6a1474b5cbbb2b1aa57e0bc3"]
  // The version of the app.
  // This field is populated iff appRecognitionVerdict != UNEVALUATED.
  versionCode: 42
}

appRecognitionVerdict pode ter estes valores:

PLAY_RECOGNIZED
O app e o certificado correspondem às versões distribuídas pelo Google Play.
UNRECOGNIZED_VERSION
O nome do certificado ou do pacote não corresponde aos registros do Google Play.
UNEVALUATED
A integridade do aplicativo não foi avaliada. Um requisito necessário está ausente, por exemplo, o dispositivo não é confiável o suficiente.

Campo de integridade do dispositivo

O campo deviceIntegrity contém um único valor, device_recognition_verdict, que representa a forma como um dispositivo pode garantir a integridade do app.

deviceIntegrity: {
  // "MEETS_DEVICE_INTEGRITY" is one of several possible values.
  deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]
}

Por padrão, device_recognition_verdict pode ter um destes identificadores:

MEETS_DEVICE_INTEGRITY
O app está sendo executado em um dispositivo Android com o Google Play Services. O dispositivo foi aprovado nas verificações de integridade do sistema e atende aos requisitos de suporte do Android.
Nenhum rótulo (um valor em branco)
O app está sendo executado em um dispositivo que tem sinais de ataque, como hooks de API, ou comprometimentos do sistema, como acesso root, ou o app não está sendo executado em um dispositivo físico, como um emulador, que não é aprovado nas verificações de integridade do Google Play.

Se você aceitar receber outros identificadores no veredito de integridade, o campo device_recognition_verdict poderá ter estes identificadores:

MEETS_BASIC_INTEGRITY
O app está sendo executado em um dispositivo que é aprovado nas verificações básicas de integridade do sistema. O dispositivo pode não atender aos requisitos de compatibilidade do Android e não ser aprovado para executar o Google Play Services. Por exemplo, o dispositivo pode estar executando uma versão não reconhecida do Android, ter um carregador de inicialização desbloqueado ou não ter sido certificado pelo fabricante.
MEETS_STRONG_INTEGRITY
O app está sendo executado em um dispositivo Android que usa o Google Play Services e tem uma forte garantia de integridade do sistema, como um keystore protegido por hardware. O dispositivo é aprovado nas verificações de integridade do sistema e atende aos requisitos de compatibilidade do Android.

Para ajudar a receber os vereditos pretendidos ao testar a precisão dos identificadores de vereditos, verifique se as condições abaixo foram atendidas nos dispositivos de teste:

  1. A depuração USB está desativada.
  2. O carregador de inicialização está bloqueado.

Saiba mais sobre como criar testes da API Play Integrity para seu app.

Campo de detalhes da conta

O campo accountDetails contém um único valor, licensingVerdict, que representa o status de licenciamento/titularidade do app.

accountDetails: {
  // This field can be LICENSED, UNLICENSED, or UNEVALUATED.
  licensingVerdict: "LICENSED"
}

licensingVerdict pode ter estes valores:

LICENSED
O usuário tem titularidade do app. Em outras palavras, o usuário instalou ou comprou seu app no Google Play.
UNLICENSED
O usuário não tem a titularidade do app. Isso acontece quando, por exemplo, o usuário transfere o app por sideload ou não o adquire do Google Play.
UNEVALUATED

Os detalhes de licenciamento não foram avaliados porque um requisito necessário está ausente.

Isso pode acontecer por vários motivos, incluindo:

  • O dispositivo não é confiável o suficiente.
  • A versão do app instalada no dispositivo é desconhecida para o Google Play.