Eseguire una richiesta API classica

Se prevedi di effettuare solo richieste API standard, adatte alla maggior parte degli sviluppatori, puoi passare direttamente alla sezione Esiti di integrità. Questa pagina descrive come effettuare richieste API classiche per gli esiti di integrità, supportati su Android 4.4 (livello API 19) o versioni successive.

Considerazioni

Confronta le richieste standard e classiche

Puoi effettuare richieste standard, richieste classiche o una combinazione delle due, a seconda delle esigenze di sicurezza e antiabuso della tua app. Le richieste standard sono adatte a tutte le app e a tutti i giochi e possono essere utilizzate per verificare che qualsiasi azione o chiamata al server sia autentica, delegando a Google Play una parte della protezione contro la riproducibilità e l'esfiltrazione. Le richieste classiche sono più costose da effettuare e sei responsabile della loro corretta implementazione per proteggerti dall'esfiltrazione e da determinati tipi di attacchi. Le richieste classiche devono essere effettuate meno frequentemente rispetto alle richieste standard, ad esempio come un'operazione occasionale per verificare se un'azione di alto valore o sensibile è autentica.

La seguente tabella evidenzia le principali differenze tra i due tipi di richieste:

Richiesta API standard Richiesta API classica
Prerequisiti
SDK Android minimo richiesto* Android 6.0 (livello API 23) o versioni successive Android 6.0 (livello API 23) o versioni successive
Requisiti di Google Play Google Play Store e Google Play Services Google Play Store e Google Play Services
Dettagli dell'integrazione
Warm up dell'API richiesto ✔️ (pochi secondi) ❌
Latenza tipica delle richieste Alcune centinaia di millisecondi Pochi secondi
Frequenza delle richieste potenziali Frequente (controllo on demand per qualsiasi azione o richiesta) Infrequente (controllo una tantum per le azioni di maggior valore o le richieste più sensibili)
Timeout La maggior parte delle fasi di preparazione dura meno di 10 secondi, ma comporta una chiamata al server, pertanto è consigliabile un timeout lungo (ad es. 1 minuto). Le richieste di verdetto avvengono lato client La maggior parte delle richieste richiede meno di 10 secondi, ma comporta una chiamata al server, pertanto è consigliabile un timeout lungo (ad es. 1 minuto)
Token di esito relativo all'integrità
Contiene i dettagli del dispositivo, dell'app e dell'account ✔️ ✔️
Memorizzazione dei token nella cache Memorizzazione nella cache on-device protetta da Google Play Selezione sconsigliata
Decripta e verifica il token utilizzando un server Google Play ✔️ ✔️
Latenza tipica delle richieste server-server di decrittografia Decine di millisecondi con disponibilità del 99,9% Decine di millisecondi con disponibilità del 99,9%
Decripta e verifica il token di verifica localmente in un ambiente server sicuro ❌ ✔️
Decripta e verifica il token lato client ❌ ❌
Aggiornamento dell'esito relativo all'integrità Alcune operazioni di memorizzazione nella cache e aggiornamento automatici da parte di Google Play Tutti i verdetti vengono ricalcolati per ogni richiesta
limiti
Richieste per app al giorno 10.000 per impostazione predefinita (è possibile richiedere un aumento) 10.000 per impostazione predefinita (è possibile richiedere un aumento)
Richieste per istanza di app al minuto Warm up: 5 al minuto
Token di integrità: nessun limite pubblico**
Token di integrità: 5 al minuto
Protezione
Mitigazione di manomissioni e attacchi simili Utilizza il campo requestHash Utilizza il campo nonce con l'associazione dei contenuti in base ai dati della richiesta
Mitigazione degli attacchi di tipo replay e simili Mitigazione automatica da parte di Google Play Utilizza il campo nonce con la logica lato server

* Per la libreria dell'API Play Integrity v1.4.0 e versioni successive, l'SDK Android minimo supportato è lo stesso per entrambi i tipi di richiesta ed è determinato dal valore minSdkVersion della libreria. Per la versione 1.3.0 e precedenti, l'SDK Android minimo richiesto è Android 5.0 (livello API 21) per le richieste API standard e Android 4.4 (livello API 19) per le richieste API classiche.

** Tutte le richieste, incluse quelle senza limiti pubblici, sono soggette a limiti difensivi non pubblici con valori elevati.

Effettua richieste classiche di rado

La generazione di un token di integrità richiede tempo, dati e batteria e ogni app ha un numero massimo di richieste classiche che può effettuare al giorno. Pertanto, devi effettuare richieste classiche solo per verificare che le azioni di valore più elevato o più sensibili siano autentiche quando vuoi una garanzia aggiuntiva rispetto a una richiesta standard. Non devi effettuare richieste classiche per azioni ad alta frequenza o di basso valore. Non effettuare richieste classiche ogni volta che l'app passa in primo piano o ogni pochi minuti in background ed evita di chiamare da un numero elevato di dispositivi contemporaneamente. Un'app che effettua troppe chiamate di richieste classiche potrebbe essere limitata per proteggere gli utenti da implementazioni errate.

Evita di memorizzare nella cache i verdetti

La memorizzazione nella cache di un verdetto aumenta il rischio di attacchi come l'esfiltrazione e la ripetizione, in cui un verdetto positivo viene riutilizzato da un ambiente non attendibile. Se stai valutando di effettuare una richiesta classica e poi memorizzarla nella cache per utilizzarla in un secondo momento, ti consigliamo di eseguire una richiesta standard on demand. Le richieste standard comportano una certa memorizzazione nella cache sul dispositivo, ma Google Play utilizza tecniche di protezione aggiuntive per mitigare il rischio di attacchi di replay ed esfiltrazione.

Utilizza il campo nonce per proteggere le richieste classiche

L'API Play Integrity offre un campo chiamato nonce, che può essere utilizzato per proteggere ulteriormente la tua app da determinati attacchi, come quelli di replay e manomissione. L'API Play Integrity restituisce il valore impostato in questo campo all'interno della risposta di integrità firmata. Segui attentamente le indicazioni su come generare nonce per proteggere la tua app dagli attacchi.

Riprova le richieste classiche con il backoff esponenziale

Le condizioni ambientali, come una connessione a internet instabile o un dispositivo sovraccarico, possono causare l'esito negativo dei controlli di integrità del dispositivo. Ciò può comportare la mancata generazione di etichette per un dispositivo che altrimenti sarebbe attendibile. Per mitigare questi scenari, includi un'opzione di nuovo tentativo con backoff esponenziale.

Panoramica

Figura 1. Diagramma di sequenza che mostra il design di alto livello dell'API Play Integrity.

Quando l'utente esegue un'azione di alto valore nella tua app che vuoi proteggere con un controllo di integrità, completa i seguenti passaggi:

  1. Il backend lato server della tua app genera e invia un valore univoco alla logica lato client. I passaggi rimanenti fanno riferimento a questa logica come alla tua "app".
  2. La tua app crea il nonce dal valore univoco e dai contenuti dell'azione di alto valore. Poi chiama l'API Play Integrity, passando il valore nonce.
  3. La tua app riceve un esito firmato e criptato dall'API Play Integrity.
  4. La tua app trasmette l'esito firmato e criptato al suo backend.
  5. Il backend della tua app invia l'esito a un server di Google Play. Il server di Google Play decripta e verifica l'esito, restituendo i risultati al backend della tua app.
  6. Il backend della tua app determina come procedere in base agli indicatori contenuti nel payload del token.
  7. Il backend della tua app invia i risultati della decisione alla tua app.

Genera un nonce

Quando proteggi un'azione nella tua app con l'API Play Integrity, puoi sfruttare il campo nonce per mitigare determinati tipi di attacchi, come gli attacchi di manomissione PITM (person-in-the-middle) e gli attacchi di replay. L'API Play Integrity restituisce il valore impostato in questo campo all'interno della risposta di integrità firmata.

Il valore impostato nel campo nonce deve essere formattato correttamente:

  • String
  • URL-safe
  • Codificata in Base64 e senza ritorno a capo
  • Almeno 16 caratteri
  • Massimo 500 caratteri

Di seguito sono riportati alcuni modi comuni per utilizzare il campo nonce nell'API Play Integrity. Per ottenere la massima protezione dal nonce, puoi combinare i metodi riportati di seguito.

Includi un hash della richiesta per proteggerti dalle manomissioni

Puoi utilizzare il parametro nonce in una richiesta API classica in modo simile al parametro requestHash in una richiesta API standard per proteggere i contenuti di una richiesta da manomissioni.

Quando richiedi un esito relativo all'integrità:

  1. Calcola un digest di tutti i parametri di richiesta critici (ad es. SHA256 di una serializzazione della richiesta stabile) dall'azione dell'utente o dalla richiesta del server in corso.
  2. Utilizza setNonce per impostare il campo nonce sul valore del digest calcolato.

Quando ricevi un esito relativo all'integrità:

  1. Decodifica e verifica il token di integrità e ottieni il digest dal campo nonce.
  2. Calcola un digest della richiesta nello stesso modo dell'app (ad es. SHA256 di una serializzazione della richiesta stabile).
  3. Confronta i digest lato app e lato server. Se non corrispondono, la richiesta non è attendibile.

Includi valori univoci per proteggerti dagli attacchi di tipo replay

Per impedire a utenti malintenzionati di riutilizzare le risposte precedenti dell'API Play Integrity, puoi utilizzare il campo nonce per identificare in modo univoco ogni messaggio.

Quando richiedi un esito relativo all'integrità:

  1. Ottenere un valore univoco a livello globale in modo che gli utenti malintenzionati non possano prevederlo. Ad esempio, un numero casuale protetto da crittografia generato sul lato server può essere un valore di questo tipo o un ID preesistente, come un ID sessione o un ID transazione. Una variante più semplice e meno sicura consiste nel generare un numero casuale sul dispositivo. Ti consigliamo di creare valori di almeno 128 bit.
  2. Chiama setNonce() per impostare il campo nonce sul valore univoco del passaggio 1.

Quando ricevi un esito relativo all'integrità:

  1. Decodifica e verifica il token di integrità e ottieni il valore univoco dal campo nonce.
  2. Se il valore del passaggio 1 è stato generato sul server, verifica che il valore univoco ricevuto sia uno dei valori generati e che venga utilizzato per la prima volta (il tuo server dovrà conservare un record dei valori generati per un periodo di tempo adeguato). Se il valore univoco ricevuto è già stato utilizzato o non è presente nel record, rifiuta la richiesta
  3. In caso contrario, se il valore univoco è stato generato sul dispositivo, verifica che il valore ricevuto venga utilizzato per la prima volta (il tuo server deve conservare un record dei valori già visualizzati per un periodo di tempo adeguato). Se il valore univoco ricevuto è già stato utilizzato, rifiuta la richiesta.

Combina entrambe le protezioni contro le manomissioni e gli attacchi di replay (consigliato)

È possibile utilizzare il campo nonce per proteggersi contemporaneamente da attacchi di manomissione e replay. Per farlo, genera il valore univoco come descritto sopra e includilo nella richiesta. Poi calcola l'hash della richiesta, assicurandoti di includere il valore univoco nell'hash. Un'implementazione che combina entrambi gli approcci è la seguente:

Quando richiedi un esito relativo all'integrità:

  1. L'utente avvia l'azione di alto valore.
  2. Ottieni un valore univoco per questa azione come descritto nella sezione Includi valori univoci per proteggerti dagli attacchi di tipo replay.
  3. Prepara un messaggio che vuoi proteggere. Includi nel messaggio il valore univoco del passaggio 2.
  4. La tua app calcola un digest del messaggio che vuole proteggere, come descritto nella sezione Includi un hash della richiesta per proteggere dalle manomissioni. Poiché il messaggio contiene il valore univoco, quest'ultimo fa parte dell'hash.
  5. Utilizza setNonce() per impostare il campo nonce sul digest calcolato nel passaggio precedente.

Quando ricevi un esito relativo all'integrità:

  1. Ottenere il valore univoco dalla richiesta
  2. Decodifica e verifica il token di integrità e ottieni il digest dal campo nonce.
  3. Come descritto nella sezione Includi un hash della richiesta per proteggerti dalle manomissioni, ricalcola il digest sul lato server e verifica che corrisponda a quello ottenuto dal token di integrità.
  4. Come descritto nella sezione Includi valori unici per proteggerti dagli attacchi di replay, verifica la validità del valore unico.

Il seguente diagramma di sequenza illustra questi passaggi con un nonce lato server:

Figura 2. Diagramma di sequenza che mostra come proteggersi dagli attacchi di manomissione e di replay.

Richiedi un esito relativo all'integrità

Dopo aver generato un nonce, puoi richiedere un esito relativo all'integrità da Google Play. Per farlo, segui questa procedura.

  1. Crea un IntegrityManager, come mostrato negli esempi seguenti.
  2. Crea un IntegrityTokenRequest, fornendo nonce tramite il metodo setNonce() nel builder associato. Anche le app distribuite esclusivamente al di fuori di Google Play e gli SDK devono specificare il numero di progetto Google Cloud tramite il metodo setCloudProjectNumber(). Le app su Google Play sono collegate a un progetto Cloud in Play Console e non devono impostare il numero di progetto Cloud nella richiesta.
  3. Utilizza il gestore per chiamare requestIntegrityToken(), fornendo IntegrityTokenRequest. Attenzione: i valori nonce vengono trasmessi così come sono ai sistemi di Google, pertanto non devi includere informazioni che consentono l'identificazione personale o altri dati sensibili direttamente o indirettamente in nonce.

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

Unità

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

// Initialize the V2 manager. Requires Play Integrity Unity Plugin v2.0.0 // or higher. var integrityManager = new IntegrityManagerV2();

// 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 != null && requestIntegrityTokenOperation.Error.ErrorCode != IntegrityErrorCode.NoError) { AppendStatusLog("IntegrityAsyncOperation failed with error: " + requestIntegrityTokenOperation.Error.ErrorCode); yield break; }

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

Unreal Engine

// .h
void MyClass::OnRequestIntegrityTokenCompleted(
  EIntegrityErrorCode ErrorCode,
  UIntegrityTokenResponse* Response)
{
  // Check the resulting error code.
  if (ErrorCode == EIntegrityErrorCode::Integrity_NO_ERROR)
  {
    // Get the token.
    FString Token = Response->Token;
  }
}

// .cpp
void MyClass::RequestIntegrityToken()
{
  // Receive the nonce from the secure server.
  FString Nonce = ...

  // Create the Integrity Token Request.
  FIntegrityTokenRequest Request = { Nonce };

  // Create a delegate to bind the callback function.
  FIntegrityOperationCompletedDelegate Delegate;

  // Bind the completion handler (OnRequestIntegrityTokenCompleted) to the delegate.
  Delegate.BindDynamic(this, &MyClass::OnRequestIntegrityTokenCompleted);

  // Initiate the integrity token request, passing the delegate to handle the result.
  GetGameInstance()
    ->GetSubsystem<UIntegrityManager>()
    ->RequestIntegrityToken(Request, Delegate);
}

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

Decripta e verifica l'esito relativo all'integrità

Quando richiedi un esito di integrità, l'API Play Integrity fornisce un token di risposta firmato. Il valore nonce che includi nella richiesta diventa parte del token di risposta.

Formato del token

Il token è un JSON Web Token (JWT) nidificato, ovvero un JSON Web Encryption (JWE) di JSON Web Signature (JWS). I componenti JWE e JWS sono rappresentati utilizzando la serializzazione compatta.

Gli algoritmi di crittografia/firma sono ben supportati in varie implementazioni JWT:

  • JWE utilizza A256KW per alg e A256GCM per enc

  • JWS utilizza ES256.

Decripta e verifica sui server di Google (opzione consigliata)

L'API Play Integrity ti consente di decriptare e verificare l'esito relativo all'integrità sui server di Google, migliorando la sicurezza della tua app. Per farlo, segui questi passaggi:

  1. Crea un service account all'interno del progetto Google Cloud collegato alla tua app.
  2. Sul server della tua app, recupera il token di accesso dalle credenziali del service account utilizzando l'ambito playintegrity ed effettua la seguente richiesta:

    playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \
    '{ "integrity_token": "INTEGRITY_TOKEN" }'
  3. Leggi la risposta JSON.

Decripta e verifica localmente

Se scegli di gestire e scaricare le chiavi di crittografia delle risposte, puoi decriptare e verificare il token restituito nel tuo ambiente server sicuro. Puoi ottenere il token restituito utilizzando il metodo IntegrityTokenResponse#token().

L'esempio seguente mostra come decodificare la chiave AES e la chiave EC pubblica con codifica DER per la verifica della firma da Play Console alle chiavi specifiche del linguaggio (il linguaggio di programmazione Java, nel nostro caso) nel backend dell'app. Tieni presente che le chiavi sono codificate in base64 utilizzando i flag predefiniti.

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

Successivamente, utilizza queste chiavi per decriptare prima il token di integrità (parte JWE) e poi verificare ed estrarre la parte JWS nidificata.

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

Il payload risultante è un token di testo normale che contiene esiti di integrità.

Risolvi i problemi relativi ai verdict con un prompt di Google Play (facoltativo)

Dopo aver ricevuto un esito relativo all'integrità, il server può determinare come procedere. Se l'esito indica un problema, ad esempio l'app non ha una licenza, è stata manomessa o il dispositivo è stato compromesso, puoi dare agli utenti la possibilità di risolvere il problema autonomamente.

L'API Play Integrity offre un'opzione per mostrare una finestra di dialogo di Google Play che invita l'utente a passare all'azione, ad esempio scaricare la versione ufficiale della tua app da Google Play.

Per scoprire come attivare queste finestre di dialogo dalla tua app in base alla risposta del server, consulta Finestre di dialogo di correzione.