node.jsgoogle-cloud-platformgoogle-cloud-cdn

How to create a signed URL in NodeJS for Google Cloud Media CDN


I'm having trouble generating a valid signed URL with NodeJS for Google Cloud Media CDN.

The goal is to generate a signed URL with NodeJS and the URL to expire after 10 minutes.

The issue is bizarre for some specific expiration timestamp it work and for most do not work. In the NodeJS example I have two timestamps for one it work and for other do not work.

My configuration of the Media CDN, appears correct. I was trying to debug it with the Python examples and there it work like a charm, no issue there regardless of expiration time. I was trying to reuse the Python generated keys in the NodeJS example, however looks like the RAW Ed25519 private key is not supported.

For Python generated Ed25519 keys I used this example from Google docs

and for the NodeJS example I used the keys generated by command line example from Google docs

For generating a signed URL with Python I use the example from here

This is the Python example that works every time with the keys generated from the Python script

import base64
import datetime

import cryptography.hazmat.primitives.asymmetric.ed25519 as ed25519


from six.moves import urllib

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

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

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

    url_pattern = "{url}{separator}Expires={expires}&KeyName={key_name}"

    url_to_sign = url_pattern.format(
        url=stripped_url,
        separator="&" if query_params else "?",
        expires=expiration_timestamp,
        key_name=key_name,
    )

    digest = ed25519.Ed25519PrivateKey.from_private_bytes(decoded_key).sign(
        url_to_sign.encode("utf-8")
    )
    signature = base64.urlsafe_b64encode(digest).decode("utf-8")
    signed_url = "{url}&Signature={signature}".format(
        url=url_to_sign, signature=signature
    )

    return signed_url

# private key: tHfW65gTzws54calc2OzICdVvdu6X1BoonVLjsD5ZQU=
# public key: 5xujsWltI4LTi0Q92hp9HmA03KsJFn2WwziiOA3POm8=

url = 'https://media.radixsoft.dev/raindrops.mp4'
key_name = 'dev-keyset'
private_key = 'tHfW65gTzws54calc2OzICdVvdu6X1BoonVLjsD5ZQU='
expiration_timestamp = datetime.datetime.now() + datetime.timedelta(minutes = 10)

result = sign_url(url, key_name, private_key, expiration_timestamp)
print(result)

Signed URL with Python

And this is my NodeJS script for signing

const crypto = require('crypto');

// private key: MC4CAQAwBQYDK2VwBCIEIPzuffDsZ43sPxf1fuwiDrKsiSoyFw8aosF2jhS3VCez
// public key: JU4ouYJAZvD12RYH8-zhSmUTqT1ohN-1kcIKhr2jL4o=

function signURL(url, expiresTs, keyName, privateKey) {
  const toSign = `${url}?Expires=${expiresTs}&KeyName=${keyName}`;
  const signature = crypto.sign(null, Buffer.from(toSign), privateKey);

  return `${toSign}&Signature=${signature.toString('base64')}`;
}

const privateKey = crypto.createPrivateKey({
  key: Buffer.from('MC4CAQAwBQYDK2VwBCIEIPzuffDsZ43sPxf1fuwiDrKsiSoyFw8aosF2jhS3VCez', 'base64'),
  format: 'der',
  type: 'pkcs8',
});

const url = 'https://media.radixsoft.dev/raindrops.mp4';
const expiresTs = Math.round(new Date().getTime() / 1000) + 10 * 60; // ten minutes after now, in seconds.
// const expiresTs = 1735689598; // 2024-12-31 23:59:58 - do not work. The response is 'Google-Edge-Cache: Invalid signed request Error: 114' 
// const expiresTs = 1735689599; // 2024-12-31 23:59:59 - work
const keyName = 'dev-keyset';

const signedUrl = signURL(url, expiresTs, keyName, privateKey);
console.log(signedUrl);

Signed URL with NodeJS The response is: Google-Edge-Cache: Invalid signed request Error: 114

I guess the problem is in my NodeJS script. Currently, I'm not sure where the problem is and what I should try. Any help or guideline would be appreciated.


Solution

  • You need to use URL-safe Base64 encoding. In your NodeJS code, change signature.toString('base64') to signature.toString('base64url').