パブリッシャー提供の IP アドレスを設定する

ブラウザのプライバシー プロキシと IP 保護の設定により、Google アド マネージャーへのサードパーティ広告リクエストでクライアント IP アドレスをマスクできます。IP アドレスがマスクされると、地域ターゲティングの精度と広告の関連性が低下する可能性があります。ウェブサーバーまたはコンテンツ配信ネットワーク(CDN)はファーストパーティの文脈でユーザーとやり取りするため、サーバーはユーザーのマスクされていない IP アドレスを受け取ります。

パブリッシャー提供の IP アドレス機能を使用すると、サーバーでユーザーのマスクされていない IPv4 または IPv6 アドレスをキャプチャし、ハイブリッド公開鍵暗号(HPKE)を使用してアドレスを暗号化し、暗号化されたペイロードをクライアントの Google パブリッシャー タグ(GPT)に渡すことができます。

このページでは、パブリッシャー提供の IP アドレスを GPT と統合する方法について説明します。

サーバーサイド ワークフローを実装する

ユーザーの平文の IP アドレスがブラウザのサードパーティ スクリプトに公開されないようにするには、ウェブサーバーまたは CDN エッジワーカーで次の手順を行います。

  • すべての IP キャプチャを実行します。
  • 公開鍵を取得します。
  • HPKE 暗号化を実行します。

クライアント IP アドレスを取得する

ウェブサーバーまたは CDN の受信ページ リクエストから、クライアントのマスクされていない IPv4 または IPv6 アドレスを抽出します。

  • 直接接続: 受信 HTTP 接続のリモート ソケット アドレスを読み取ります。
  • CDN、ロードバランサ、リバース プロキシの背後にある場合: 信頼できるエッジ プロキシからクライアント IP ヘッダー(X-Forwarded-For ヘッダーや CDN 固有のクライアント IP ヘッダーなど)を読み取ります。プロキシ インフラストラクチャが追加またはオーバーライドする転送ヘッダーのみを信頼します。X-Forwarded-For ヘッダーにカンマ区切りの複数のアドレスが含まれている場合は、信頼できるエッジ プロキシによって追加されたクライアント IP を選択します。

アド マネージャーでは、次のアドレスがサポートされています。

  • IPv4(例: 203.0.113.42)。
  • IPv6(例: 2001:db8::8a2e:370:7334)。

ポート番号、角かっこ、CIDR プレフィックス、周囲の空白文字を含まない、IP アドレス文字列のみを渡します。

公開鍵を取得してキャッシュに保存する

Google は、アクティブな HPKE 暗号鍵を RFC 8037 JSON Web Key Set(JWKS)として次のエンドポイントに公開します。

https://www.gstatic.com/real-ip/ip_enc_key.json

このエンドポイントへの GET リクエストは JSON オブジェクトを返します。このオブジェクトの keys 配列には、単一のアクティブな鍵オブジェクトが含まれています。そのオブジェクトの x プロパティの鍵を使用して、クライアント IP アドレスを暗号化します。

次の例では、公開鍵を取得し、成功した場合は x プロパティを抽出し、失敗した場合は例外をスローします。

Java

private static final String KEY_ENDPOINT_URL = "https://www.gstatic.com/real-ip/ip_enc_key.json";

/**
 * Fetches the active X25519 public key from Google's key endpoint.
 *
 * <p>In production, cache the returned public key in memory and refresh it at least once every 24
 * hours rather than fetching it on every incoming request.
 *
 * @return The unpadded Base64URL-encoded 32-byte X25519 public key ('x').
 */
public static String fetchPublicKey() throws IOException, InterruptedException {
  return fetchPublicKey(KEY_ENDPOINT_URL);
}

public static String fetchPublicKey(String endpointUrl) throws IOException, InterruptedException {
  HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
  HttpRequest request =
      HttpRequest.newBuilder()
          .uri(URI.create(endpointUrl))
          .timeout(Duration.ofSeconds(5))
          .GET()
          .build();
  HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
  if (response.statusCode() < 200 || response.statusCode() >= 300) {
    throw new IOException("Failed to fetch public key, HTTP status: " + response.statusCode());
  }
  JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
  JsonArray keys = json.getAsJsonArray("keys");
  if (keys == null || keys.isEmpty()) {
    throw new IOException("No keys found in response from " + endpointUrl);
  }
  JsonObject keyObj = keys.get(0).getAsJsonObject();
  if (!keyObj.has("x")) {
    throw new IOException("Key payload missing 'x' property: " + keyObj);
  }
  return keyObj.get("x").getAsString();
}

Python

KEY_ENDPOINT_URL = "https://www.gstatic.com/real-ip/ip_enc_key.json"


def fetch_public_key(url: str = KEY_ENDPOINT_URL) -> str:
  """Fetches the active X25519 public key from Google's key endpoint.

  In production, cache the returned public key in memory and refresh it at
  least once every 24 hours rather than fetching it on every incoming request.

  Args:
    url: URL of the public key endpoint.

  Returns:
    The unpadded Base64URL-encoded 32-byte X25519 public key ('x').
  """
  with urllib.request.urlopen(url, timeout=5) as response:
    data = json.loads(response.read().decode("utf-8"))
  keys = data.get("keys", [])
  if not keys:
    raise ValueError(f"No keys found in response from {url}")
  key_payload = keys[0].get("x")
  if not key_payload:
    raise ValueError(f"Key payload missing 'x' property: {keys[0]}")
  return key_payload


JavaScript

const KEY_ENDPOINT_URL = 'https://www.gstatic.com/real-ip/ip_enc_key.json';

/**
 * Fetches the active X25519 public key from Google's key endpoint.
 *
 * In production, cache the returned public key in memory and refresh it at
 * least once every 24 hours rather than fetching it on every incoming request.
 *
 * @param {string=} url URL of the public key endpoint.
 * @return {!Promise<string>} The unpadded Base64URL-encoded 32-byte X25519
 *     public key ('x').
 */
async function fetchPublicKey(url = KEY_ENDPOINT_URL) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(
      `Failed to fetch public key, HTTP status: ${response.status}`,
    );
  }
  const data = await response.json();
  if (!data.keys || data.keys.length === 0) {
    throw new Error(`No keys found in response from ${url}`);
  }
  const keyPayload = data.keys[0].x;
  if (!keyPayload) {
    throw new Error(
      `Key payload missing 'x' property: ${JSON.stringify(data.keys[0])}`,
    );
  }
  return keyPayload;
}

TypeScript

const KEY_ENDPOINT_URL = 'https://www.gstatic.com/real-ip/ip_enc_key.json';

interface JwkKey {
  kty?: string;
  crv?: string;
  x?: string;
  kid?: string;
}

interface KeyEndpointResponse {
  keys?: JwkKey[];
}

/**
 * Fetches the active X25519 public key from Google's key endpoint.
 *
 * In production, cache the returned public key in memory and refresh it at
 * least once every 24 hours rather than fetching it on every incoming request.
 *
 * @param url URL of the public key endpoint.
 * @return The unpadded Base64URL-encoded 32-byte X25519 public key ('x').
 */
export async function fetchPublicKey(
  url: string = KEY_ENDPOINT_URL,
): Promise<string> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(
      `Failed to fetch public key, HTTP status: ${response.status}`,
    );
  }
  const data = (await response.json()) as KeyEndpointResponse;
  if (!data.keys || data.keys.length === 0) {
    throw new Error(`No keys found in response from ${url}`);
  }
  const keyPayload = data.keys[0].x;
  if (!keyPayload) {
    throw new Error(
      `Key payload missing 'x' property: ${JSON.stringify(data.keys[0])}`,
    );
  }
  return keyPayload;
}

成功した場合、レスポンスには x プロパティを持つ鍵オブジェクトを含む keys 配列が含まれます。

{
  "keys": [
    {
      "kty": "OKP",
      "crv": "X25519",
      "x": "7bws5TIHOUrcJNofEubW3DPW8zR9EtjFNLm_Grv98TA",
      "kid": "EZMLFw"
    }
  ]
}

次の表に、keys 配列のキー オブジェクトのプロパティを示します。

プロパティ 説明
crv 楕円曲線識別子。常に X25519 です。
kid パディングなしの base64url でエンコードされた鍵識別子。Google が鍵をローテーションすると変更されます。
kty 鍵のタイプ。常に OKP(オクテット鍵ペア)。
x 暗号化に使用される、パディングなしの base64url エンコードされた 32 バイトの未加工 X25519 公開鍵。

取得した公開鍵をサーバーのメモリにキャッシュに保存し、少なくとも 24 時間に 1 回、または HTTP Cache-Control レスポンス ヘッダーに従ってそれより短い間隔で鍵を更新します。受信ページ リクエストごとに公開鍵を取得しないでください。ローテーションされた秘密鍵は Google が保持します。過去 24 時間にキャッシュに保存された公開鍵で暗号化されたペイロードは、鍵のローテーション中も中断されることなく復号されます。

IP アドレスを暗号化する

HPKE(RFC 9180)と、公開鍵を取得してキャッシュに保存するで取得したデコード済みの 32 バイト X25519 公開鍵 x を使用して、クライアントの IP アドレス文字列の UTF-8 バイト表現を暗号化します。

次の例では、IP アドレス文字列を暗号化します。

Java

この例では、Tink(com.google.crypto.tink:tink 1.16.0 以降)を使用します。

static {
  try {
    HybridConfig.register();
  } catch (GeneralSecurityException e) {
    throw new ExceptionInInitializerError(e);
  }
}

/**
 * Encrypts an IP address using HPKE (DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-256-GCM).
 *
 * @param pubXB64Url Base64URL-encoded 32-byte X25519 public key ('x').
 * @param ipStr Plaintext IPv4 or IPv6 address string (for example, "203.0.113.42").
 * @return Unpadded Base64URL-encoded payload (enc || ciphertext).
 */
@AccessesPartialKey
public static String encryptIp(String pubXB64Url, String ipStr) throws GeneralSecurityException {
  // 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
  byte[] pubBytes = Base64.getUrlDecoder().decode(pubXB64Url);
  if (pubBytes.length != 32) {
    throw new IllegalArgumentException(
        "Public key must be exactly 32 bytes, got " + pubBytes.length);
  }

  // 2. Configure HPKE parameters (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
  //    AES_256_GCM, NO_PREFIX).
  HpkeParameters parameters =
      HpkeParameters.builder()
          .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
          .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
          .setAeadId(HpkeParameters.AeadId.AES_256_GCM)
          .setVariant(HpkeParameters.Variant.NO_PREFIX)
          .build();

  HpkePublicKey publicKey =
      HpkePublicKey.create(parameters, Bytes.copyFrom(pubBytes), /* idRequirement= */ null);

  // 3. Encrypt the UTF-8 IP address bytes. With Variant.NO_PREFIX, Tink
  //    outputs raw (enc || ciphertext_with_tag).
  KeysetHandle handle =
      KeysetHandle.newBuilder()
          .addEntry(KeysetHandle.importKey(publicKey).withRandomId().makePrimary())
          .build();

  HybridEncrypt encryptor = handle.getPrimitive(RegistryConfiguration.get(), HybridEncrypt.class);
  byte[] payload =
      encryptor.encrypt(ipStr.getBytes(StandardCharsets.UTF_8), /* contextInfo= */ new byte[0]);

  // 4. Return unpadded Base64URL-encoded payload.
  return Base64.getUrlEncoder().withoutPadding().encodeToString(payload);
}

Python

この例では pyhpke を使用しています。

def encrypt_ip(pub_x_b64url: str, ip_str: str) -> str:
  """Encrypts an IP address using HPKE (RFC 9180).

  Args:
    pub_x_b64url: Base64URL-encoded 32-byte X25519 public key ('x').
    ip_str: Plaintext IPv4 or IPv6 address string (for example, "203.0.113.42").

  Returns:
    Unpadded Base64URL-encoded payload (enc || ciphertext).
  """
  # 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
  pub_bytes = base64.urlsafe_b64decode(
      pub_x_b64url + "=" * (-len(pub_x_b64url) % 4)
  )
  if len(pub_bytes) != 32:
    raise ValueError(
        f"Public key must be exactly 32 bytes, got {len(pub_bytes)}"
    )

  # 2. Configure HPKE cipher suite (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
  #    AES256_GCM).
  suite = CipherSuite.new(
      KEMId.DHKEM_X25519_HKDF_SHA256, KDFId.HKDF_SHA256, AEADId.AES256_GCM
  )
  public_key = suite.kem.deserialize_public_key(pub_bytes)

  # 3. Create a fresh sender context per encryption (you may cache 'suite' and
  #    'public_key', but never reuse 'sender' across multiple seal() calls) and
  #    package (enc || ciphertext).
  enc, sender = suite.create_sender_context(public_key)
  ciphertext = sender.seal(ip_str.encode("utf-8"))

  # 4. Return unpadded Base64URL-encoded payload.
  return base64.urlsafe_b64encode(enc + ciphertext).decode("utf-8").rstrip("=")


JavaScript

この例では @hpke/core を使用しています。

/**
 * Encrypts an IP address using HPKE (RFC 9180).
 *
 * @param {string} pubXB64Url Base64URL-encoded 32-byte X25519 public key ('x').
 * @param {string} ipStr Plaintext IPv4 or IPv6 address string (for example,
 *     "203.0.113.42").
 * @return {!Promise<string>} Unpadded Base64URL-encoded payload
 *     (enc || ciphertext).
 */
async function encryptIp(pubXB64Url, ipStr) {
  // 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
  const pubBytes = Buffer.from(pubXB64Url, 'base64url');
  if (pubBytes.length !== 32) {
    throw new Error(
      `Public key must be exactly 32 bytes, got ${pubBytes.length}`,
    );
  }

  // 2. Configure HPKE cipher suite (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
  //    AES_256_GCM).
  const suite = new CipherSuite({
    kem: new DhkemX25519HkdfSha256(),
    kdf: new HkdfSha256(),
    aead: new Aes256Gcm(),
  });
  const rawPubKey = new Uint8Array(pubBytes).buffer;
  const publicKey = await suite.kem.importKey('raw', rawPubKey, true);

  // 3. Encrypt the UTF-8 IP address bytes and package (enc || ciphertext).
  const plaintext = new Uint8Array(Buffer.from(ipStr, 'utf8')).buffer;
  const {enc, ct} = await suite.seal(
    {recipientPublicKey: publicKey},
    plaintext,
  );

  // 4. Return unpadded Base64URL-encoded payload.
  return Buffer.concat([Buffer.from(enc), Buffer.from(ct)]).toString(
    'base64url',
  );
}

TypeScript

この例では @hpke/core を使用しています。

/**
 * Encrypts an IP address using HPKE (RFC 9180).
 *
 * @param pubXB64Url Base64URL-encoded 32-byte X25519 public key ('x').
 * @param ipStr Plaintext IPv4 or IPv6 address string (for example,
 *     "203.0.113.42").
 * @return Unpadded Base64URL-encoded payload (enc || ciphertext).
 */
export async function encryptIp(
  pubXB64Url: string,
  ipStr: string,
): Promise<string> {
  // 1. Decode the 32-byte X25519 public key 'x' from Base64URL.
  const pubBytes = Buffer.from(pubXB64Url, 'base64url');
  if (pubBytes.length !== 32) {
    throw new Error(
      `Public key must be exactly 32 bytes, got ${pubBytes.length}`,
    );
  }

  // 2. Configure HPKE cipher suite (DHKEM_X25519_HKDF_SHA256, HKDF_SHA256,
  //    AES_256_GCM).
  const suite = new CipherSuite({
    kem: new DhkemX25519HkdfSha256(),
    kdf: new HkdfSha256(),
    aead: new Aes256Gcm(),
  });
  const rawPubKey = new Uint8Array(pubBytes).buffer;
  const publicKey = await suite.kem.importKey('raw', rawPubKey, true);

  // 3. Encrypt the UTF-8 IP address bytes and package (enc || ciphertext).
  const plaintext = new Uint8Array(Buffer.from(ipStr, 'utf8')).buffer;
  const {enc, ct} = await suite.seal(
    {recipientPublicKey: publicKey},
    plaintext,
  );

  // 4. Return unpadded Base64URL-encoded payload.
  return Buffer.concat([Buffer.from(enc), Buffer.from(ct)]).toString(
    'base64url',
  );
}

次の暗号スイートとパラメータを使用して HPKE ライブラリを構成します。

パラメータ 値
鍵カプセル化メカニズム(KEM) DHKEM(X25519, HKDF-SHA256)(0x0020)
鍵導出関数(KDF) HKDF-SHA256(0x0001)
認証付き暗号(AEAD) AES-256-GCM(0x0002)
モード ベース(0x00)
アプリケーション情報(info) 空白("")
関連付けられたデータ(aad) 空白("")

暗号化された出力は、32 バイトのカプセル化されたエフェメラル公開鍵 enc と、16 バイトの GCM 認証タグを含む AEAD 暗号テキストの未加工の連結としてフォーマットします。Tink またはカスタム ヘッダー接頭辞は使用しません。結果をパディングなしの base64url 文字列としてエンコードします。

新しいエフェメラル鍵ペアを使用して、各ペイロードをスタンドアロンのシングルショット HPKE メッセージとして暗号化します。HPKE ライブラリがステートフルな送信者コンテキストを使用している場合は、複数のメッセージで再利用するのではなく、暗号化ごとに新しいコンテキストを作成します。ページ リクエストごとにクライアント IP アドレスを暗号化するか、結果の base64url 暗号テキスト文字列を IP アドレスごとに短い有効期間(TTL)でキャッシュに保存します。

クライアントサイドのワークフローを実装する

サーバーが暗号化された base64url IP ペイロードを生成したら、広告をリクエストする前に、クライアントの GPT にペイロードを渡します。

暗号化された IP を GPT に渡す

PageSettingsConfig の ipAddress プロパティを使用して、暗号化された IP 文字列を googletag.setConfig メソッドに渡します。

次の例では、サービスを有効にして広告スロットを表示する前に、サーバーで生成された暗号化された IP ペイロードを googletag.setConfig メソッドに挿入しています。

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Publisher-provided IP example</title>
    <script src="https://securepubads.g.doubleclick.net/tag/js/gpt.js" crossorigin="anonymous" async></script>
    <script>
      window.googletag = window.googletag || {cmd: []};

      // Inject the base64url-encoded encrypted IP generated by your server.
      const encryptedClientIp = 'ENCRYPTED_IP_PAYLOAD';

      googletag.cmd.push(function() {
        // Configure the encrypted IP before enabling services.
        googletag.setConfig({
          ipAddress: encryptedClientIp,
        });

        googletag
            .defineSlot('/6355419/Travel/Europe/France/Paris', [300, 250], 'banner-ad')
            .addService(googletag.pubads());
        googletag.enableServices();
      });
    </script>
  </head>
  <body>
    <div id="banner-ad" style="width: 300px; height: 250px;"></div>
    <script>
      googletag.cmd.push(function() {
        googletag.display('banner-ad');
      });
    </script>
  </body>
</html>

ENCRYPTED_IP_PAYLOAD は、サーバーが生成するパディングなしの base64url エンコード文字列に置き換えます。

ページで以前に構成した IP アドレスを消去するには、ipAddress プロパティを null 値に設定します。

googletag.setConfig({ ipAddress: null });

統合を確認する

GPT が暗号化された IP ペイロードを送信し、アド マネージャーがペイロードを復号することを確認するには、ブラウザのデベロッパー ツールと Google パブリッシャー コンソールを使用します。

ブラウザのデベロッパー ツール

  1. ブラウザのデベロッパー ツールを開きます。
  2. [ネットワーク] タブを選択します。
  3. /gampad/ads への広告リクエストをフィルタします。
  4. リクエスト URL に空でない ips クエリ パラメータが含まれていることを確認します。GPT は広告リクエストを送信する前に ipAddress 値をさらにエンコードするため、ips パラメータは暗号化された文字列と完全に一致しません。

Google サイト運営者コンソール

  1. コンソールを開くには、Google パブリッシャー コンソールのガイドの手順に沿って操作します。
  2. [Page Request] タブを選択します。
  3. ipAddress 警告が表示されないことを確認します。ペイロードに問題がある場合、コンソールに次のいずれかの警告が記録されます。

    • ipAddress プロパティに渡された値が、67 ~ 124 文字の有効な base64 または base64url 文字列でない場合、GPT はその値を無視し、広告リクエストから ips パラメータを省略して、次の警告をログに記録します。

      Invalid value encountered when calling: googletag.setConfig: {"ipAddress":"..."}. (string) ... was found at PageSettingsConfig.ipAddress. Invalid IP address format.
      
    • アド マネージャーが提供された IP ペイロードを復号できない場合、または復号された平文が有効な IPv4 または IPv6 アドレスでない場合、コンソールに次の警告が記録され、ネットワーク リクエストの IP アドレスにフォールバックします。

      Decryption failed on the provided encrypted client IP (supplied via googletag.setConfig({ipAddress: ...})).