1
votes

I can't get a signed URL working with a URLPrefix for Google Cload CDN.

I've setup a bucket which is a backend bucket to my Cloud CDN instance. I've successfully setup a URL signing key and have produced a working signed URL for a specific path all use the instruction found at https://cloud.google.com/cdn/docs/using-signed-urls?hl=en_US

Using my signCdnUrl2 function below I can produce a working signed url for a specific resource e.g.

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

export function signCdnUrl2(fileName: string, opts: SignedUrlOptions, urlPrefix?: string) {
    const expireVal = '' + new Date().getTime() + opts.expires;
    const urlToSign = `${opts.baseUrl}/${fileName}?Expires=${expireVal}&KeyName=${opts.keyName}`;

    // Compute signature
    const keyBuffer = Buffer.from(opts.keyBase64, 'base64');
    let signature = createHmac('sha1', keyBuffer).update(urlToSign).digest('base64');
    signature = Base64urlUtil.escape(signature);

    // Add signature to urlToSign and return signedUrl
    return urlToSign + `&Signature=${signature}`;
}

I want to avoid "the need to create a new signature for each distinct URL" so I'm following instructions at https://cloud.google.com/cdn/docs/using-signed-urls?hl=en_US#url-prefix to add the URL Prefix option.

I'm unable to successfully produce a working signed url with a prefix. My currrent attempt is below

export function signCdnUrl3(fileName: string, opts: SignedUrlOptions, urlPrefix?: string) {

    const expireVal = '' + new Date().getTime() + opts.expires;

    const urlPrefixCombined = `${opts.baseUrl}${urlPrefix}`;
    // UrlPrefix param if provided otherwise empty string
    const urlPrefixEncoded = urlPrefix ? Base64urlUtil.encode(urlPrefixCombined) : '';

    // Param string to be signed with key
    const paramsToSign = `URLPrefix=${urlPrefixEncoded}&Expires=${expireVal}&KeyName=${opts.keyName}`;

    // Compute signature
    const keyBuffer = Buffer.from(opts.keyBase64, 'base64');
    let signature = createHmac('sha1', keyBuffer).update(paramsToSign).digest('base64');
    signature = Base64urlUtil.escape(signature);

    // Add signature to url
    return `${opts.baseUrl}/${fileName}?${paramsToSign}&Signature=${signature}`;
}

I get a 403 response from cloud cdn if I try and access any resource under the given prefix in the case the root of the bucket

403 Response

Log entry from the load balancer shows it's detecting it as an invalid signature

Load Balancer Log

Is there something I'm interpreting wrong in the instructions or have I just missed something in my implementation? Any guidance would be appreciated.

Added Base64Util code for completeness

export class Base64urlUtil {

    public static encode(str: string, encoding: any = 'utf8'): string {
        const buffer: Buffer = Buffer.from(str, encoding);
        const encodedStr: string = buffer.toString('base64');
        const final: string = Base64urlUtil.escape(encodedStr);
        return final;
    }

    public static decode(str: string, encoding?: string): string {
        return Buffer.from(Base64urlUtil.unescape(str), 'base64').toString(encoding || 'utf8');
    }

    public static escape(str: string): string {
        return str.replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=/g, '');
    }

    public static unescape(str: string): string {
        return (str + '==='.slice((str.length + 3) % 4))
            .replace(/-/g, '+')
            .replace(/_/g, '/');
    }
}

Update

Using the implementation provided by @elithrar https://stackoverflow.com/a/61315372/4330441 I swapped out the his sample values in signedParams for my own live values.

let signedParams = signURLPrefix(
  "https://<my-server>/sample/360p/",
  1588291200,
  "<my-key>",
  "<valid-key>"
)

The result was so:

URLPrefix=aHR0cHM6Ly9zcHluYWwucmNmc29mdHdhcmUuaW8vc2FtcGxlLzM2MHAv&Expires=1588291200&KeyName=my-key-name&Signature=wrbOloT+m31ZnQZei2Csqq0XaGY=

When I then append these query params to call the cloud cdn endpoint at this address:

https://my-server/sample/360p/video.mp4?URLPrefix=aHR0cHM6Ly9zcHluYWwucmNmc29mdHdhcmUuaW8vc2FtcGxlLzM2MHAv&Expires=1588291200&KeyName=my-key-name&Signature=wrbOloT+m31ZnQZei2Csqq0XaGY=

I get the same 403 response and the matching invalid signature in the cdn logs

CDN log

Attempted with two different signing keys which have worked fine for signing single specific urls without the url prefix.

2
What is an example URL + prefix you're constructing? - what does your prefix end with? There is a (waiting to be merged) Python example here and a Go example here that may help - both will be published in the docs this week.elithrar
I'm trying to url prefix for clients consuming VOD dash, hls and mp4 content. Folder structure is so i.gyazo.com/56b0850c4b8f9ddbbf0030498b3bb40b.png I also added my Base64urlUtil for completeness but did not use it when testing out your sample. I've updated question with my progress. I still can't seem to get the server to accept the signature with a URLPrefix. As an added note should the signature not be url-safe? I don't think your sample does that.Robert Field
Thanks for your Go sample code it was helpful. I'm looking to make use of signed cookies in the near future.Robert Field
Yes - base64 output should be URL safe. Did you get this working via the Go sample code?elithrar
Thanks for all your examples and help. The problem seems to be the features not being enable on my project until recently. It's now working with no change to my code.Robert Field

2 Answers

1
votes

It's not entirely clear what is wrong - I suspect you may be double-base64-encoding the signature, but Base64urlUtil isn't included in the snippet you provided.

Here is a working version that generates the same signature as the tests for the Go sample code:

const crypto = require("crypto")

export function signURLPrefix(
  urlPrefix: string,
  expires: number,
  keyName: string,
  key: string
) {
  const expireVal = expires
  const urlPrefixEncoded = Buffer.from(urlPrefix)
     .toString("base64")
     .replace(/_/g, '/')
     .replace(/-/g, '+')

  // Param string to be signed with key
  const paramsToSign = `URLPrefix=${urlPrefixEncoded}&Expires=${expireVal}&KeyName=${keyName}`

  // Compute signature
  const keyBytes = Buffer.from(key, "base64")
  // Expected key: []byte{0x9d, 0x9b, 0x51, 0xa2, 0x17, 0x4d, 0x17, 0xd9,
  // 0xb7, 0x70, 0xa3, 0x36, 0xe0, 0x87, 0x0a, 0xe3}
  let signature = crypto
    .createHmac("sha1", keyBytes)
    .update(paramsToSign)
    .digest("base64")
    .replace(/_/g, '/')
    .replace(/-/g, '+')

  return `${paramsToSign}&Signature=${signature}`
}

let signedParams = signURLPrefix(
  "https://media.example.com/segments/",
  1558131350,
  "my-key",
  "nZtRohdNF9m3cKM24IcK4w=="
)

let expected =
  "URLPrefix=aHR0cHM6Ly9tZWRpYS5leGFtcGxlLmNvbS9zZWdtZW50cy8=&Expires=1558131350&KeyName=my-key&Signature=HWE5tBTZgnYVoZzVLG7BtRnOsgk="

if (signedParams === expected) {
  console.log("✔️ Signature matches")
} else {
  console.error(
    `❌ Does not match: \n\tgot ${signedParams},\n\twant ${expected}`
  )
}

Output:

➜  ts-node signed_prefix.ts
✔️ Signature matches
1
votes

I have same problem about it. Signed url with URLPrefix and signed cookie both do not work. I had tried to implement with Golang/Ruby, and I'm sure the signed logic is same with Golang example.

After asked Google Support, they said: "As this feature was recently moved to GA, they found that it wasn't correctly enabled on your project. A fix is being implemented to address this issue and I expect it to be completely rolled out by next week. I will update you once the fix has been applied." I think it was also not correctly enabled on your project.

I will try again and update information once the fix has been applied next week.

Update

We receive the latest response from Google Support that our project had enabled the feature. My code works without any modify.