Envoyer une requête API classique

Si vous prévoyez de n'effectuer que des requêtes API standards, qui conviennent à la majorité des développeurs, vous pouvez passer directement à la section sur les évaluations de l'intégrité. Cette page explique comment effectuer des requêtes API classiques pour des évaluations de l'intégrité sous Android 4.4 (niveau d'API 19) ou version ultérieure.

Points à prendre en compte

Comparer les requêtes standards et classiques

Vous pouvez effectuer des requêtes standards, des requêtes classiques ou une combinaison des deux, en fonction des besoins de votre application en termes de sécurité et de lutte contre les utilisations abusives. Les requêtes standards conviennent à toutes les applications et à tous les jeux. Elles peuvent être utilisées pour vérifier que les actions ou les appels de serveur sont authentiques, tout en déléguant à Google Play la protection contre la rejouabilité et l'exfiltration. Les requêtes classiques sont plus coûteuses à effectuer. Vous êtes responsable de leur mise en œuvre afin de vous protéger contre l'exfiltration et certains types d'attaques. Les requêtes classiques devraient être effectuées moins fréquemment que les requêtes standards, par exemple pour vérifier de temps en temps si une action particulièrement intéressante ou sensible est légitime.

Le tableau suivant met en évidence les principales différences entre ces deux types de requêtes :

Requête API standard Requête API classique
Conditions préalables
Version minimale du SDK Android requise Android 5.0 (niveau d'API 21) ou version ultérieure Android 4.4 (niveau d'API 19) ou version ultérieure
Configuration requise pour Google Play Google Play Store et services Google Play Google Play Store et services Google Play
Détails de l'intégration
Préparation de l'API requise ✔️ (quelques secondes)
Latence habituelle des requêtes Quelques centaines de millisecondes Quelques secondes
Fréquence des requêtes potentielles Fréquentes (vérification à la demande pour toute action ou requête) Occasionnelles (vérification ponctuelle des actions les plus intéressantes ou des requêtes les plus sensibles)
Délais d'inactivité La plupart des préparations durent moins de 10 secondes, mais ils impliquent un appel au serveur. Il est donc recommandé de définir un délai d'inactivité long (par exemple, une minute). Les requêtes d'évaluation sont envoyées côté client La plupart des requêtes durent moins de 10 secondes, mais elles impliquent un appel au serveur. Il est donc recommandé de définir un délai d'inactivité long (par exemple, une minute).
Jeton d'évaluation de l'intégrité
Contient des informations sur l'appareil, l'application et le compte ✔️ ✔️
Mise en cache des jetons Mise en cache protégée sur l'appareil par Google Play Non recommandé
Déchiffrer et valider le jeton via le serveur Google Play ✔️ ✔️
Latence habituelle des requêtes de déchiffrement de serveur à serveur Dizaines de millisecondes avec disponibilité à 99,9 % Dizaines de millisecondes avec disponibilité à 99,9 %
Déchiffrer et valider le jeton localement dans un environnement de serveur sécurisé ✔️
Déchiffrer et vérifier le jeton côté client
Actualisation des évaluations de l'intégrité Mise en cache et actualisation automatiques par Google Play Toutes les évaluations sont recalculées à chaque requête
Limites
Requêtes par application et par jour 10 000 par défaut (une augmentation peut être demandée) 10 000 par défaut (une augmentation peut être demandée)
Requêtes par instance d'application et par minute Préparation : 5 par minute
Jetons d'intégrité : pas de limite publique*
Jetons d'intégrité : 5 par minute
Protection
Atténuer les risques de falsification et d'attaques similaires Utiliser le champ requestHash Utiliser le champ nonce avec une liaison de contenu basée sur les données de la requête
Atténuer les risques de rejeu et d'attaques similaires Atténuation automatique par Google Play Utiliser le champ nonce avec la logique côté serveur

* Toutes les requêtes, y compris celles sans limite publique, sont soumises à limites défensives non publiques à des valeurs élevées.

Limiter la fréquence des requêtes classiques

La génération d'un jeton d'intégrité nécessite du temps, utilise des données et consomme de la batterie, et chaque application est soumise à un nombre maximal de requêtes classiques par jour. Par conséquent, vous ne devez émettre des requêtes classiques que pour vérifier que les actions les plus sensibles ou à très forte valeur ajoutée sont authentiques lorsque vous souhaitez obtenir une garantie supplémentaire par rapport à une requête standard. Évitez les requêtes classiques pour les actions à haute fréquence ou à faible valeur ajoutée. N'effectuez pas de requêtes classiques chaque fois que l'application est exécutée au premier plan ni toutes les cinq ou dix minutes en arrière-plan. Évitez également de passer des appels depuis un grand nombre d'appareils en même temps. Toute application effectuant trop d'appels de requêtes classiques peut être limitée afin de protéger les utilisateurs contre les implémentations incorrectes.

Éviter la mise en cache des évaluations

La mise en cache d'une évaluation augmente le risque d'attaques, telles que l'exfiltration et la rejeu, où une bonne évaluation est réutilisée depuis un environnement non fiable. Si vous envisagez d'envoyer une requête classique, puis que vous la mettez en cache pour une utilisation ultérieure, nous vous recommandons d'effectuer une requête standard à la demande. Les requêtes standards impliquent une mise en cache sur l'appareil, mais Google Play utilise des techniques de protection supplémentaires pour atténuer les risques d'attaques par rejeu et d'exfiltration.

Utiliser le champ "nonce" pour protéger les requêtes classiques

L'API Play Integrity propose un champ nommé nonce, qui peut être utilisé pour mieux protéger votre application contre certaines attaques, telles que les attaques par rejeu et la falsification. Elle renvoie la valeur que vous avez définie dans ce champ, dans la réponse d'intégrité signée. Pour protéger votre application contre les attaques, suivez attentivement les consignes de génération des nonces.

Réessayer les requêtes classiques avec un intervalle exponentiel entre les tentatives

Les conditions environnementales, telles qu'une connexion Internet instable ou un appareil surchargé, peuvent entraîner l'échec des vérifications de l'intégrité de l'appareil. Par conséquent, aucun libellé n'est généré pour un appareil digne de confiance. Pour pallier ce problème, veillez à permettre les nouvelles tentatives avec un intervalle exponentiel entre chacune d'elles.

Présentation

Schéma séquentiel illustrant la conception générale de l'API Play Integrity

Lorsque l'utilisateur effectue dans votre application une action à forte valeur ajoutée que vous souhaitez protéger à l'aide d'une vérification de l'intégrité, suivez ces étapes :

  1. Le backend côté serveur de votre application génère et envoie une valeur unique à la logique côté client. Les étapes restantes désignent cette logique comme étant votre "application".
  2. Votre application crée le nonce à partir de la valeur unique et du contenu de votre action à forte valeur ajoutée. Elle appelle ensuite l'API Play Integrity, et transmet le nonce.
  3. L'application reçoit un verdict signé et chiffré de l'API Play Integrity.
  4. L'application transmet le verdict signé et chiffré à son backend.
  5. Le backend de votre application envoie le résultat à un serveur Google Play. Le serveur Google Play déchiffre et vérifie le résultat, puis renvoie les résultats au backend de votre application.
  6. Le backend de votre application décide de la marche à suivre en fonction des signaux qui se trouvent dans la charge utile du jeton.
  7. Le backend de votre application envoie les résultats de la décision à votre application.

Générer un nonce

Lorsque vous protégez une action dans votre application avec l'API Play Integrity, vous pouvez exploiter le champ nonce pour atténuer certains types d'attaques, tels que les attaques de l'intercepteur par rejeu ou par falsification. Elle renvoie la valeur que vous avez définie dans ce champ, dans la réponse d'intégrité signée.

La valeur définie dans le champ nonce doit être correctement formatée :

  • String
  • Sécurisée pour les URL
  • Encodée en base64 et non encapsulée
  • 16 caractères minimum
  • 500 caractères maximum

Voici quelques façons courantes d'utiliser le champ nonce dans l'API Play Integrity. Pour bénéficier d'une protection optimale du nonce, vous pouvez combiner les méthodes ci-dessous.

Inclure un hachage de requête pour vous protéger contre la falsification

Vous pouvez utiliser le paramètre nonce dans une requête API classique de la même manière que le paramètre requestHash dans une requête API standard afin de protéger le contenu d'une requête contre la falsification.

Lorsque vous demandez une évaluation de l'intégrité, procédez comme suit :

  1. Calculez un récapitulatif de tous les paramètres de requête critiques (par exemple, SHA256 d'une sérialisation de requête stable) à partir de l'action de l'utilisateur ou de la requête de serveur en cours.
  2. Utilisez setNonce pour définir le champ nonce sur la valeur du récapitulatif calculé.

Lorsque vous recevez une évaluation de l'intégrité, procédez comme suit :

  1. Décodez et vérifiez le jeton d'intégrité, puis obtenez le récapitulatif du champ nonce.
  2. Calculez un récapitulatif de la requête de la même manière que dans l'application (par exemple, SHA256 d'une sérialisation de requête stable).
  3. Comparez les récapitulatifs côté application et côté serveur. S'ils ne correspondent pas, la requête n'est pas fiable.

Inclure des valeurs uniques pour se protéger contre les attaques par rejeu

Afin d'empêcher les utilisateurs malveillants de réutiliser les réponses précédentes de l'API Play Integrity, vous pouvez utiliser le champ nonce pour identifier chaque message de manière unique.

Lorsque vous demandez une évaluation de l'intégrité, procédez comme suit :

  1. Obtenez une valeur unique, que les utilisateurs malveillants ne pourront pas prédire. Par exemple, il peut s'agir d'un nombre aléatoire généré de manière cryptographique côté serveur ou d'un identifiant préexistant, tel qu'un ID de session ou de transaction. Une variante plus simple et moins sûre consiste à générer un nombre aléatoire sur l'appareil. Nous vous recommandons de créer des valeurs d'au moins 128 bits.
  2. Appelez setNonce() pour définir le champ nonce sur la valeur unique de l'étape 1.

Lorsque vous recevez une évaluation de l'intégrité, procédez comme suit :

  1. Décodez et vérifiez le jeton d'intégrité, puis obtenez la valeur unique du champ nonce.
  2. Si la valeur de l'étape 1 a été générée sur le serveur, vérifiez que la valeur unique reçue était l'une des valeurs générées et qu'elle est utilisée pour la première fois (votre serveur doit conserver un enregistrement des valeurs générées pendant une durée appropriée). Si la valeur unique reçue a déjà été utilisée ou n'apparaît pas dans l'enregistrement, refusez la requête.
  3. Sinon, si la valeur unique a été générée sur l'appareil, vérifiez que la valeur reçue est utilisée pour la première fois (votre serveur doit conserver un enregistrement des valeurs déjà vues pendant une durée appropriée). Si la valeur unique reçue a déjà été utilisée, refusez la requête.

Combiner les deux protections contre la falsification et les attaques par rejeu (recommandé)

Le champ nonce permet de se protéger simultanément contre les attaques de falsification et les attaques par rejeu. Pour ce faire, générez la valeur unique comme décrit ci-dessus et incluez-la dans votre requête. Calculez ensuite le hachage de la requête en veillant à y inclure cette valeur unique. Voici une implémentation qui combine ces deux approches :

Lorsque vous demandez une évaluation de l'intégrité, procédez comme suit :

  1. L'utilisateur effectue l'action à forte valeur ajoutée.
  2. Obtenez une valeur unique pour cette action, comme décrit dans la section Inclure des valeurs uniques pour se protéger contre les attaques par rejeu.
  3. Préparez un message que vous souhaitez protéger. Incluez-y la valeur unique de l'étape 2.
  4. Votre application calculera le récapitulatif du message qu'elle souhaite protéger, comme décrit dans la section Inclure un hachage de requête à protéger contre la falsification. Étant donné que le message contient la valeur unique, celle-ci fait partie du hachage.
  5. Utilisez setNonce() pour définir le champ nonce sur le récapitulatif calculé à l'étape précédente.

Lorsque vous recevez une évaluation de l'intégrité, procédez comme suit :

  1. Obtenez la valeur unique de la requête.
  2. Décodez et vérifiez le jeton d'intégrité, puis obtenez le récapitulatif du champ nonce.
  3. Comme décrit dans la section Inclure un hachage de requête pour vous protéger contre la falsification, recalculez le récapitulatif côté serveur et vérifiez qu'il correspond à celui qui a été obtenu par le jeton d'intégrité.
  4. Comme décrit dans la section Inclure des valeurs uniques pour se protéger contre les attaques par rejeu, vérifiez la validité de la valeur unique.

Le schéma séquentiel suivant illustre ces étapes avec un élément nonce côté serveur :

Schéma séquentiel montrant comment se protéger à la fois contre la falsification et les attaques par rejeu

Demander une évaluation de l'intégrité

Après avoir généré un nonce, vous pouvez demander une évaluation de l'intégrité sur Google Play. Pour ce faire, procédez comme suit :

  1. Créez un IntegrityManager, comme illustré dans les exemples suivants.
  2. Créez un IntegrityTokenRequest en fournissant le nonce via la méthode setNonce() du compilateur associé. Les applications distribuées exclusivement en dehors de Google Play et des SDK doivent également spécifier leur numéro de projet Google Cloud via la méthode setCloudProjectNumber(). Les applications sur Google Play sont associées à un projet Cloud dans la Play Console et n'ont pas besoin de définir leur numéro de projet Cloud dans la requête.
  3. Utilisez le gestionnaire pour appeler requestIntegrityToken(), en fournissant IntegrityTokenRequest.

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

Java

import com.google.android.gms.tasks.Task; ...

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

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

Natif

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

/// Prepare an IntegrityTokenResponse opaque type pointer and call
/// 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.
/// Note, the polling shouldn't block the thread where the IntegrityManager
/// is running.

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

Déchiffrer et vérifier l'évaluation de l'intégrité

Lorsque vous demandez une évaluation de l'intégrité, l'API Play Integrity fournit un jeton de réponse signé. Le nonce que vous incluez dans votre requête fait partie du jeton de réponse.

Format du jeton

Le jeton est un jeton Web JSON (JWT) imbriqué, c'est-à-dire un chiffrement Web JSON (JWE) de signature Web JSON (JWS). Les composants JWE et JWS sont représentés par une sérialisation compacte.

Les algorithmes de chiffrement/signature sont bien pris en charge par diverses implémentations JWT :

  • JWE utilise A256KW pour alg et A256GCM pour enc

  • JWS utilise ES256.

Déchiffrer et valider les données sur les serveurs Google (recommandé)

L'API Play Integrity vous permet de déchiffrer et de valider l'évaluation de l'intégrité sur les serveurs de Google, ce qui renforce la sécurité de votre application. Pour ce faire, procédez comme suit :

  1. Créez un compte de service dans le projet Google Cloud associé à votre application. Lors de la création du compte, vous devez attribuer à votre compte de service les rôles d'Utilisateur du compte de service et de Consommateur de Service Usage.
  2. Sur le serveur de votre application, récupérez le jeton d'accès à partir des identifiants de votre compte de service à l'aide du champ d'application playintegrity, puis exécutez la requête suivante :

    playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \
    '{ "integrity_token": "INTEGRITY_TOKEN" }'
  3. Lisez la réponse JSON.

Déchiffrer et vérifier en local

Si vous choisissez de gérer et de télécharger vos clés de chiffrement de réponse, vous pouvez déchiffrer et vérifier le jeton renvoyé dans votre propre environnement de serveur sécurisé. Vous pouvez obtenir le jeton renvoyé en utilisant la méthode IntegrityTokenResponse#token().

L'exemple suivant montre comment décoder la clé AES et la clé EC publique encodée en DER pour la vérification de la signature depuis la Play Console en clés spécifiques au langage (le langage de programmation Java, dans notre cas) dans le backend de l'application. Notez que les clés sont encodées en base64 à l'aide d'indicateurs par défaut.

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

Utilisez ensuite ces clés pour déchiffrer le jeton d'intégrité (partie JWE), puis vérifier et extraire la partie JWS imbriquée.

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

La charge utile résultante est un jeton en texte brut contenant des évaluations de l'intégrité.