サーバー側の検証(SSV)コールバックを検証する

サーバー側の検証コールバックは、Google により展開されたクエリ パラメータを含む URL のリクエストです。このコールバックが外部システムに送信され、ユーザーに動画リワード広告視聴に対する報酬を付与する必要があることが通知されます。動画リワード SSV(サーバー側の検証)コールバックは、ユーザーに報酬を与えるクライアント側のコールバックのなりすましに対する追加の保護層となります。

このガイドでは、Tink サードパーティの暗号化ライブラリを使用して、動画リワード SSV コールバックを検証し、コールバックのクエリ パラメータが正当な値であることを確認する方法を説明します。このガイドでは Tink を使用していますが、ECDSA をサポートするサードパーティのライブラリを使用することもできます。

前提条件

Tink から RewardedAdsVerifier を使用する

Tink GitHub リポジトリには RewardedAdsVerifier ヘルパークラスが含まれているため、動画リワード SSV コールバックの検証に必要なコードは少なくて済みます。このクラスを Tink サードパーティの暗号ライブラリとともに使用すると、次のコードでコールバック URL を検証できます。

RewardedAdsVerifier verifier = new RewardedAdsVerifier.Builder()
    .fetchVerifyingPublicKeysWith(
        RewardedAdsVerifier.KEYS_DOWNLOADER_INSTANCE_PROD)
    .build();
String rewardUrl = ...;
verifier.verify(rewardUrl);

verify() メソッドが例外を発生せずに実行された場合、コールバック URL は正常に検証されています。ユーザーに報酬を与えるセクションには、報酬を付与するタイミングについてのおすすめの方法が記載されています。動画リワード SSV コールバックを検証するためにこのクラスで実行される各ステップについては、動画リワード SSV の手動検証セクションを参照してください。

SSV コールバック パラメータ

サーバー側の検証コールバックには、動画リワード広告のインタラクションを説明するクエリ パラメータが含まれています。パラメータ名、説明、値の例を以下に示します。パラメータはアルファベット順に送信されます。

パラメータ名 説明 値の例
ad_network この広告を配信したネットワークの広告ネットワーク ID。ID 値に対応するネットワーク名は、広告ネットワーク ID セクションに一覧表示されています。 1953547073528090325
ad_unit 動画リワード広告をリクエストするために使用された AdMob 広告ユニット ID。 2747237135
custom_data customRewardString で指定されたカスタムデータ文字列。

アプリでカスタムデータ文字列が指定されていない場合、このクエリ パラメータ値は SSV コールバックに存在しません。

SAMPLE_CUSTOM_DATA_STRING
key_id SSV コールバックを検証するために使用されるキー。この値は、AdMob キーサーバーによって提供される公開キーにマッピングされます。 1234567890
reward_amount 広告ユニットの設定で指定された報酬額。 5
reward_item 広告ユニットの設定で指定された報酬アイテム。 コイン
signature AdMob によって生成された SSV コールバックの署名。 MEUCIQCLJS_s4ia_sN06HqzeW7Wc3nhZi4RlW3qV0oO-6AIYdQIgGJEh-rzKreO-paNDbSCzWGMtmgJHYYW9k2_icM9LFMY
timestamp ユーザが報酬を受けたときのエポック タイムスタンプ(ミリ秒単位)。 1507770365237823
transaction_id AdMob によって生成された報酬付与イベントごとに固有の 16 進数でエンコードされた ID。 18fa792de1bca816048293fc71035638
user_id userIdentifier で指定されたユーザー ID。

アプリでユーザー ID が指定されていない場合、このクエリ パラメータは SSV コールバックに存在しません。

1234567

広告ネットワーク ID

次の表に、SSV コールバックの広告ネットワーク ID 値に対応する広告ネットワーク名を示します。

広告ネットワーク名 広告ネットワーク ID
AdColony 15586990674969969776
AdMob 5450213213286189855
Applovin 1063618907739174004
Chartboost 2873236629771172317
Facebook Audience Network 10568273599589928883
Fuse 8914788932458531264
Fyber 4839637394546996422
InMobi 7681903010231960328
maio 7505118203095108657
myTarget 8450873672465271579
Nend 9383070032774777750
Tapjoy 7295217276740746030
Unity Ads 4970775877303683148
Vungle 1953547073528090325

ユーザーに報酬を付与する

ユーザーに報酬を付与するタイミングを決める際には、ユーザー エクスペリエンスと報酬検証のバランスをとることが重要です。サーバー側のコールバックでは、外部システムに達する前に遅延が発生する可能性があります。したがって、クライアント側のコールバックを使用して即座にユーザーに報酬を与え、サーバー側のコールバックの受信時にすべての報酬の検証を実行することをおすすめします。このアプローチを使うと、優れたユーザー エクスペリエンスが提供されると同時に、付与された報酬の有効性が保証されます。

ただし、報酬の検証が重要となるアプリ(報酬がアプリのゲーム内経済に影響を与えるなど)や報酬付与の遅延が許容されるアプリの場合は、サーバー側のコールバック検証を行うとよいでしょう。

カスタムデータ

サーバー側の検証コールバックで追加データを必要とするアプリでは、リワード広告のカスタムデータ機能を使用する必要があります。リワード広告オブジェクトに設定されている文字列値はすべて、SSV コールバックの custom_data クエリ パラメータ内に転送されます。カスタムデータ値が設定されていない場合、custom_data クエリ パラメータ値は SSV コールバックに存在しません。

以下のコード スニペットは、広告をリクエストする前に、リワード広告オブジェクトにカスタムデータを設定する方法を示しています。

GADRewardedAd *rewardedAd = [[GADRewardedAd alloc]
      initWithAdUnitID:@"ca-app-pub-3940256099942544/5224354917"];

GADServerSideVerificationOptions *options = [[GADServerSideVerificationOptions alloc] init];
options.customRewardString = @"SAMPLE_CUSTOM_DATA_STRING";
rewardedAd.serverSideVerificationOptions = options;

注: カスタムのリワード文字列は、パーセント エスケープされます。また、SSV コールバックからの解析時にはデコードが必要となる場合があります。

動画リワード SSV の手動検証

RewardedAdsVerifier クラスが動画リワード SSV を検証するために実行する手順を下記で説明します。含まれているコード スニペットは Java であり、Tink サードパーティ ライブラリを使用していますが、ECDSA をサポートするサードパーティのライブラリを使用して、これらの手順を任意の言語で実装することもできます。

公開キーを取得する

動画リワード SSV コールバックを検証するには、AdMob から提供された公開キーが必要です。

動画リワード SSV コールバックを検証するために使用される公開キーのリストは、AdMob キーサーバーから取得できます。公開キーのリストは、次のような形式の JSON 表現として提供されます。

{
 "keys": [
    {
      keyId: 1916455855,
      pem: "-----BEGIN PUBLIC KEY-----\nMF...YTPcw==\n-----END PUBLIC KEY-----"
      base64: "MFkwEwYHKoZIzj0CAQYI...ltS4nzc9yjmhgVQOlmSS6unqvN9t8sqajRTPcw=="
    },
    {
      keyId: 3901585526,
      pem: "-----BEGIN PUBLIC KEY-----\nMF...aDUsw==\n-----END PUBLIC KEY-----"
      base64: "MFYwEAYHKoZIzj0CAQYF...4akdWbWDCUrMMGIV27/3/e7UuKSEonjGvaDUsw=="
    },
  ],
}

公開キーを取得するには、AdMob キーサーバーに接続してキーをダウンロードします。次のコードはこのタスクを実行し、キーの JSON 表現を data 変数に保存します。

String url = ...;
NetHttpTransport httpTransport = new NetHttpTransport.Builder().build();
HttpRequest httpRequest =
    httpTransport.createRequestFactory().buildGetRequest(new GenericUrl(url));
HttpResponse httpResponse = httpRequest.execute();
if (httpResponse.getStatusCode() != HttpStatusCodes.STATUS_CODE_OK) {
  throw new IOException("Unexpected status code = " + httpResponse.getStatusCode());
}
String data;
InputStream contentStream = httpResponse.getContent();
try {
  InputStreamReader reader = new InputStreamReader(contentStream, UTF_8);
  data = readerToString(reader);
} finally {
  contentStream.close();
}

注: 公開キーは定期的にローテーションされます。次回のローテーションについては、メールでお知らせが届きます。公開キーをキャッシュしている場合は、このメールを受け取った時点でキーを更新してください。

取得された公開キーは、解析される必要があります。以下の parsePublicKeysJson メソッドでは、上記の例のような JSON 文字列を取り込み、key_id 値から公開キーへのマッピングを作成しています。公開キーは Tink ライブラリの ECPublicKey オブジェクトとしてカプセル化されています。

private static Map<Integer, ECPublicKey> parsePublicKeysJson(String publicKeysJson)
    throws GeneralSecurityException {
  Map<Integer, ECPublicKey> publicKeys = new HashMap<>();
  try {
    JSONArray keys = new JSONObject(publicKeysJson).getJSONArray("keys");
    for (int i = 0; i < keys.length(); i++) {
      JSONObject key = keys.getJSONObject(i);
      publicKeys.put(
          key.getInt("keyId"),
          EllipticCurves.getEcPublicKey(Base64.decode(key.getString("base64"))));
    }
  } catch (JSONException e) {
    throw new GeneralSecurityException("failed to extract trusted signing public keys", e);
  }
  if (publicKeys.isEmpty()) {
    throw new GeneralSecurityException("No trusted keys are available.");
  }
  return publicKeys;
}

検証するコンテンツを取得する

動画リワード SSV コールバックの最後の 2 つのクエリ パラメータは、signaturekey_id, で、常にこの順番となります。残りのクエリ パラメータは、検証されるコンテンツを指定します。リワード コールバックを https://www.myserver.com/mypath に送信するように AdMob を設定するとしましょう。下のスニペットは、検証されるコンテンツがハイライト表示された動画リワード SSV コールバックの例を示しています。

https://www.myserver.com/path?ad_network=54...55&ad_unit=12345678&reward_amount=10&reward_item=coins
&timestamp=150777823&transaction_id=12...DEF&user_id=1234567&signature=ME...Z1c&key_id=1268887

以下のコードは、検証するコンテンツを UTF-8 バイト配列としてコールバック URL から解析する方法を示しています。

public static final String SIGNATURE_PARAM_NAME = "signature=";
...
URI uri;
try {
  uri = new URI(rewardUrl);
} catch (URISyntaxException ex) {
  throw new GeneralSecurityException(ex);
}
String queryString = uri.getQuery();
int i = queryString.indexOf(SIGNATURE_PARAM_NAME);
if (i == -1) {
  throw new GeneralSecurityException("needs a signature query parameter");
}
byte[] queryParamContentData =
    queryString
        .substring(0, i - 1)
        // i - 1 instead of i because of & in the query string
        .getBytes(Charset.forName("UTF-8"));

コールバック URL から signature と key_id を取得する

前の手順の queryString 値を使用して、以下に示すように、コールバック URL の signaturekey_id のクエリ パラメータを解析します。

public static final String KEY_ID_PARAM_NAME = "key_id=";
...
String sigAndKeyId = queryString.substring(i);
i = sigAndKeyId.indexOf(KEY_ID_PARAM_NAME);
if (i == -1) {
  throw new GeneralSecurityException("needs a key_id query parameter");
}
String sig =
    sigAndKeyId.substring(
        SIGNATURE_PARAM_NAME.length(), i - 1 /* i - 1 instead of i because of & */);
int keyId = Integer.valueOf(sigAndKeyId.substring(i + KEY_ID_PARAM_NAME.length()));

検証を実行する

最後のステップは、適切な公開キーでコールバック URL の内容を検証することです。parsePublicKeysJson メソッドから返されたマッピングを取得し、コールバック URL の key_id パラメータを使用して、そのマッピングから公開キーを取得します。次に、その公開キーで署名を検証します。これらの手順は、以下の verify メソッドで示されます。

private void verify(final byte[] dataToVerify, int keyId, final byte[] signature)
    throws GeneralSecurityException {
  Map<Integer, ECPublicKey> publicKeys = parsePublicKeysJson();
  if (publicKeys.containsKey(keyId)) {
    foundKeyId = true;
    ECPublicKey publicKey = publicKeys.get(keyId);
    EcdsaVerifyJce verifier = new EcdsaVerifyJce(publicKey, "SHA256WithECDSA");
    verifier.verify(signature, dataToVerify);
  } else {
    throw new GeneralSecurityException("cannot find verifying key with key id: " + keyId);
  }
}

メソッドが例外を発生させずに実行された場合、コールバック URL は正常に検証されています。

よくある質問

AdMob キーサーバーから提供された公開キーをキャッシュできますか?
SSV コールバックを検証するために必要なオペレーションの数を減らすために、AdMob キーサーバーによって提供された公開キーをキャッシュすることをおすすめします。ただし、公開鍵は定期的にローテーションされるため、24 時間以上キャッシュしないでください。
AdMob キーサーバーによって提供される公開キーはどのくらいの頻度でローテーションされますか?
AdMob キーサーバーによって提供される公開キーは、可変スケジュールでローテーションされます。SSV コールバックが引き続き正常に検証されるには、公開鍵を 24 時間以上キャッシュしないでください。
サーバーにアクセスできない場合はどうなりますか?
SSV コールバック用に HTTP 200 OK 成功ステータス レスポンス コードが必要です。サーバーにアクセスできない場合または期待どおりのレスポンスが得られない場合、1 秒間隔で最大 5 回の SSV コールバックの送信が再試行されます。
SSV コールバックが Google から送信されていることを確認するにはどうすればよいですか?
SSV コールバックが Google から送信されていることを確認するには、リバース DNS ルックアップを使用します。