Usar URLs firmadas

En esta página se ofrece una descripción general de las URLs firmadas y se explica cómo usarlas con Cloud CDN. Las URLs firmadas proporcionan acceso a recursos durante un tiempo limitado a cualquier usuario que disponga de dicha URL, independientemente de si tiene una cuenta de Google o no.

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

Una vez que hayas generado una URL firmada, cualquier persona que la tenga podrá usarla para realizar las acciones especificadas (como leer un objeto) durante un periodo determinado.

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

Si quieres limitar el acceso a un prefijo de URL específico, te recomendamos que utilices cookies firmadas.

Antes de empezar

Antes de usar URLs firmadas, haga lo siguiente:

  • Asegúrate de que Cloud CDN esté habilitado. Para obtener instrucciones, consulta el artículo Usar Cloud CDN. Puedes configurar URLs 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 solicitud firmada

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

Firmar URLs

El último paso es firmar las URLs y distribuirlas. Puedes firmar URLs con el comando gcloud compute sign-url o con código que escribas tú. Si necesitas muchas URLs firmadas, el código personalizado ofrece un mejor rendimiento.

Crear URLs firmadas

Sigue estas instrucciones para crear URLs firmadas con el comando gcloud compute sign-url. En este paso se da por hecho que ya has creado las claves.

Consola

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

gcloud

La CLI de Google Cloud incluye un comando para firmar URLs. El comando implementa el algoritmo descrito en la sección sobre 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 valor de clave codificado en base64url de KEY_FILE_NAME y, a continuación, genera una URL firmada que puedes usar para las solicitudes GET o HEAD de la URL dada.

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

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

Si se proporciona la marca opcional --validate, este comando envía una solicitud HEAD con la URL resultante e 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 enviado por tu backend. Si el código de respuesta no es el mismo, vuelve a comprobar KEY_NAME y el contenido del archivo especificado, y asegúrate de que el valor de TIME_UNTIL_EXPIRATION sea de al menos varios segundos.

Si no se indica la marca --validate, no se verifican los siguientes elementos:

  • Las entradas
  • La URL generada
  • La URL firmada generada

Crear URLs firmadas mediante programación

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

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

/** Samples to create a signed URL for a Cloud CDN endpoint */
public class SignedUrls {

  /**
   * 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, timezone
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 as time-zone aware datetime.

    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.fromtimestamp(0, timezone.utc)
    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}";
}

Crear URLs firmadas de forma programática con un prefijo de URL

En los siguientes ejemplos de código se muestra cómo crear de forma programática URLs firmadas 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, timezone
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 as time-zone aware datetime.

    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.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")

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

Generar URLs firmadas personalizadas

Cuando escribas tu propio código para generar URLs firmadas, tu objetivo será crear URLs con el siguiente formato o algoritmo. Todos los parámetros de la URL distinguen entre mayúsculas y minúsculas y deben estar en el orden que se muestra:

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

Para generar URLs firmadas, sigue estos pasos:

  1. Asegúrate de que la URL de firma no tenga un parámetro de consulta Signature.

  2. Determina cuándo caduca la URL y añade un parámetro de consulta Expires con la hora de vencimiento necesaria en UTC (el número de segundos transcurridos desde el 1 de enero de 1970 a las 00:00:00 UTC). Para maximizar la seguridad, define el valor en el periodo más breve posible para tu caso práctico. Cuanto más tiempo sea válida una URL firmada, mayor será el riesgo de que el usuario al que se la proporciones la comparta con otros usuarios, ya sea por error o de otra forma.

  3. Define el nombre de la clave. La URL debe estar firmada con una clave del servicio de backend o del bucket de backend que la proporcione. Es mejor usar la clave añadida más recientemente para cambiar la clave. Añade la clave a la URL añadiendo &KeyName=KEY_NAME. Sustituye KEY_NAME por el nombre de la clave elegida que has creado en Crear claves de solicitud firmadas.

  4. Firma la URL. Crea la URL firmada siguiendo estos pasos. Asegúrate de que los parámetros de consulta estén en el orden que se muestra inmediatamente antes del paso 1 y de que no se modifiquen las mayúsculas ni las minúsculas en la URL firmada.

    a. Cifra con hash toda la URL (incluidos http:// o https:// al principio y &KeyName... al final) con HMAC-SHA1 mediante la clave secreta que corresponda al nombre de clave elegido anteriormente. Usa la clave secreta sin procesar de 16 bytes, no la clave codificada en base64url. Descifrarlo si es necesario.

    b. Usa base64url encode para codificar el resultado.

    c. Añade &Signature= a la URL, seguido de la firma codificada. No convierta los caracteres = finales de la firma a su forma codificada como porcentaje, %3D.

Usar prefijos de URL para URLs firmadas

En lugar de firmar la URL de solicitud completa con los parámetros de consulta Expires y KeyName, puede firmar solo los parámetros de consulta URLPrefix, Expires y KeyName. De esta forma, se puede reutilizar literalmente una combinación determinada de parámetros de consulta URLPrefix, Expires, KeyName y Signature en varias URLs que coincidan con URLPrefix, lo que evita tener que crear una firma nueva para cada URL distinta.

En el ejemplo siguiente, el texto destacado muestra los parámetros que firmas. El Signature se añade como 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 cuando se firma una URL de solicitud completa, al firmar con URLPrefix no se firma ningún parámetro de consulta, por lo que 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 tanto antes como después de los parámetros de consulta que componen la firma. Por lo tanto, la siguiente también es una URL válida con un prefijo de URL firmada:

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

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.

Validar URLs firmadas

El proceso de validación de una URL firmada es esencialmente el mismo que el de generar una URL firmada. Por ejemplo, supongamos que quiere validar la siguiente URL firmada:

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

Puede usar la clave secreta denominada por KEY_NAME para generar de forma independiente la firma de la siguiente URL:

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

A continuación, puedes verificar que coincida con SIGNATURE.

Supongamos que quieres validar una URL firmada que tiene un URLPrefix, como se muestra a continuación:

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

Primero, comprueba que el valor decodificado en Base64 de URL_PREFIX es un prefijo de https://example.com/PATH. Si es así, puedes calcular la firma de lo siguiente:

URLPrefix=URL_PREFIX&Expires=EXPIRATION&KeyName=KEY_NAME

A continuación, puedes verificar que coincida con SIGNATURE.

En los métodos de firma basados en URLs, en los que la firma forma parte de los parámetros de consulta o se inserta como un componente de la ruta de la URL, la firma y los parámetros relacionados se eliminan de la URL antes de que se envíe la solicitud al origen. De esta forma, se evita que la firma cause problemas de enrutamiento cuando el origen gestione la solicitud. Para validar estas solicitudes, puede inspeccionar el encabezado de solicitud x-client-request-url, que incluye la URL de solicitud del cliente original (firmada) antes de que se eliminen los componentes firmados.

Retira el acceso público al segmento de Cloud Storage

Para que las URLs firmadas protejan el contenido correctamente, es importante que el servidor de origen no conceda acceso público a ese contenido. Cuando se usa un segmento de Cloud Storage, una práctica habitual es hacer que los objetos sean públicos temporalmente con fines de prueba. Después de habilitar las URLs firmadas, es importante quitar los permisos de lectura allUsers (y allAuthenticatedUsers, si procede) (es decir, el rol de gestión de identidades y accesos Storage Object Viewer) del segmento.

Después de inhabilitar el acceso público al segmento, los usuarios individuales podrán seguir accediendo a Cloud Storage sin URLs firmadas si tienen permiso de acceso, como el permiso OWNER.

Para quitar el acceso de allUsers LECTURA público a un segmento de Cloud Storage, invierte la acción descrita en Hacer que todos los objetos de un segmento se puedan leer públicamente.

Distribuir y usar URLs firmadas

La URL devuelta por la CLI de Google Cloud o generada por tu código personalizado se puede distribuir según tus necesidades. Te recomendamos que solo firmes URLs HTTPS, ya que HTTPS proporciona un transporte seguro que evita que se intercepte el Signaturecomponente de la URL firmada. Del mismo modo, asegúrate de distribuir las URLs firmadas a través de protocolos de transporte seguros, como TLS o HTTPS.