Usar cookies firmadas

En esta página se ofrece una descripción general de las cookies firmadas y se explica cómo usarlas con Cloud CDN. Las cookies firmadas proporcionan acceso a recursos durante un tiempo limitado a un conjunto de archivos, independientemente de si los usuarios tienen cuentas de Google.

Las cookies firmadas son una alternativa a las URLs firmadas. Protegen el acceso cuando no es viable firmar por separado decenas o cientos de URLs para cada usuario en tu aplicación.

Las cookies firmadas te permiten hacer lo siguiente:

  • Autoriza a un usuario y proporciónale un token con un tiempo limitado para acceder a tu contenido protegido (en lugar de firmar cada URL).
  • Limita el acceso del usuario a un prefijo de URL específico, como https://media.example.com/videos/, y concede al usuario autorizado acceso solo al contenido protegido de ese prefijo de URL.
  • Mantenga sin cambios sus URLs y manifiestos multimedia para simplificar su proceso de empaquetado y mejorar la capacidad de almacenamiento en caché.

Si quiere limitar el acceso a URLs específicas, le recomendamos que use URLs firmadas.

Antes de empezar

Antes de usar cookies firmadas, haga lo siguiente:

  • Asegúrate de que Cloud CDN esté habilitado. Para obtener instrucciones, consulta el artículo Usar Cloud CDN. Puedes configurar cookies firmadas en un backend antes de habilitar Cloud CDN, pero no tendrán ningún efecto hasta que Cloud CDN esté habilitado.

  • Si es necesario, actualiza a la versión más reciente de la CLI de Google Cloud:

    gcloud components update
    

Para obtener una descripción general, consulta URLs y cookies firmadas.

Configurar claves de solicitudes firmadas

Para crear claves para sus URLs firmadas o cookies firmadas, debe seguir varios pasos, que se describen en las siguientes secciones.

Cuestiones sobre seguridad

Cloud CDN no valida las solicitudes en las siguientes circunstancias:

  • La solicitud no está firmada.
  • El servicio de backend o el segmento de backend de la solicitud no tiene habilitada la CDN de Cloud.

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

  • Cloud CDN no bloquea las solicitudes que no tienen el parámetro de consulta Signature ni la cookie HTTP Cloud-CDN-Cookie. Rechaza las solicitudes con parámetros no válidos (o con un formato incorrecto).
  • Cuando tu aplicación detecte una firma no válida, asegúrate de que responda con el código de respuesta HTTP 403 (Unauthorized). Los códigos de respuesta HTTP 403 no se pueden almacenar en caché.
  • Las respuestas a 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 utiliza para servir una solicitud sin firmar.
  • Si tu aplicación envía un código de respuesta que se puede almacenar en caché a una solicitud no válida, es posible que se rechacen incorrectamente solicitudes válidas en el futuro.

En el caso de los back-ends de Cloud Storage, asegúrate de eliminar el acceso público para que Cloud Storage pueda rechazar las solicitudes que no tengan una firma válida.

En la siguiente tabla se resume el comportamiento.

Request has signature Resultado en caché Comportamiento
No No Reenvía la solicitud al origen del backend.
No Servir desde la caché.
No Validar firma. Si es válido, reenvíalo al origen backend.
Validar firma. Si es válido, se sirve desde la caché.

Crear claves de solicitud firmadas

Para habilitar la compatibilidad con las URLs y las cookies firmadas de Cloud CDN, debes crear una o varias claves en un servicio de backend o un segmento de backend (o ambos) que tenga habilitado Cloud CDN.

En cada servicio o segmento de backend, puedes crear y eliminar claves según tus necesidades de seguridad. Cada backend puede tener hasta tres claves configuradas a la vez. Te recomendamos que rotes periódicamente tus claves eliminando la más antigua, añadiendo una nueva y usando la nueva clave al firmar URLs o cookies.

Puedes usar el mismo nombre de clave en varios servicios de backend y segmentos de backend, ya que cada conjunto de claves es independiente de los demás. Los nombres de las claves pueden tener hasta 63 caracteres. Para asignar nombres a las claves, utiliza los caracteres A-Z, a-z, 0-9, _ (guion bajo) y - (guion).

Cuando crees claves, asegúrate de que estén protegidas, ya que cualquier persona que tenga una de tus claves podrá crear URLs firmadas o cookies firmadas que Cloud CDN acepte hasta que se elimine la clave de Cloud CDN. Las claves se almacenan en el ordenador en el que generas las URLs o las 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 de API. Si pierdes una clave, debes crear otra.

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

Consola

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

    Ir a Cloud CDN

  2. Haga clic en el nombre del origen al que quiera añadir la clave.
  3. En la página Detalles del origen, haga clic en el botón Editar.
  4. En la sección Información básica del origen, haz clic en Siguiente para abrir la sección Reglas de host y ruta.
  5. En la sección Reglas de host y ruta, haga clic en Siguiente para abrir la sección Rendimiento de la caché.
  6. En la sección Contenido restringido, selecciona Restringir acceso mediante URLs y cookies firmadas.
  7. Haz clic en Añadir 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, seleccione Generar automáticamente. También puedes hacer clic en Quiero introducirla y 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 en 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 de caché, introduce un valor y, a continuación, selecciona una unidad de tiempo.

  8. Haz clic en Listo.

gcloud

La herramienta de línea de comandos gcloud lee las claves de un archivo local que especifiques. El archivo de claves debe crearse generando 128 bits aleatorios, codificándolos con Base64 y, a continuación, sustituyendo el carácter + por - y el carácter / por _. Para obtener más información, consulta RFC 4648. Es fundamental que la clave sea aleatoria. En un sistema de tipo UNIX, puedes generar una clave aleatoria segura y almacenarla en el archivo de claves con el siguiente comando:

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

Para añadir la clave a un servicio backend, sigue estos pasos:

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

Para añadir la clave a un backend, sigue estos pasos:

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

Configurar permisos de Cloud Storage

Si usas Cloud Storage y has restringido quién puede leer los objetos, debes dar permiso a Cloud CDN para que lea los objetos añadiendo la cuenta de servicio de Cloud CDN a las ACLs de Cloud Storage.

No es necesario que crees la cuenta de servicio. La cuenta de servicio se crea automáticamente la primera vez que añades una clave a un backend de un proyecto.

Antes de ejecutar el siguiente comando, añade al menos una clave a un backend de tu proyecto. De lo contrario, el comando fallará y se producirá un error porque la cuenta de servicio de relleno de caché de Cloud CDN no se crea hasta que añadas 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

Sustituye PROJECT_NUM por el número de tu proyecto y BUCKET por tu segmento 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 de tu proyecto. Esto se debe a que la cuenta de servicio de Cloud CDN es propiedad de Cloud CDN, no de tu proyecto.

Para obtener más información sobre los números de proyecto, consulta el artículo Buscar el ID y el número de proyecto de la documentación de ayuda de la consola de Google Cloud .

Personalizar el tiempo máximo de la caché

La CDN de Cloud almacena en caché las respuestas de las solicitudes firmadas, independientemente del encabezado Cache-Control del backend. El tiempo máximo que se pueden almacenar en caché las respuestas sin volver a validarlas se define mediante la marca signed-url-cache-max-age, que tiene un valor predeterminado de una hora y se puede modificar como se muestra aquí.

Para definir el tiempo máximo de almacenamiento en caché de un servicio de backend o un segmento 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

Lista de nombres de claves de solicitudes firmadas

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

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

Eliminar claves de solicitud firmada

Cuando las URLs firmadas por una clave concreta ya no deban aceptarse, ejecuta uno de los siguientes comandos para eliminar esa clave del servicio backend o del backend del contenedor:

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

Crear 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 consulta que se usan en una URL firmada. Para ver ejemplos, consulta Emitir cookies a los usuarios.

Las políticas representan los parámetros para los que es válida una solicitud. 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.

Definir el formato y los campos de la política

Hay cuatro campos obligatorios que debe definir en el siguiente orden:

  • URLPrefix
  • Expires
  • KeyName
  • Signature

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

URLPrefix

URLPrefix indica un prefijo de URL codificado en base64 seguro para URLs que abarca todas las rutas para las que debe ser válida la firma.

Un URLPrefix codifica un esquema (http:// o https://), un FQDN y una ruta opcional. Terminar la ruta con una / es opcional, pero recomendable. El prefijo no debe incluir parámetros de consulta ni fragmentos, como ? o #.

Por ejemplo, https://media.example.com/videos coincide con las solicitudes de los dos elementos siguientes:

  • 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 subcadena de texto, no como una ruta de directorio estricta. Por ejemplo, el prefijo https://example.com/data concede acceso a lo siguiente:

  • /data/file1
  • /database

Para evitar este error, te recomendamos que termines todos los prefijos con /, a menos que quieras terminar el prefijo con un nombre de archivo parcial, como https://media.example.com/videos/123, para conceder acceso a lo siguiente:

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

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

Caducidad

Expires debe ser una marca de tiempo de Unix (el número de segundos transcurridos desde el 1 de enero de 1970).

KeyName

KeyName es el nombre de la clave creada en el backend o en el servicio de backend. En los nombres de las claves se distingue entre mayúsculas y minúsculas.

Firma

Signature es la firma HMAC-SHA-1 codificada en Base64 segura para URLs de los campos que componen la política de cookies. Esta validación se realiza en cada solicitud. Las solicitudes con una firma no válida se rechazan con un error HTTP 403.

Crear cookies firmadas mediante programación

En los siguientes ejemplos de código se muestra cómo crear cookies firmadas mediante programación.

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

Validar cookies firmadas

El proceso de validación de una cookie firmada es esencialmente el mismo que el de generación de una cookie firmada. Por ejemplo, supongamos que quiere validar la siguiente cabecera 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 denominada por KEY_NAME para generar la firma de forma independiente y, a continuación, validar que coincide con SIGNATURE.

Emitir cookies a los usuarios

Tu aplicación debe generar y emitir a cada usuario (cliente) una sola cookie HTTP que contenga una política firmada correctamente:

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

  2. Firma la política con la clave elegida y anota el nombre de la clave que has añadido al backend, como mySigningKey.

  3. Crea una política de cookies con el siguiente formato. Ten en cuenta que tanto el nombre como el valor distinguen entre mayúsculas y 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

  • Define explícitamente los atributos Domain y Path para que coincidan con el prefijo de dominio y de ruta desde el que quieras servir el contenido protegido, que puede ser diferente del dominio y la ruta en los que se emite la cookie (example.com frente a media.example.com o /browse frente a /videos).

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

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

  • Define las marcas Secure y HttpOnly cuando proceda. Secure asegura que la cookie solo se envíe a través de conexiones HTTPS. HttpOnly impide que la cookie esté disponible para JavaScript.

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

  • En caso de que se llene la caché o no se encuentre el contenido en ella, la cookie firmada se transfiere al origen definido en el servicio backend. Asegúrate de validar el valor de la cookie firmada en cada solicitud antes de servir contenido.