Usa URL firmadas

En esta página, se proporciona una descripción general de las URL firmadas, además de instrucciones para usarlas con Cloud CDN. Las URL firmadas otorgan acceso a recursos por tiempo limitado a cualquier persona que tenga la URL, sin importar si el usuario tiene una Cuenta de Google.

Una URL firmada es una URL que proporciona permisos y tiempo limitados para realizar una solicitud. Las URL firmadas contienen información de autenticación en las cadenas de consulta, lo que permite a los usuarios sin credenciales realizar acciones específicas en un recurso. Cuando generas una URL firmada, debes especificar un usuario o una cuenta de servicio que tenga los permisos suficientes para realizar la solicitud asociada con la URL.

Después de generar una URL firmada, cualquier persona que la posea puede usarla para realizar acciones específicas (como leer un objeto) dentro de un período determinado.

Las URL firmadas también admiten un parámetro URLPrefix opcional, que te permite proporcionar acceso a varias URL basadas en un prefijo común.

Si deseas limitar el acceso a un prefijo de URL específico, considera usar cookies firmadas.

Antes de comenzar

Antes de usar URL firmadas, haz lo siguiente:

  • Asegúrate de que Cloud CDN esté habilitado. Para obtener instrucciones, consulta Usa Cloud CDN. Puedes configurar las URL 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 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 solicitud 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 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 clave de firma nueva.
    2. En la sección Método de creación de claves, selecciona Generar automáticamente. Como alternativa, haz clic en Permitirme ingresar y, luego, especifica una clave de firma valor.

      Para la opción anterior, copia la clave de firma generada automáticamente. valor en un archivo privado, que puedes usar para Crea 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 del 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

Enumerar 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

Borrar claves de solicitud 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

Firma URL

El último paso es firmar las URL y distribuirlas. Puedes firmar las URL mediante el comando gcloud compute sign-url o con un código que escribes tú mismo. Si necesitas una gran cantidad de URL firmadas, el código personalizado brinda un mejor rendimiento.

Crea URL firmadas

Usa estas instrucciones para crear URL firmadas mediante el comando gcloud compute sign-url. En este paso, se supone que ya creaste las claves.

Console

No puedes crear URL firmadas mediante la consola de Cloud. Puedes usar Google Cloud CLI o escribir un código personalizado con los siguientes ejemplos.

gcloud

Google Cloud CLI incluye un comando para firmar las URL. El comando implementa el algoritmo que se describe en la sección sobre cómo escribir tu propio código.

gcloud compute sign-url \
  "URL" \
  --key-name KEY_NAME \
  --key-file KEY_FILE_NAME \
  --expires-in TIME_UNTIL_EXPIRATION \
  [--validate]

Este comando lee y decodifica el archivo base64url codificado. par clave-valor de KEY_FILE_NAME y, luego, da como resultado un una URL firmada que puedes usar para las solicitudes GET o HEAD de la URL determinada.

Por ejemplo:

gcloud compute sign-url \
  "https://example.com/media/video.mp4" \
  --key-name my-test-key \
  --expires-in 30m \
  --key-file sign-url-key-file

La URL debe ser una URL válida que tenga un componente de ruta. Por ejemplo, http://example.com no es una URL válida, pero https://example.com/ y https://example.com/whatever sí lo son.

Si se proporciona la marca --validate opcional, este comando envía una solicitud HEAD con la URL resultante y, luego, imprime el código de respuesta HTTP. Si la URL firmada es correcta, el código de respuesta es el mismo que el código de resultado que envía tu backend. Si el código de respuesta no es el mismo, vuelve a verificar KEY_NAME y el contenido del archivo especificado, y asegúrate de que el valor de TIME_UNTIL_EXPIRATION sea al menos de varios segundos.

Si no se proporciona la marca --validate, no se verifica lo siguiente:

  • Las entradas
  • La URL que se genera
  • La URL firmada que se genera

Crea URLs firmadas de manera programática

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

Go

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

// SignURL creates a signed URL for an endpoint on Cloud CDN.
//
// - url must start with "https://" and should not have the "Expires", "KeyName", or "Signature"
// query parameters.
// - 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 signURL(url, keyName string, key []byte, expiration time.Time) string {
	sep := "?"
	if strings.Contains(url, "?") {
		sep = "&"
	}
	url += sep
	url += fmt.Sprintf("Expires=%d", expiration.Unix())
	url += fmt.Sprintf("&KeyName=%s", keyName)

	mac := hmac.New(sha1.New, key)
	mac.Write([]byte(url))
	sig := base64.URLEncoding.EncodeToString(mac.Sum(nil))
	url += fmt.Sprintf("&Signature=%s", sig)
	return url
}

Ruby

def signed_url url:, key_name:, key:, expiration:
  # url        = "URL of the endpoint served by Cloud CDN"
  # key_name   = "Name of the signing key added to the Google Cloud Storage bucket or service"
  # key        = "Signing key as urlsafe base64 encoded string"
  # expiration = Ruby Time object with expiration time

  require "base64"
  require "openssl"
  require "time"

  # Decode the URL safe base64 encode key
  decoded_key = Base64.urlsafe_decode64 key

  # Get UTC time in seconds
  expiration_utc = expiration.utc.to_i

  # Determine which separator makes sense given a URL
  separator = "?"
  separator = "&" if url.include? "?"

  # Concatenate url with expected query parameters Expires and KeyName
  url = "#{url}#{separator}Expires=#{expiration_utc}&KeyName=#{key_name}"

  # Sign the url using the key and url safe base64 encode the signature
  signature         = OpenSSL::HMAC.digest "SHA1", decoded_key, url
  encoded_signature = Base64.urlsafe_encode64 signature

  # Concatenate the URL and encoded signature
  signed_url = "#{url}&Signature=#{encoded_signature}"
end

.NET

        /// <summary>
        /// Creates signed URL for Google Cloud SDN
        /// More details about order of operations is here: 
        /// <see cref="https://cloud.google.com/cdn/docs/using-signed-urls#programmatically_creating_signed_urls"/>
        /// </summary>
        /// <param name="url">The Url to sign. This URL can't include Expires and KeyName query parameters in it</param>
        /// <param name="keyName">The name of the key used to sign the URL</param>
        /// <param name="encodedKey">The key used to sign the Url</param>
        /// <param name="expirationTime">Expiration time of the signature</param>
        /// <returns>Signed Url that is valid until {expirationTime}</returns>
        public static string CreateSignedUrl(string url, string keyName, string encodedKey, DateTime expirationTime)
        {
            var builder = new UriBuilder(url);

            long unixTimestampExpiration = ToUnixTime(expirationTime);

            char queryParam = string.IsNullOrEmpty(builder.Query) ? '?' : '&';
            builder.Query += $"{queryParam}Expires={unixTimestampExpiration}&KeyName={keyName}".ToString();

            // Key is passed as base64url encoded
            byte[] decodedKey = Base64UrlDecode(encodedKey);

            // Computes HMAC SHA-1 hash of the URL using the key
            byte[] hash = ComputeHash(decodedKey, builder.Uri.AbsoluteUri);
            string encodedHash = Base64UrlEncode(hash);

            builder.Query += $"&Signature={encodedHash}";
            return builder.Uri.AbsoluteUri;
        }

        private static long ToUnixTime(DateTime date)
        {
            var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            return Convert.ToInt64((date - epoch).TotalSeconds);
        }

        private static byte[] Base64UrlDecode(string arg)
        {
            string s = arg;
            s = s.Replace('-', '+'); // 62nd char of encoding
            s = s.Replace('_', '/'); // 63rd char of encoding

            return Convert.FromBase64String(s); // Standard base64 decoder
        }

        private static string Base64UrlEncode(byte[] inputBytes)
        {
            var output = Convert.ToBase64String(inputBytes);

            output = output.Replace('+', '-')      // 62nd char of encoding
                           .Replace('/', '_');     // 63rd char of encoding

            return output;
        }

        private static byte[] ComputeHash(byte[] secretKey, string signatureString)
        {
            var enc = Encoding.ASCII;
            using (HMACSHA1 hmac = new HMACSHA1(secretKey))
            {
                hmac.Initialize();

                byte[] buffer = enc.GetBytes(signatureString);

                return hmac.ComputeHash(buffer);
            }
        }

Java

/**
 * Creates a signed URL for a Cloud CDN endpoint with the given key
 * URL must start with http:// or https://, and must contain a forward
 * slash (/) after the hostname.
 *
 * @param url the Cloud CDN endpoint to sign
 * @param key url signing key uploaded to the backend service/bucket, as a 16-byte array
 * @param keyName the name of the signing key added to the back end bucket or service
 * @param expirationTime the date that the signed URL expires
 * @return a properly formatted signed URL
 * @throws InvalidKeyException when there is an error generating the signature for the input key
 * @throws NoSuchAlgorithmException when HmacSHA1 algorithm is not available in the environment
 */
public static String signUrl(String url,
                             byte[] key,
                             String keyName,
                             Date expirationTime)
        throws InvalidKeyException, NoSuchAlgorithmException {

  final long unixTime = expirationTime.getTime() / 1000;

  String urlToSign = url
                      + (url.contains("?") ? "&" : "?")
                      + "Expires=" + unixTime
                      + "&KeyName=" + keyName;

  String encoded = SignedUrls.getSignature(key, urlToSign);
  return urlToSign + "&Signature=" + encoded;
}

public static String getSignature(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()));
}

Python

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


def sign_url(
    url: str,
    key_name: str,
    base64_key: str,
    expiration_time: datetime,
) -> str:
    """Gets the Signed URL string for the specified URL and configuration.

    Args:
        url: URL to sign.
        key_name: name of the signing key.
        base64_key: signing key as a base64 encoded string.
        expiration_time: expiration time.

    Returns:
        Returns the Signed URL appended with the query parameters based on the
        specified configuration.
    """
    stripped_url = url.strip()
    parsed_url = urlsplit(stripped_url)
    query_params = parse_qs(parsed_url.query, keep_blank_values=True)
    epoch = datetime.utcfromtimestamp(0)
    expiration_timestamp = int((expiration_time - epoch).total_seconds())
    decoded_key = base64.urlsafe_b64decode(base64_key)

    url_to_sign = f"{stripped_url}{'&' if query_params else '?'}Expires={expiration_timestamp}&KeyName={key_name}"

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

    return f"{url_to_sign}&Signature={signature}"

PHP

/**
 * Decodes base64url (RFC4648 Section 5) string
 *
 * @param string $input base64url encoded string
 *
 * @return string
 */
function base64url_decode($input)
{
    $input .= str_repeat('=', (4 - strlen($input) % 4) % 4);
    return base64_decode(strtr($input, '-_', '+/'), true);
}

/**
* Encodes a string with base64url (RFC4648 Section 5)
* Keeps the '=' padding by default.
*
* @param string $input   String to be encoded
* @param bool   $padding Keep the '=' padding
*
* @return string
*/
function base64url_encode($input, $padding = true)
{
    $output = strtr(base64_encode($input), '+/', '-_');
    return ($padding) ? $output : str_replace('=', '',  $output);
}

/**
 * Creates signed URL for Google Cloud CDN
 * Details about order of operations: https://cloud.google.com/cdn/docs/using-signed-urls#creating_signed_urls
 *
 * Example function invocation (In production store the key safely with other secrets):
 *
 *     <?php
 *     $base64UrlKey = 'wpLL7f4VB9RNe_WI0BBGmA=='; // head -c 16 /dev/urandom | base64 | tr +/ -_
 *     $signedUrl = sign_url('https://example.com/foo', 'my-key', $base64UrlKey, time() + 1800);
 *     echo $signedUrl;
 *     ?>
 *
 * @param string $url             URL of the endpoint served by Cloud CDN
 * @param string $keyName         Name of the signing key added to the Google Cloud Storage bucket or service
 * @param string $base64UrlKey    Signing key as base64url (RFC4648 Section 5) encoded string
 * @param int    $expirationTime  Expiration time as a UNIX timestamp (GMT, e.g. time())
 *
 * @return string
 */
function sign_url($url, $keyName, $base64UrlKey, $expirationTime)
{
    // Decode the key
    $decodedKey = base64url_decode($base64UrlKey);

    // Determine which separator makes sense given a URL
    $separator = (strpos($url, '?') === false) ? '?' : '&';

    // Concatenate url with expected query parameters Expires and KeyName
    $url = "{$url}{$separator}Expires={$expirationTime}&KeyName={$keyName}";

    // Sign the url using the key and encode the signature using base64url
    $signature = hash_hmac('sha1', $url, $decodedKey, true);
    $encodedSignature = base64url_encode($signature);

    // Concatenate the URL and encoded signature
    return "{$url}&Signature={$encodedSignature}";
}

Crea de manera programática URLs firmadas con un prefijo de URL

En las siguientes muestras de código, se explica cómo crear de manera programática URLs con un prefijo de URL.

Go

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

// SignURLWithPrefix creates a signed URL prefix for an endpoint on Cloud CDN.
// Prefixes allow access to any URL with the same prefix, and can be useful for
// granting access broader content without signing multiple URLs.
//
// - urlPrefix must start with "https://" and should not include query parameters.
// - 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 signURLWithPrefix(urlPrefix, keyName string, key []byte, expiration time.Time) (string, error) {
	if strings.Contains(urlPrefix, "?") {
		return "", fmt.Errorf("urlPrefix must not include query params: %s", urlPrefix)
	}

	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 SignedUrlWithPrefix {

  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 date that the signed URL expires.
    long expirationTime = ZonedDateTime.now().plusDays(1).toEpochSecond();
    // URL of request
    String requestUrl = "https://media.example.com/videos/id/main.m3u8?userID=abc123&starting_profile=1";
    // 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);

    // Sign the url with prefix
    String signUrlWithPrefixResult = signUrlWithPrefix(requestUrl,
        urlPrefix, keyBytes, keyName, expirationTime);
    System.out.println(signUrlWithPrefixResult);
  }

  // Creates a signed URL with a URL prefix for a Cloud CDN endpoint with the given key. Prefixes
  // allow access to any URL with the same prefix, and can be useful for granting access broader
  // content without signing multiple URLs.
  static String signUrlWithPrefix(String requestUrl, 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 urlToSign = "URLPrefix=" + encodedUrlPrefix
        + "&Expires=" + expirationTime
        + "&KeyName=" + keyName;

    String encoded = getSignatureForUrl(key, urlToSign);
    return requestUrl + "&" + urlToSign + "&Signature=" + encoded;
  }

  // Creates signature for input url 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
import hashlib
import hmac
from urllib.parse import parse_qs, urlsplit


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

    Args:
        url: URL of request.
        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.

    Returns:
        Returns the Signed URL appended with the query parameters based on the
        specified URL prefix and configuration.
    """
    stripped_url = url.strip()
    parsed_url = urlsplit(stripped_url)
    query_params = parse_qs(parsed_url.query, keep_blank_values=True)
    encoded_url_prefix = base64.urlsafe_b64encode(
        url_prefix.strip().encode("utf-8")
    ).decode("utf-8")
    epoch = datetime.utcfromtimestamp(0)
    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")

    return f"{stripped_url}{'&' if query_params else '?'}{policy}&Signature={signature}"

Genera URLs firmadas personalizadas

Cuando escribes tu propio código para generar URL firmadas, tu objetivo es crear URL con el siguiente formato o algoritmo (todos los parámetros de URL distinguen mayúsculas de minúsculas, y deben estar en el orden que se muestra):

https://example.com/foo?Expires=EXPIRATION&KeyName=KEY_NAME&Signature=SIGNATURE

Para generar URL firmadas, sigue estos pasos:

  1. Asegúrate de que la URL que se debe firmar no tenga un parámetro de búsqueda Signature.

  2. Determina cuándo vence la URL y agrega un parámetro de consulta Expires con la hora de vencimiento requerida en hora UTC (la cantidad de segundos desde 1970-01-01 00:00:00 UTC). A fin de maximizar la seguridad, establece el valor en el período más corto posible para el caso de uso. Mientras más tiempo dure la validez de una URL firmada, más grande será el riesgo de que el usuario al cual le diste la URL la comparta con otros, de manera accidental o por otro motivo.

  3. Establece el nombre de la clave. La URL debe estar firmada con una clave del servicio o bucket de backend que entrega la URL. Lo mejor es usar la última clave agregada para la rotación de claves. Adjunta &KeyName=KEY_NAME para agregar una clave a la URL. Reemplaza KEY_NAME por el nombre de la clave elegida que se creó en Crea claves de solicitudes firmadas.

  4. Firma la URL. Sigue estos pasos para crear la URL firmada. Asegúrate de que los parámetros de búsqueda estén en el orden que se muestra justo antes del paso 1 y que no se produzca ningún cambio en la URL firmada.

    a. Genera un hash de la URL completa (incluidos http:// o https:// al principio y &KeyName... al final) con HMAC-SHA1 mediante la clave secreta que corresponde al nombre de clave que se seleccionó antes. Usa la clave secreta de 16 bytes sin procesar, no la clave codificada en base64url. Decodifícala si es necesario.

    b. Usa la codificación en base64url. para codificar el resultado.

    c. Agrega &Signature= a la URL, seguido de la firma codificada.

Usa prefijos de URL para las URLs firmadas

En lugar de firmar la URL de solicitud completa con los parámetros de consulta Expires y KeyName, puedes firmar solo los parámetros de consulta URLPrefix, Expires y KeyName. Esto permite que una combinación determinada de parámetros de consulta URLPrefix, Expires, KeyName y Signature se vuelvan a usar tal como aparecen en varias URL que coincidan con el URLPrefix, lo que evita la necesidad de crear una nueva firma para cada URL distinta.

En el siguiente ejemplo, en el texto destacado, se muestran los parámetros que firmas. El Signature se agrega como el parámetro de consulta final, como de costumbre.

https://media.example.com/videos/id/master.m3u8?userID=abc123&starting_profile=1&URLPrefix=aHR0cHM6Ly9tZWRpYS5leGFtcGxlLmNvbS92aWRlb3Mv&Expires=1566268009&KeyName=mySigningKey&Signature=8NBSdQGzvDftrOIa3WHpp646Iis=

A diferencia de la firma de una URL de solicitud completa, cuando firmas con URLPrefix, no firmas ningún parámetro de consulta, por lo que los parámetros de consulta se pueden incluir libremente en la URL. Además, a diferencia de las firmas de URL de solicitud completas, esos parámetros de consulta adicionales pueden aparecer antes y después de los parámetros de consulta que conforman la firma. Como resultado, lo siguiente también es una URL válida con un prefijo de URL firmado:

https://media.example.com/videos/id/master.m3u8?userID=abc123&URLPrefix=aHR0cHM6Ly9tZWRpYS5leGFtcGxlLmNvbS92aWRlb3Mv&Expires=1566268009&KeyName=mySigningKey&Signature=8NBSdQGzvDftrOIa3WHpp646Iis=&starting_profile=1

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.

Validar URLs firmadas

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

https://example.com/PATH?Expires=EXPIRATION&KeyName=KEY_NAME&Signature=SIGNATURE

Puedes usar la clave secreta llamada KEY_NAME a fin de generar la firma de forma independiente para la siguiente URL:

https://example.com/PATH?Expires=EXPIRATION&KeyName=KEY_NAME

Luego, puedes verificar que coincida con SIGNATURE.

Supongamos que deseas validar una URL firmada que tiene un URLPrefix, como se muestra aquí:

https://example.com/PATH?URLPrefix=URL_PREFIX&Expires=EXPIRATION&KeyName=KEY_NAME&Signature=SIGNATURE

Primero, verifica que el valor decodificado en Base64 de URL_PREFIX sea un prefijo de https://example.com/PATH. De ser así, puedes calcular la firma para lo siguiente:

URLPrefix=URL_PREFIX&Expires=EXPIRATION&KeyName=KEY_NAME

Luego, puedes verificar que coincida con SIGNATURE.

En el caso de los métodos de firma basados en URLs, en los que la firma forma parte de los parámetros de consulta o está incorporada como un componente de ruta de URL, la firma y los parámetros relacionados se quitan de la URL antes de que se envíe la solicitud al origen. Esto evita que la firma cause problemas de enrutamiento cuando la el origen controla la solicitud. Para validar estas solicitudes, puedes inspeccionar el Encabezado de la solicitud x-client-request-url, que incluye el original (firmado) URL de solicitud del cliente antes de quitar los componentes firmados.

Quita el acceso público al bucket de Cloud Storage

Para que las URL firmadas protejan el contenido de forma adecuada, es importante que el servidor de origen no otorgue acceso público a ese contenido. Cuando se usa un bucket de Cloud Storage, un enfoque común es hacer que los objetos sean públicos de forma temporal con fines de prueba. Después de habilitar las URL firmadas, es importante quitar los permisos de LECTURA allUsers (y allAuthenticatedUsers, si corresponde) (en otras palabras, la función de IAM Visualizador de objetos de almacenamiento) del depósito.

Después de inhabilitar el acceso público en el bucket, los usuarios individuales aún podrán acceder a Cloud Storage sin URL firmadas si tienen un permiso de acceso, como PROPIETARIO.

Para quitar el acceso de LECTURA allUsers público en un bucket de Cloud Storage, revierte la acción descrita en Haz que todos los objetos de un bucket se puedan leer de forma pública.

Distribuye y usa las URLs firmadas

La URL que se muestra desde Google Cloud CLI o que genera el código personalizado puede distribuirse según tus necesidades. Recomendamos firmar solo las URL HTTPS, ya que HTTPS proporciona un transporte seguro que evita que se intercepte el componente Signature de la URL firmada. De manera similar, asegúrate de distribuir las URL firmadas a través de protocolos de transporte seguros, como TLS o HTTPS.