Usar cookies firmadas

En esta página, se proporciona una descripción general de las cookies firmadas, además de instrucciones para usarlas con Cloud CDN. Las cookies firmadas otorgan acceso a recursos por tiempo limitado a un conjunto de archivos, sin importar si los usuarios tienen Cuentas de Google.

Las cookies firmadas son una alternativa a las URL firmadas. Estas cookies protegen el acceso cuando no es posible firmar decenas o cientos de URL por separado para cada usuario en la aplicación.

Las cookies firmadas te permiten hacer lo siguiente:

  • Autorizar a un usuario y proporcionarle un token por tiempo limitado para acceder al contenido protegido (en lugar de firmar cada URL)
  • Determinar el alcance del acceso del usuario a un prefijo de URL específico, como https://media.example.com/videos/, y otorgar al usuario autorizado acceso a contenido protegido dentro de ese prefijo de URL
  • Mantener las URL y los manifiestos de medios sin cambios, lo que simplifica la canalización de empaquetado y mejora la capacidad de almacenamiento en caché

En cambio, si deseas limitar el acceso a URL específicas, considera usar URL firmadas.

Antes de comenzar

Antes de usar cookies firmadas, haz lo siguiente:

  • Asegúrate de que Cloud CDN esté habilitado. Para obtener instrucciones, consulta Usa Cloud CDN. Puedes configurar las cookies firmadas en un backend antes de habilitar Cloud CDN, pero no tendrá efecto hasta que esté habilitado.

  • Si es necesario, actualiza a la última versión de Google Cloud CLI:

    gcloud components update
    

Para obtener una descripción general, consulta Descripción general de las URL firmadas y las cookies firmadas.

Configura las claves de solicitudes firmadas

Crear claves para URL firmadas o cookies firmadas requiere varios pasos, que se describen en las siguientes secciones.

Consideraciones de seguridad

Cloud CDN no valida las solicitudes en las siguientes circunstancias:

  • La solicitud no está firmada.
  • El servicio o bucket de backend para la solicitud no tiene Cloud CDN habilitado.

Las solicitudes firmadas siempre se deben validar en el origen antes de entregar la respuesta. Esto se debe a que los orígenes se pueden usar para entregar una combinación de contenido firmado y sin firmar, y debido a que un cliente puede acceder directamente al origen.

  • Cloud CDN no bloquea las solicitudes sin un parámetro de búsqueda Signature o una cookie HTTP Cloud-CDN-Cookie. Rechaza las solicitudes con parámetros de solicitud no válidos (o con formato incorrecto).
  • Cuando tu aplicación detecte una firma no válida, asegúrate de que la aplicación responda con un código de respuesta HTTP 403 (Unauthorized). Los códigos de respuesta de HTTP 403 no se pueden almacenar en caché.
  • Las respuestas a las solicitudes firmadas y sin firmar se almacenan en caché por separado, por lo que una respuesta correcta a una solicitud firmada válida nunca se usa para entregar una solicitud sin firmar.
  • Si la aplicación envía un código de respuesta que se puede almacenar en caché a una solicitud no válida, es posible que las solicitudes futuras válidas se rechacen de forma incorrecta.

En los backends de Cloud Storage, asegúrate de quitar el acceso público, de modo que Cloud Storage pueda rechazar solicitudes que no cuentan con una firma válida.

En la siguiente tabla, se resume el comportamiento.

La solicitud tiene firma Acierto de caché Comportamiento
No No Se reenvía al origen del backend.
No Se entrega desde la caché.
No Se valida la firma. Si es válida, se reenvía al origen del backend.
Se valida la firma. Si es válida, se entrega desde la caché.

Crea claves de solicitudes firmadas

Para habilitar la compatibilidad con las cookies firmadas y las URL firmadas de Cloud CDN, crea una o más claves en un servicio de backend o un bucket de backend con Cloud CDN habilitado, o en ambos.

Para cada servicio de backend o bucket de backend, puedes crear y borrar claves según las necesidades de seguridad. Cada backend puede tener hasta tres claves configuradas a la vez. Te recomendamos rotar las claves de forma periódica. Para ello, borra la clave más antigua, agrega una nueva y úsala cuando firmes las URL o las cookies.

Puedes usar el mismo nombre de clave en varios servicios de backend y buckets de backend, porque cada conjunto de claves es independiente de los demás. Los nombres de claves pueden tener hasta 63 caracteres. Para nombrar tus claves, usa los caracteres de la “A” a la “Z”, de la “a” a la “z”, dígitos del 0 al 9, _ (guion bajo) y - (guion).

Cuando crees claves, asegúrate de mantenerlas seguras, ya que cualquier persona que tenga una de las claves puede crear URL firmadas o cookies firmadas que Cloud CDN acepte hasta que la clave se borre de Cloud CDN. Las claves se almacenan en la computadora en la que generas las URL firmadas o cookies firmadas. Cloud CDN también almacena las claves para verificar las firmas de las solicitudes.

Para mantener las claves en secreto, los valores de las claves no se incluyen en las respuestas a ninguna solicitud a la API. Si pierdes una clave, debes crear una nueva.

Para crear una clave de solicitud firmada, sigue estos pasos.

Console

  1. En la consola de Google Cloud, ve a la página Cloud CDN.

    Ir a Cloud CDN

  2. Haz clic en el nombre del origen al que deseas agregar la clave.
  3. En la página Detalles del origen, haz clic en el botón Editar.
  4. En la sección Aspectos básicos del origen, haz clic en Siguiente para abrir la sección Reglas de host y ruta de acceso.
  5. En la sección Reglas de host y ruta de acceso, haz clic en Siguiente para abrir la sección Rendimiento de la caché.
  6. En la sección Contenido restringido, selecciona Restringir el acceso con URLs firmadas y cookies firmadas.
  7. Haz clic en Agregar clave de firma.

    1. Especifica un nombre único para la nueva clave de firma.
    2. En la sección Método de creación de claves, selecciona Generar automáticamente. También puedes hacer clic en Permitirme ingresar y, luego, especificar un valor de clave de firma.

      En el caso de la primera opción, copia el valor de la clave de firma generada automáticamente a un archivo privado, que puedes usar para crear URLs firmadas.

    3. Haz clic en Listo.

    4. En la sección Antigüedad máxima de la entrada en la caché, ingresa un valor y, luego, selecciona una unidad de tiempo.

  8. Haz clic en Listo.

gcloud

La herramienta de línea de comandos de gcloud lee las claves de un archivo local que especifiques. Para crear un archivo de claves, genera 128 bits altamente aleatorios, codifícalos con base64 y, luego, reemplaza el carácter + por -/ por _. Para obtener más información, consulta RFC 4648. Es fundamental que la clave sea altamente aleatoria. En un sistema similar a UNIX, puedes generar una clave altamente aleatoria y almacenarla en el archivo de claves con el siguiente comando:

head -c 16 /dev/urandom | base64 | tr +/ -_ > KEY_FILE_NAME

Para agregar la clave a un servicio de backend, usa lo siguiente:

gcloud compute backend-services \
   add-signed-url-key BACKEND_NAME \
   --key-name KEY_NAME \
   --key-file KEY_FILE_NAME

Para agregar la clave a un bucket de backend, usa lo siguiente:

gcloud compute backend-buckets \
   add-signed-url-key BACKEND_NAME \
   --key-name KEY_NAME \
   --key-file KEY_FILE_NAME

Configura los permisos de Cloud Storage

Si usas Cloud Storage y restringiste quién puede leer los objetos, debes otorgar permiso a Cloud CDN a fin de que los lea. Para ello, agrega la cuenta de servicio de Cloud CDN a las LCA de Cloud Storage.

No necesitas crear la cuenta de servicio. La cuenta de servicio se crea de forma automática la primera vez que agregas una clave a un bucket de backend en un proyecto.

Antes de ejecutar el siguiente comando, agrega al menos una clave a un bucket de backend en tu proyecto. De lo contrario, el comando falla con un error porque la cuenta de servicio de llenado de caché de Cloud CDN no se crea hasta que agregas una o más claves al proyecto.

gcloud storage buckets add-iam-policy-binding gs://BUCKET \
  --member=serviceAccount:service-PROJECT_NUM@cloud-cdn-fill.iam.gserviceaccount.com \
  --role=roles/storage.objectViewer

Reemplaza PROJECT_NUM por el número de proyecto y BUCKET por el bucket de almacenamiento.

La cuenta de servicio de Cloud CDN, service-PROJECT_NUM@cloud-cdn-fill.iam.gserviceaccount.com, no aparece en la lista de cuentas de servicio del proyecto. Esto se debe a que la cuenta de servicio de Cloud CDN es propiedad de Cloud CDN, no del proyecto.

Para obtener más información sobre los números de proyecto, consulta Identifica el ID y el número del proyecto en la documentación de ayuda de la consola de Google Cloud.

Personaliza el tiempo máximo de caché

Cloud CDN almacena en caché las respuestas para las solicitudes firmadas, sin importar el encabezado Cache-Control del backend. El tiempo máximo en que las respuestas pueden almacenarse en caché sin revalidación se establece mediante la marca signed-url-cache-max-age, cuyo valor predeterminado es de una hora y puede modificarse como se muestra a continuación.

A fin de establecer el tiempo máximo de caché para un servicio o un bucket de backend, ejecuta uno de los siguientes comandos:

gcloud compute backend-services update BACKEND_NAME
  --signed-url-cache-max-age MAX_AGE
gcloud compute backend-buckets update BACKEND_NAME
  --signed-url-cache-max-age MAX_AGE

Enumera los nombres de claves de solicitudes firmadas

Para enumerar las claves en un servicio de backend o en un bucket de backend, ejecuta uno de los siguientes comandos:

gcloud compute backend-services describe BACKEND_NAME
gcloud compute backend-buckets describe BACKEND_NAME

Borra claves de solicitudes firmadas

Cuando ya no deben respetarse las URL que tienen la firma de una clave particular, ejecuta uno de los siguientes comandos para borrar esa clave del servicio de backend o del bucket de backend:

gcloud compute backend-services \
   delete-signed-url-key BACKEND_NAME --key-name KEY_NAME
gcloud compute backend-buckets \
   delete-signed-url-key BACKEND_NAME --key-name KEY_NAME

Crea una política

Las políticas de cookies firmadas son una serie de pares key-value (delimitados por el carácter :), similares a los parámetros de búsqueda usados en una URL firmada. Para ver ejemplos, consulta Emite cookies a los usuarios.

Las políticas representan los parámetros para los que una solicitud es válida. Las políticas se firman mediante un código de autenticación de mensajes basado en hash (HMAC) que Cloud CDN valida en cada solicitud.

Define los campos y el formato de políticas

Existen cuatro campos obligatorios que debes definir en el siguiente orden:

  • URLPrefix
  • Expires
  • KeyName
  • Signature

En los pares key-value de una política de cookies firmadas, se distinguen mayúsculas de minúsculas.

URLPrefix

URLPrefix denota un prefijo de URL codificado en base64 seguro para URL que incluye todas las rutas de acceso en las que debe ser válida la firma.

Un URLPrefix codifica un esquema (http:// o https://), un FQDN y una ruta opcional. Finalizar la ruta con una / es opcional, pero se recomienda. El prefijo no debe incluir parámetros de búsqueda o fragmentos como ?#.

Por ejemplo, https://media.example.com/videos hace coincidir las solicitudes con ambas de las siguientes opciones:

  • https://media.example.com/videos?video_id=138183&user_id=138138
  • https://media.example.com/videos/137138595?quality=low

La ruta del prefijo se usa como una substring de texto, no estrictamente como una ruta del directorio. Por ejemplo, el prefijo https://example.com/data otorga acceso a lo siguiente:

  • /data/file1
  • /database

A fin de evitar este error, te recomendamos finalizar todos los prefijos con /, a menos que decidas finalizar el prefijo de forma intencional con un nombre de archivo parcial, como https://media.example.com/videos/123, para otorgar acceso a lo siguiente:

  • /videos/123_chunk1
  • /videos/123_chunk2
  • /videos/123_chunkN

Si la URL solicitada no coincide con URLPrefix, Cloud CDN rechaza la solicitud y muestra un error HTTP 403 al cliente.

Expires

Expires debe ser una marca de tiempo de Unix (la cantidad de segundos desde el 1 de enero de 1970).

KeyName

KeyName es el nombre de una clave creada para el bucket o servicio de backend. En los nombres de clave, se distinguen mayúsculas de minúsculas.

Signature

Signature es la firma HMAC-SHA-1 codificada en base64 segura para URL de los campos que conforman la política de cookies. Esto se valida en cada solicitud. Las solicitudes con una firma no válida se rechazan con un error HTTP 403.

Crea cookies firmadas de manera programática

En las siguientes muestras de código, se indica cómo crear cookies firmadas de manera programática.

Go

import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"time"
)

// signCookie creates a signed cookie for an endpoint served by Cloud CDN.
//
// - urlPrefix must start with "https://" and should include the path prefix
// for which the cookie will authorize access to.
// - key should be in raw form (not base64url-encoded) which is
// 16-bytes long.
// - keyName must match a key added to the backend service or bucket.
func signCookie(urlPrefix, keyName string, key []byte, expiration time.Time) (string, error) {
	encodedURLPrefix := base64.URLEncoding.EncodeToString([]byte(urlPrefix))
	input := fmt.Sprintf("URLPrefix=%s:Expires=%d:KeyName=%s",
		encodedURLPrefix, expiration.Unix(), keyName)

	mac := hmac.New(sha1.New, key)
	mac.Write([]byte(input))
	sig := base64.URLEncoding.EncodeToString(mac.Sum(nil))

	signedValue := fmt.Sprintf("%s:Signature=%s",
		input,
		sig,
	)

	return signedValue, nil
}

Java

import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.time.ZonedDateTime;
import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class SignedCookies {

  public static void main(String[] args) throws Exception {
    // TODO(developer): Replace these variables before running the sample.

    // The name of the signing key must match a key added to the back end bucket or service.
    String keyName = "YOUR-KEY-NAME";
    // Path to the URL signing key uploaded to the backend service/bucket.
    String keyPath = "/path/to/key";
    // The Unix timestamp that the signed URL expires.
    long expirationTime = ZonedDateTime.now().plusDays(1).toEpochSecond();
    // URL prefix to sign as a string. URL prefix must start with either "http://" or "https://"
    // and must not include query parameters.
    String urlPrefix = "https://media.example.com/videos/";

    // Read the key as a base64 url-safe encoded string, then convert to byte array.
    // Key used in signing must be in raw form (not base64url-encoded).
    String base64String = new String(Files.readAllBytes(Paths.get(keyPath)),
        StandardCharsets.UTF_8);
    byte[] keyBytes = Base64.getUrlDecoder().decode(base64String);

    // Create signed cookie from policy.
    String signedCookie = signCookie(urlPrefix, keyBytes, keyName, expirationTime);
    System.out.println(signedCookie);
  }

  // Creates a signed cookie for the specified policy.
  public static String signCookie(String urlPrefix, byte[] key, String keyName,
      long expirationTime)
      throws InvalidKeyException, NoSuchAlgorithmException {

    // Validate input URL prefix.
    try {
      URL validatedUrlPrefix = new URL(urlPrefix);
      if (!validatedUrlPrefix.getProtocol().startsWith("http")) {
        throw new IllegalArgumentException(
            "urlPrefix must start with either http:// or https://: " + urlPrefix);
      }
      if (validatedUrlPrefix.getQuery() != null) {
        throw new IllegalArgumentException("urlPrefix must not include query params: " + urlPrefix);
      }
    } catch (MalformedURLException e) {
      throw new IllegalArgumentException(
          "urlPrefix malformed: " + urlPrefix);
    }

    String encodedUrlPrefix = Base64.getUrlEncoder().encodeToString(urlPrefix.getBytes(
        StandardCharsets.UTF_8));
    String policyToSign = String.format("URLPrefix=%s:Expires=%d:KeyName=%s", encodedUrlPrefix,
        expirationTime, keyName);

    String signature = getSignatureForUrl(key, policyToSign);
    return String.format("Cloud-CDN-Cookie=%s:Signature=%s", policyToSign, signature);
  }

  // Creates signature for input string with private key.
  private static String getSignatureForUrl(byte[] privateKey, String input)
      throws InvalidKeyException, NoSuchAlgorithmException {

    final String algorithm = "HmacSHA1";
    final int offset = 0;
    Key key = new SecretKeySpec(privateKey, offset, privateKey.length, algorithm);
    Mac mac = Mac.getInstance(algorithm);
    mac.init(key);
    return Base64.getUrlEncoder()
        .encodeToString(mac.doFinal(input.getBytes(StandardCharsets.UTF_8)));
  }
}

Python

import argparse
import base64
from datetime import datetime, timezone
import hashlib
import hmac
from urllib.parse import parse_qs, urlsplit


def sign_cookie(
    url_prefix: str,
    key_name: str,
    base64_key: str,
    expiration_time: datetime,
) -> str:
    """Gets the Signed cookie value for the specified URL prefix and configuration.

    Args:
        url_prefix: URL prefix to sign.
        key_name: name of the signing key.
        base64_key: signing key as a base64 encoded string.
        expiration_time: expiration time as time-zone aware datetime.

    Returns:
        Returns the Cloud-CDN-Cookie value based on the specified configuration.
    """
    encoded_url_prefix = base64.urlsafe_b64encode(
        url_prefix.strip().encode("utf-8")
    ).decode("utf-8")
    epoch = datetime.fromtimestamp(0, timezone.utc)
    expiration_timestamp = int((expiration_time - epoch).total_seconds())
    decoded_key = base64.urlsafe_b64decode(base64_key)

    policy = f"URLPrefix={encoded_url_prefix}:Expires={expiration_timestamp}:KeyName={key_name}"

    digest = hmac.new(decoded_key, policy.encode("utf-8"), hashlib.sha1).digest()
    signature = base64.urlsafe_b64encode(digest).decode("utf-8")

    signed_policy = f"Cloud-CDN-Cookie={policy}:Signature={signature}"

    return signed_policy

Valida las cookies firmadas

El proceso de validación de una cookie firmada es básicamente el mismo que el de generar una cookie firmada. Por ejemplo, supongamos que deseas validar el siguiente encabezado de cookie firmada:

Cookie: Cloud-CDN-Cookie=URLPrefix=URL_PREFIX:Expires=EXPIRATION:KeyName=KEY_NAME:Signature=SIGNATURE; Domain=media.example.com; Path=/; Expires=Tue, 20 Aug 2019 02:26:49 GMT; HttpOnly

Puedes usar la clave secreta llamada KEY_NAME para generar la firma de forma independiente y, luego, validar que coincida con SIGNATURE.

Emite cookies a los usuarios

Tu aplicación debe generar una única cookie HTTP que contenga una política firmada de forma correcta y emitirla a cada usuario (cliente).

  1. Crea un firmante HMAC-SHA-1 en el código de la aplicación.

  2. Usa la clave elegida para firmar la política y toma nota del nombre de clave que agregaste al backend, como mySigningKey.

  3. Crea una política de cookies con el siguiente formato; ten en cuenta que en el nombre y el valor, se distinguen mayúsculas de minúsculas.

    Name: Cloud-CDN-Cookie
    Value: URLPrefix=$BASE64URLECNODEDURLORPREFIX:Expires=$TIMESTAMP:KeyName=$KEYNAME:Signature=$BASE64URLENCODEDHMAC
    

    Ejemplo de encabezado Set-Cookie:

    Set-Cookie: Cloud-CDN-Cookie=URLPrefix=aHR0cHM6Ly9tZWRpYS5leGFtcGxlLmNvbS92aWRlb3Mv:Expires=1566268009:KeyName=mySigningKey:Signature=0W2xlMlQykL2TG59UZnnHzkxoaw=; Domain=media.example.com; Path=/; Expires=Tue, 20 Aug 2019 02:26:49 GMT; HttpOnly
    

    Los atributos Domain y Path de la cookie determinan si el cliente envía la cookie a Cloud CDN.

Recomendaciones y requisitos

  • Establece de forma explícita Domain y Path para que coincidan con el dominio y el prefijo de la ruta de acceso desde los que deseas publicar el contenido protegido, que puede diferir del dominio y la ruta en la que se emite la cookie (example.com en comparación con media.example.com o /browse en comparación con /videos).

  • Asegúrate de tener solo una cookie con un nombre determinado para el mismo Domain y Path.

  • Asegúrate de no emitir cookies en conflicto, ya que esto podría impedir el acceso al contenido en otras sesiones del navegador (ventanas o pestañas).

  • Configura las marcas Secure y HttpOnly cuando corresponda. Secure garantiza que la cookie se envíe solo a través de conexiones HTTPS. HttpOnly evita que la cookie esté disponible para JavaScript.

  • Los atributos de cookies Expires y Max-Age son opcionales. Si los omites, la cookie existirá mientras exista la sesión del navegador (pestaña o ventana).

  • En un llenado de caché o error de caché, la cookie firmada se pasa a través del origen definido en el servicio de backend. Asegúrate de validar el valor de la cookie firmada en cada solicitud antes de entregar contenido.