无法在App Engine中创建签名的URL

I'm trying to create a signed URL to download a file in Google Cloud Storage, from an App Engine app written in Go. There's a nifty signing method in App Engine which I'm using, which [in theory] allows me to avoid putting a private key in my app. However, the URLs don't appear to work: I always get a 403 "SignatureDoesNotMatch" error. Any ideas?

Here's the code:

func createDownloadURL(c context.Context, resource string, validUntil time.Time, bucket string) (string, error) {
  serviceAccount, err := appengine.ServiceAccount(c)
  if err != nil {
    return "", err
  }

  // Build up the string to sign.
  validUntilString := strconv.FormatInt(validUntil.Unix(), 10)
  toSign := []string{
    "GET",            // http verb (required)
    "",               // content MD5 hash (optional)
    "",               // content type (optional)
    validUntilString, // expiration (required)
    resource,         // resource (required)
  }

  // Sign it.
  _, signedBytes, err := appengine.SignBytes(c, []byte(strings.Join(toSign, "
")))
  if err != nil {
    return "", err
  }
  signedString := base64.StdEncoding.EncodeToString(signedBytes)

  // Build and return the URL.
  arguments := url.Values{
    "GoogleAccessId": {serviceAccount},
    "Expires":        {validUntilString},
    "Signature":      {signedString},
  }
  return fmt.Sprintf("https://storage.googleapis.com/%s/%s?%s", bucket, resource, arguments.Encode()), nil
}

Solved. There were 2 problems with my code.

I forgot to include the bucket name when building up toSign. Fix:

fmt.Sprintf("/%s/%s", bucket, resource),         // resource (required)

This returned an AccessDenied error -- progress!

The second mistake was using the XML API storage.googleapis.com instead of the authenticated browser endpoint storage.cloud.google.com. Fix:

return fmt.Sprintf("https://storage.cloud.google.com/%s/%s?%s", bucket, resource, arguments.Encode()), nil

This works.

StringToSign may require uninterpreted newlines. Could you give this a try:

_, signedBytes, err := appengine.SignBytes(c, []byte(strings.Join(toSign, "\
"))) // escaped newline

Writing a function that signs URLs is tricky, since due to the nature of encryption it's very difficult to tell what's wrong when things don't work. You may find it easier to use a library like gcloud-golang, which has a SignedURL method.