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)
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.
You need to use URL-safe Base64 encoding. In your NodeJS code, change signature.toString('base64')
to signature.toString('base64url')
.