Los proxies de privacidad del navegador y la configuración de protección de IP pueden enmascarar las direcciones IP de los clientes en las solicitudes de anuncios de terceros a Google Ad Manager. Las direcciones IP enmascaradas pueden reducir la precisión de la segmentación geográfica y la relevancia de los anuncios. Como tu servidor web o red de distribución de contenidos (CDN) interactúa con el usuario en un contexto propio, tu servidor recibe la dirección IP sin enmascarar del usuario.
La función de IP proporcionada por el publicador te permite capturar la dirección IPv4 o IPv6 sin enmascarar del usuario en tu servidor, encriptar la dirección con la encriptación híbrida de clave pública (HPKE) y pasar la carga útil encriptada a Google Publisher Tag (GPT) en el cliente.
En esta página, se explica cómo integrar la IP proporcionada por el publicador con GPT.
Implementa el flujo de trabajo del servidor
Para evitar exponer la dirección IP de texto sin formato del usuario a secuencias de comandos de terceros en el navegador, realiza los siguientes pasos en tu servidor web o trabajador perimetral de CDN:
- Realizar toda la captura de IP
- Recupera la clave pública.
- Realiza la encriptación de HPKE.
Captura la dirección IP del cliente
Extrae la dirección IPv4 o IPv6 sin enmascarar del cliente de la solicitud de página entrante en tu servidor web o CDN:
- Conexiones directas: Lee la dirección del socket remoto de la conexión HTTP entrante.
- Detrás de una CDN, un balanceador de cargas o un proxy inverso: Lee el encabezado de IP del cliente
desde tu proxy perimetral de confianza, como el encabezado
X-Forwarded-Foro un encabezado de IP del cliente específico de la CDN. Confía solo en los encabezados de reenvío que tu infraestructura de proxy agrega o anula. Cuando el encabezadoX-Forwarded-Forcontiene varias direcciones separadas por comas, selecciona la IP del cliente que agregó el proxy perimetral de confianza.
Ad Manager admite las siguientes direcciones:
- IPv4, por ejemplo,
203.0.113.42. - IPv6, por ejemplo,
2001:db8::8a2e:370:7334.
Pasa solo la cadena de dirección IP sin formato, sin números de puerto, corchetes, prefijos CIDR ni espacios en blanco circundantes.
Recupera y almacena en caché la clave pública
Google publica la clave de encriptación de HPKE activa como un conjunto de claves web JSON (JWKS) de RFC 8037 en el siguiente extremo:
https://www.gstatic.com/real-ip/ip_enc_key.json
Una solicitud GET a este extremo devuelve un objeto JSON, cuyo array keys
contiene un solo objeto de clave activa. Usa la clave en la propiedad x de ese objeto para encriptar la dirección IP del cliente.
En el siguiente ejemplo, se recupera la clave pública y se extrae la propiedad x si se
realiza correctamente, y se arroja una excepción si falla:
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;
}
Si se ejecuta de forma correcta, la respuesta incluye el array keys, que contiene un objeto de clave
con la propiedad x:
{
"keys": [
{
"kty": "OKP",
"crv": "X25519",
"x": "7bws5TIHOUrcJNofEubW3DPW8zR9EtjFNLm_Grv98TA",
"kid": "EZMLFw"
}
]
}
En la siguiente tabla, se describen las propiedades del objeto de clave en el array
keys:
| Propiedad | Descripción |
|---|---|
crv |
Es el identificador de curva elíptica. Siempre X25519. |
kid |
Identificador de clave codificado en base64url sin relleno, que cambia cuando Google rota la clave. |
kty |
Tipo de clave. Siempre es OKP (par de claves de octeto). |
x |
Es la clave pública X25519 sin procesar de 32 bytes codificada en base64url sin relleno que se usa para la encriptación. |
Almacena en caché la clave pública recuperada en la memoria de tu servidor y actualízala al
menos una vez cada 24 horas o antes, según el encabezado de respuesta HTTP
Cache-Control. No recuperes la clave pública en cada solicitud de página entrante.
Google conserva las claves privadas rotadas. Las cargas útiles encriptadas con una clave pública almacenada en caché
en las 24 horas anteriores se siguen desencriptando sin interrupciones durante la rotación
de claves.
Encripta la dirección IP
Encripta la representación de bytes UTF-8 de la cadena de dirección IP del cliente con
HPKE (RFC 9180) y la clave pública X25519 de 32 bytes decodificada, x,
recuperada en Recupera y almacena en caché la clave pública.
En el siguiente ejemplo, se encripta una cadena de dirección IP:
Java
En este ejemplo, se usa Tink (com.google.crypto.tink:tink 1.16.0 o
una versión posterior):
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
En este ejemplo, se usa 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
En este ejemplo, se usa @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
En este ejemplo, se usa @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',
);
}
Configura tu biblioteca de HPKE con el siguiente conjunto de algoritmos de cifrado y parámetros:
| Parámetro | Valor |
|---|---|
| Mecanismo de encapsulamiento de claves (KEM) | DHKEM(X25519, HKDF-SHA256) (0x0020) |
| Función de derivación de claves (KDF) | HKDF-SHA256 (0x0001) |
| Encriptación autenticada con datos asociados (AEAD) | AES-256-GCM (0x0002) |
| Modo | Base (0x00) |
Información de la aplicación (info) |
Vacío ("") |
Datos asociados (aad) |
Vacío ("") |
Asigna el formato de concatenación sin procesar de la clave pública efímera encapsulada de 32 bytes, enc, y el texto cifrado de AEAD, incluida la etiqueta de autenticación de GCM de 16 bytes, sin Tink ni prefijo de encabezado personalizado, al resultado encriptado. Codifica el resultado como
una cadena base64url sin relleno.
Encripta cada carga útil como un mensaje de HPKE independiente de un solo intento con un par de claves efímeras nuevas. Si tu biblioteca de HPKE usa un contexto de remitente con estado, crea un contexto nuevo para cada encriptación en lugar de reutilizarlo en varios mensajes. Encripta la dirección IP del cliente por solicitud de página o almacena en caché la cadena de texto cifrado base64url resultante por dirección IP durante un tiempo de actividad (TTL) breve.
Implementa el flujo de trabajo del cliente
Después de que tu servidor genere la carga útil de IP base64url encriptada, pásala a GPT en el cliente antes de solicitar anuncios.
Pasa la IP encriptada a GPT
Pasa la cadena de IP encriptada al método googletag.setConfig con la propiedad ipAddress en PageSettingsConfig.
En el siguiente ejemplo, se inserta la carga útil de IP encriptada generada por el servidor en el método googletag.setConfig antes de habilitar los servicios y mostrar un espacio publicitario:
<!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>Reemplaza ENCRYPTED_IP_PAYLOAD por la cadena codificada en base64url sin relleno que produce tu servidor.
Para borrar una dirección IP configurada con anterioridad en la página, establece la propiedad ipAddress
en el valor null:
googletag.setConfig({ ipAddress: null });
Verifica la integración
Para verificar que GPT envíe tu carga útil de IP encriptada y que Ad Manager la desencripte, usa las herramientas para desarrolladores de tu navegador y la Consola para publicadores de Google:
Herramientas para desarrolladores del navegador
- Abre las herramientas para desarrolladores de tu navegador.
- Selecciona la pestaña Red.
- Filtra las solicitudes de anuncios para
/gampad/ads. - Confirma que la URL de la solicitud incluya un parámetro de consulta
ipsque no esté vacío. Debido a que GPT codifica aún más el valoripAddressantes de enviar la solicitud de anuncio, el parámetroipsno coincide con tu cadena encriptada de forma literal.
Consola para editores de Google
- Para abrir la consola, sigue los pasos que se indican en la guía de la Consola para publicadores de Google.
- Selecciona la pestaña Solicitud de página.
Confirma que no aparezcan advertencias de
ipAddress. Si hay un problema con la carga útil, la consola registra una de las siguientes advertencias:Si el valor que se pasa a la propiedad
ipAddressno es una cadena base64 o base64url válida de entre 67 y 124 caracteres, GPT ignora el valor, omite el parámetroipsde la solicitud de anuncio y registra la siguiente advertencia:Invalid value encountered when calling: googletag.setConfig: {"ipAddress":"..."}. (string) ... was found at PageSettingsConfig.ipAddress. Invalid IP address format.Si Ad Manager no puede desencriptar la carga útil de IP proporcionada o si el texto sin formato desencriptado no es una dirección IPv4 o IPv6 válida, la consola registra la siguiente advertencia y recurre a la dirección IP de la solicitud de red:
Decryption failed on the provided encrypted client IP (supplied via googletag.setConfig({ipAddress: ...})).