ほとんどのデベロッパーに適している標準 API リクエストのみの実行を計画している場合は、完全性判定の結果の説明にスキップできます。このページでは、Android 4.4(API レベル 19)以降でサポートされているクラシック API リクエストを実行して、完全性判定の結果を取得する方法について説明します。
考慮事項
標準リクエストとクラシック リクエストの比較
アプリのセキュリティと不正使用対策のニーズに応じて、標準リクエスト、クラシック リクエスト、または両者の組み合わせを実行できます。標準リクエストはすべてのアプリとゲームに適しており、リプレイ攻撃と漏洩からの保護の一部を Google Play に委任するとともに、アクションまたはサーバー呼び出しが真正なものであることを確認するために使用できます。クラシック リクエストの実行はよりコストが高く、デベロッパーには正しく実装して漏洩と特定のタイプの攻撃を防ぐ責任があります。クラシック リクエストは、標準リクエストより実行頻度を下げる必要があります。たとえば、価値の高いアクションまたは機密性の高いアクションが真正なものかどうかを確認するために、低い頻度で 1 回限り実行します。
2 種類のリクエストの主な違いを次の表に示します。
標準 API リクエスト | クラシック API リクエスト | |
---|---|---|
前提条件 | ||
必要な最小 Android SDK バージョン | Android 5.0(API レベル 21)以降 | Android 4.4(API レベル 19)以降 |
Google Play の要件 | Google Play ストアと Google Play 開発者サービス | Google Play ストアと Google Play 開発者サービス |
統合の詳細 | ||
API のウォームアップが必要 | ✔️(数秒) | ❌ |
リクエストの一般的なレイテンシ | 数百ミリ秒 | 数秒 |
可能なリクエスト頻度 | 高頻度(アクションまたはリクエストのオンデマンド チェック) | 低頻度(価値の高いアクションまたは機密性の高いリクエストの 1 回限りのチェック) |
タイムアウト | ほとんどのウォームアップは 10 秒未満ですが、サーバー呼び出しを伴うため、タイムアウトを長く設定することをおすすめします(1 分など)。判定リクエストはクライアントサイドで発生 | ほとんどのリクエストは 10 秒未満ですが、サーバー呼び出しを伴うため、タイムアウトを長く設定することをおすすめします(1 分など)。 |
完全性判定トークン | ||
デバイス、アプリ、アカウントの詳細が含まれている | ✔️ | ✔️ |
トークンのキャッシュ保存 | Google Play による保護されたオンデバイス キャッシュ保存 | 推奨されない |
Google Play サーバーによるトークンの復号と検証 | ✔️ | ✔️ |
サーバー間復号リクエストの一般的なレイテンシ | 数十ミリ秒、99.9% の可用性 | 数十ミリ秒、99.9% の可用性 |
安全なサーバー環境におけるローカルでのトークンの復号と検証 | ❌ | ✔️ |
クライアント サイドでのトークンの復号と検証 | ❌ | ❌ |
完全性判定の結果の鮮度 | Google Play による一部の自動キャッシュ保存と更新 | 各リクエストでのすべての判定結果の再計算 |
制限 | ||
アプリごとの 1 日あたりのリクエスト数 | デフォルトで 10,000(上限引き上げのリクエストが可能) | デフォルトで 10,000(上限引き上げのリクエストが可能) |
アプリ インスタンスごとの 1 分あたりのリクエスト数 | ウォームアップ: 1 分あたり 5 回の 完全性トークン: 公開されている制限なし* |
完全性トークン: 1 分あたり 5 回 |
保護 | ||
改ざんおよびその種の攻撃の軽減 | requestHash フィールドを使用する |
リクエスト データに基づくコンテンツ バインディングで nonce フィールドを使用する |
リプレイおよびその種の攻撃の軽減 | Google Play による自動軽減 | サーバーサイド ロジックで nonce フィールドを使用する |
* すべてのリクエスト(公開されている制限がないリクエストを含む)には、公開されていない高い値の防御制限が適用されます。
クラシック リクエストの頻度を下げる
完全性トークンの生成は、時間とデータと電力を消費します。各アプリには、1 日に実行できるクラシック リクエスト数の上限があります。したがって、最も価値の高いアクションまたは最も機密性の高いアクションが真正なものかどうかを確かめ、標準リクエストに保証を追加する目的でのみクラシック リクエストを実行するようにしてください。頻度の高いアクションや価値の低いアクションについては、クラシック リクエストを実行すべきではありません。アプリがフォアグラウンドに移行するたび、またはバックグラウンドで数分おきに実行することは控えてください。多数のデバイスから同時に呼び出すことも避けてください。クラシック リクエストの呼び出し回数が多すぎるアプリは、不適切な実装からユーザーを保護するためにスロットリングされる可能性があります。
判定結果をキャッシュに保存しない
判定結果をキャッシュに保存すると、漏洩やリプレイなどの攻撃により、信頼できない環境から適切な判定結果を再利用されるなどのリスクが高くなります。クラシック リクエストを作成して後で使用するためにキャッシュに保存することを検討している場合は、代わりに標準リクエストをオンデマンドで実行することをおすすめします。標準リクエストではデバイスにキャッシュが保存されることがありますが、Google Play ではリプレイ攻撃と漏洩のリスクを軽減するために追加の保護手法を使用しています。
nonce フィールドを使用してクラシック リクエストを保護する
Play Integrity API で提供される nonce
というフィールドを使用すると、リプレイや改ざんなどの特定の攻撃からのアプリの保護を強化できます。Play Integrity API は、このフィールドに設定した値を、署名付きの完全性レスポンス内で返します。アプリを攻撃から保護するには、ノンスを生成する方法のガイダンスに記載されている手順に厳密に従ってください。
指数バックオフを使用してクラシック リクエストを再試行する
不安定なインターネット接続や過負荷のデバイスといった環境条件が原因で、デバイスの完全性チェックが失敗することがあります。そのために、本来であれば信頼性が高いデバイスのラベルが生成されないことがあります。こうした条件の影響を軽減するには、指数バックオフによる再試行オプションを追加します。
概要
ユーザーがアプリで実行する価値の高いアクションを完全性チェックで保護したい場合は、次の手順を実施します。
- アプリのサーバーサイド バックエンドが、一意の値を生成してクライアントサイド ロジックに送信します。以降のステップでは、このロジックを「アプリ」と呼びます。
- アプリが、一意の値と価値の高いアクションの内容から、
nonce
を作成します。次に、アプリが Play Integrity API を呼び出してnonce
を渡します。 - アプリが、Play Integrity API から署名付きの暗号化された判定結果を受け取ります。
- アプリが、署名付きの暗号化された判定結果をアプリのバックエンドに渡します。
- アプリのバックエンドが、判定結果を Google Play サーバーに送信します。Google Play サーバーが判定結果を復号して検証し、その結果をアプリのバックエンドに返します。
- アプリのバックエンドが、トークン ペイロードに含まれるシグナルに基づいて処理方法を決定します。
- アプリのバックエンドが、決定結果をアプリに送信します。
ノンスを生成する
Play Integrity API を使用してアプリのアクションを保護する場合は、nonce
フィールドを利用して、中間者(PITM)改ざん攻撃やリプレイ攻撃のような特定のタイプの攻撃を軽減することができます。Play Integrity API は、このフィールドに設定した値を、署名付きの完全性レスポンス内で返します。
nonce
フィールドに設定した値が正しい形式であることが必要です。
String
- URL セーフ
- Base64、ラップなしでエンコード
- 16 文字以上
- 500 文字以下
以下では、Play Integrity API で nonce
フィールドを使用する一般的な方法をいくつか示します。下記の方法を組み合わせることで、nonce
による保護を強化できます。
改ざんを防ぐためにリクエスト ハッシュを含める
標準 API リクエストの requestHash
パラメータと同様にクラシック API リクエストの nonce
パラメータを使用して、リクエストの内容を改ざんから保護できます。
完全性判定の結果をリクエストするとき:
- 実行中のユーザー アクションまたはサーバー リクエストから、すべての重要なリクエスト パラメータ(安定したリクエストのシリアル化の SHA256 など)のダイジェストを計算します。
setNonce
を使用して、nonce
フィールドを計算されたダイジェストの値に設定します。
完全性判定の結果を受け取ったとき:
- 完全性トークンをデコードして検証し、
nonce
フィールドからダイジェストを取得します。 - アプリと同じ方法で、リクエストのダイジェスト(安定したリクエストのシリアル化の SHA256 など)を計算します。
- アプリサイドのダイジェストとサーバーサイドのダイジェストを比較します。一致しない場合、リクエストは信頼できません。
リプレイ攻撃を防ぐために一意の値を含める
悪意のあるユーザーが Play Integrity API から以前に返されたレスポンスを再利用することを防ぐために、nonce
フィールドを使用して各メッセージを一意に識別することができます。
完全性判定の結果をリクエストするとき:
- 悪意のあるユーザーが予測できないような、グローバルに一意の値を取得します。そのような値としては、たとえば暗号で保護された乱数(サーバーサイドで生成される)または既存の ID(セッション ID やトランザクション ID など)を使用できます。よりシンプルで安全性の低い代替策としては、デバイスで乱数を生成する方法があります。128 ビット以上の値を作成することをおすすめします。
setNonce()
を呼び出して、nonce
フィールドをステップ 1 の一意の値に設定します。
完全性判定の結果を受け取ったとき:
- 完全性トークンをデコードして検証し、
nonce
フィールドから一意の値を取得します。 - ステップ 1 の値がサーバーで生成されたものである場合は、受け取った一意の値が生成された値のいずれかであることと、その値が初めて使用されたことを確認します(サーバーは、生成された値のレコードを適切な期間が経過するまで保持する必要があります)。受け取った一意の値がすでに使用されていた場合、またはレコードに見つからない場合は、リクエストを拒否します。
- それ以外の場合は、一意の値がデバイスで生成されたものであれば、受け取った値が初めて使用されたことを確認します(サーバーは、確認済みの値のレコードを適切な期間が経過するまで保持する必要があります)。受け取った一意の値がすでに使用されていた場合は、リクエストを拒否します。
改ざん攻撃からの保護とリプレイ攻撃からの保護を組み合わせる(推奨)
nonce
フィールドを使用して、改ざん攻撃とリプレイ攻撃の両方を同時に防止することができます。そのためには、上記の方法で一意の値を生成し、リクエスト内に含めます。次に、一意の値をハッシュ内に含めて、リクエスト ハッシュを計算します。両方のアプローチを組み合わせた実装は次のとおりです。
完全性判定の結果をリクエストするとき:
- ユーザーが価値の高いアクションを開始します。
- リプレイ攻撃を防ぐために一意の値を含めるセクションの説明に沿って、このアクション用の一意の値を取得します。
- 保護したいメッセージを準備します。ステップ 2 の一意の値をメッセージに含めます。
- 改ざんを防ぐためにリクエスト ハッシュを含めるセクションの説明に沿って、保護したいメッセージのダイジェストをアプリで計算します。メッセージには一意の値が含まれているため、一意の値がハッシュの一部になります。
setNonce()
を使用して、nonce
フィールドを前のステップで計算したダイジェストに設定します。
完全性判定の結果を受け取ったとき:
- リクエストから一意の値を取得します。
- 完全性トークンをデコードして検証し、
nonce
フィールドからダイジェストを取得します。 - 改ざんを防ぐためにリクエスト ハッシュを含めるセクションの説明に沿って、サーバーサイドでダイジェストを再計算し、完全性トークンから取得したダイジェストと一致するかどうかをチェックします。
- リプレイ攻撃を防ぐために一意の値を含めるセクションの説明に沿って、一意の値の有効性をチェックします。
次のシーケンス図は、サーバーサイドの nonce
を使用する手順を示しています。
完全性判定の結果をリクエストする
nonce
を生成したら、Google Play に完全性判定の結果をリクエストできます。そのための手順は次のとおりです。
- 以下の例に示すように、
IntegrityManager
を作成します。 IntegrityTokenRequest
を作成し、関連付けられたビルダーのsetNonce()
メソッドで、生成したnonce
を指定します。Google Play 以外で独占配信されているアプリや、SDK についても、setCloudProjectNumber()
メソッドで Google Cloud プロジェクト番号を指定する必要があります。Google Play 上のアプリは、Google Play Console で Cloud プロジェクトにリンクされているため、こうしたリクエストで Cloud プロジェクト番号を設定する必要はありません。作成したマネージャーを使用して
requestIntegrityToken()
を呼び出し、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(); }
ネイティブ
/// 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();
完全性判定の結果の復号と検証
完全性判定の結果をリクエストすると、Play Integrity API によって署名付きレスポンス トークンが提供されます。リクエストに含めた nonce
は、レスポンス トークンの一部になります。
トークン形式
このトークンは、ネストされた JSON Web Token(JWT)、つまり JSON Web Signature(JWS)の JSON Web Encryption(JWE)です。JWE と JWS のコンポーネントは、Compact Serialization で表現されます。
暗号化と署名のアルゴリズムは、次のようなさまざまな JWT 実装で適切にサポートされています。
Google のサーバーでの復号と検証(推奨)
Play Integrity API を使用すると、Google のサーバー上で完全性判定の結果を復号して検証することができます。これにより、アプリのセキュリティが強化されます。手順は次のとおりです。
- サービス アカウントを作成する Google Cloud プロジェクト内に表示されます
アプリのサーバーで、
playintegrity
スコープを使用してサービス アカウントの認証情報からアクセス トークンを取得し、次のリクエストを行います。playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \ '{ "integrity_token": "INTEGRITY_TOKEN" }'
JSON レスポンスを読み取ります。
ローカルでの復号と検証
レスポンスの暗号鍵をご自身で管理、ダウンロードすることを選択した場合は、独自の安全なサーバー環境内で、返されたトークンの復号、検証を行えます。返されたトークンは、IntegrityTokenResponse#token()
メソッドを使用して取得できます。
次の例は、Google Play Console から取得した AES 鍵と DER エンコードされた署名検証用の公開 EC 鍵を、アプリのバックエンドで言語(この例では Java プログラミング言語)固有の鍵に復号する方法を示しています。なお、鍵はデフォルトのフラグを使用して Base64 エンコードされています。
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));
次に、これらのキーを使用して完全性トークン(JWE の部分)を復号してから、ネストされた JWS の部分を検証して抽出します。
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();
結果として返されるペイロードは、完全性判定の結果を含む書式なしテキストのトークンです。