4

I was successfully able to create a stand-alone Java Application that creates Expiring Signed URL's to assets sitting in Google Cloud Storage. However, I have been unsuccessful in figuring out how to create Expiring Signed URL's to these same assets through AppEngine.

How can I create a Expiring Signed URL to Google Cloud Storage Assets that I can return to client applications?

Here is my Java Application that works:

import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.UnrecoverableKeyException;
import java.util.Calendar;

import org.apache.commons.codec.binary.Base64;

public class GCSSignedURL {

public static void main(String[] args) throws Exception {

    final String googleAccessId = "XXXXXXXXXXXX@developer.gserviceaccount.com";
    final String keyFile = "D:\\XXXXXXXXXXXXXXXXXXXXXXXXXXXXX-privatekey.p12";
    final  String keyPassword = "notasecret";
    Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.MINUTE, 30);
    String httpVerb = "GET";
    String contentMD5 = "";         
    String contentType = "";
    long expiration = calendar.getTimeInMillis();
    String canonicalizedExtensionHeaders = ""; 
    String canonicalizedResource = "/myproj/foo.txt";

    String stringToSign = 
            httpVerb + "\n" + 
            contentMD5 + "\n" + 
            contentType + "\n" + 
            expiration + "\n" + 
            canonicalizedExtensionHeaders + 
            canonicalizedResource;

    PrivateKey pkcsKey = loadKeyFromPkcs12(keyFile, keyPassword.toCharArray());
    String signature = signData(pkcsKey, stringToSign);     
    String baseURL = "https://storage.cloud.google.com/myproj/foo.txt";     
    String urlEncodedSignature = URLEncoder.encode(signature, "UTF-8");
    String url = baseURL + "?GoogleAccessId=" + googleAccessId + "&Expires=" + expiration + "&Signature=" + urlEncodedSignature;

    System.out.println(url);
}

private static PrivateKey loadKeyFromPkcs12(String filename, char[] password)
        throws Exception {
    FileInputStream fis = new FileInputStream(filename);
    KeyStore ks = KeyStore.getInstance("PKCS12");
    try {
        ks.load(fis, password);
    } catch (IOException e) {
        if (e.getCause() != null
                && e.getCause() instanceof UnrecoverableKeyException) {
            System.err.println("Incorrect password");
        }
        throw e;
    }
    return (PrivateKey) ks.getKey("privatekey", password);
}

private static String signData(PrivateKey key, String data)
        throws Exception {
    Signature signer = Signature.getInstance("SHA256withRSA");
    signer.initSign(key);
    signer.update(data.getBytes("UTF-8"));
    byte[] rawSignature = signer.sign();
    String encodedSignature = new String(Base64.encodeBase64(rawSignature,
            false), "UTF-8");
    return encodedSignature;
}

}

Here is what I have tried:

public String signUrl(Long _songId, String _format) throws ResourceNotFoundException
{
    final String googleAccessId = "XXXXXXXXXXXXXXXXXX@developer.gserviceaccount.com";

    AppIdentityService service = AppIdentityServiceFactory.getAppIdentityService();

    Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.MINUTE, 5);       
    String httpVerb = "GET";
    String contentMD5 = "";
    String contentType = "";
    long expiration = calendar.getTimeInMillis();       
    String canonicalizedExtensionHeaders = "";      
    String canonicalizedResource = "/myproj/foo.txt";
    String stringToSign = 
            httpVerb + "\n" + 
            contentMD5 + "\n" + 
            contentType + "\n" + 
            expiration + "\n" + 
            canonicalizedExtensionHeaders + 
            canonicalizedResource;  

    SigningResult key = service.signForApp(stringToSign.getBytes());
    String baseURL = "https://storage.cloud.google.com/myproj/foo.txt";
    String encodedUrl = baseURL + "?GoogleAccessId=" + googleAccessId + "&Expires=" + expiration
            + "&Signature=" + key.getKeyName();

    return encodedUrl;
}

The result is an expiring URL but requires me to authenticate with my google email / password so the signing isn't working properly.

I was able to finally generate an encoded URL using Fabio's suggestion, however, I now get:

<?xml version="1.0" encoding="UTF-8"?>
-<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated 
does not match the signature you provided. Check your Google secret key and signing 
method.</Message>
<StringToSign>
GET 1374729586 /[my_bucket]/[my_folder]/file.png</StringToSign>    
</Error>

The code I am using to generate the URL is:

AppIdentityService service = AppIdentityServiceFactory.getAppIdentityService();
    final String googleAccessId = service.getServiceAccountName();
    String url = songUrl(_songId, _format);  
    Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.MINUTE, 10);
    String httpVerb = "GET";
    String contentMD5 = ""; 
    String contentType = ""; 
    long expiration = calendar.getTimeInMillis()/1000L;
    String canonicalizedExtensionHeaders = "";      
    String canonicalizedResource = "/[my_bucket]/[my_folder]/file.png";
    String stringToSign = 
            httpVerb + "\n" + 
            contentMD5 + "\n" + 
            contentType + "\n" + 
            expiration + "\n" + 
            canonicalizedExtensionHeaders + 
            canonicalizedResource;

    try 
    {
        String baseURL = http://[my_bucket].commondatastorage.googleapis.com/[my_folder]/file.png;  
        SigningResult signingResult = service.signForApp(stringToSign.getBytes());
        String encodedSignature = new String(Base64.encodeBase64(signingResult.getSignature(), false), "UTF-8");
        String encodedUrl = baseURL + "?GoogleAccessId=" + googleAccessId + "&Expires=" + expiration
                + "&Signature=" + encodedSignature;

        return encodedUrl;
    } 
    catch (UnsupportedEncodingException e) 
    {
        throw new ResourceNotFoundException("Unable to encode URL.  Unsupported encoding exception.", e);
    }       
user1869501
  • 43
  • 1
  • 5
  • 1
    Have you arranged for the App Engine service account (X@developer.gserviceaccount.com) to have access to the object, either by including that email address in the owning project's team or in the object's Access Control List? – Marc Cohen Jul 07 '13 at 02:42
  • Yes, I've got access. Can make it work from a Java application, just not in App Engine. Not sure how to access my private.key file and then encrypt the URL. – user1869501 Jul 07 '13 at 06:35

3 Answers3

10

Two things:

For googleAccessId use:

String googleAccessId = service.getServiceAccountName();

And for Signature use:

SigningResult signingResult = service
            .signForApp(stringToSign.getBytes());
String encodedSignature = new String(Base64.encodeBase64(
            signingResult.getSignature(), false), "UTF-8");

That's what worked for me. See below a sample signer class:

public class GcsAppIdentityServiceUrlSigner  {

    private static final int EXPIRATION_TIME = 5;
    private static final String BASE_URL = "https://storage.googleapis.com";
    private static final String BUCKET = "my_bucket";
    private static final String FOLDER = "folder";


    private final AppIdentityService identityService = AppIdentityServiceFactory.getAppIdentityService();

    public String getSignedUrl(final String httpVerb, final String fileName) throws Exception {
        final long expiration = expiration();
        final String unsigned = stringToSign(expiration, fileName, httpVerb);
        final String signature = sign(unsigned);

        return new StringBuilder(BASE_URL).append("/")
                .append(BUCKET)
                .append("/")
                .append(FOLDER)
                .append("/")
                .append(fileName)
                .append("?GoogleAccessId=")
                .append(clientId())
                .append("&Expires=")
                .append(expiration)
                .append("&Signature=")
                .append(URLEncoder.encode(signature, "UTF-8")).toString();
    }

    private static long expiration() {
        final long unitMil = 1000l;
        final Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, EXPIRATION_TIME);
        final long expiration = calendar.getTimeInMillis() / unitMil;
        return expiration;
    }

    private String stringToSign(final long expiration, String filename, String httpVerb) {
        final String contentType = "";
        final String contentMD5 = "";
        final String canonicalizedExtensionHeaders = "";
        final String canonicalizedResource = "/" + BUCKET + "/" + FOLDER + "/" + filename;
        final String stringToSign = httpVerb + "\n" + contentMD5 + "\n" + contentType + "\n"
                + expiration + "\n" + canonicalizedExtensionHeaders + canonicalizedResource;
        return stringToSign;
    }

    protected String sign(final String stringToSign) throws UnsupportedEncodingException {
        final SigningResult signingResult = identityService
                .signForApp(stringToSign.getBytes());
        final String encodedSignature = new String(Base64.encodeBase64(
                signingResult.getSignature(), false), "UTF-8");
        return encodedSignature;
    }

    protected String clientId() {
        return identityService.getServiceAccountName();
    }
}
Fábio Uechi
  • 807
  • 7
  • 25
  • Fabio, I've got it generating the encoded URL using your suggestion, but now I get: -SignatureDoesNotMatchThe request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.GET 1374729586 /[my_bucket]/[my_folder]/file.png – user1869501 Jul 25 '13 at 05:15
  • I just added the full source code above. I hope that works for you! – Fábio Uechi Jul 26 '13 at 01:10
  • what are you using for Base64? I am using org.apache.commons.codec.binary.Base64. I wonder if that is the wrong one. I use your code, but import org.apache.commons.codec.binary.Base64 and still get SignatureDoesNotMatch. – user1869501 Jul 26 '13 at 05:56
  • I finally got it to work. I had to upgrade to the latest SDK for App Engine. As soon as I did this, the signatures matched and all worked using your solution. Thank you Fabio!!!!! – user1869501 Aug 01 '13 at 05:31
  • I was able to get a signedUrl using this example. However, once I have the URL, how do I go about uploading a file to cloud storage? – Julio Jul 29 '15 at 16:44
1

1/ Add the google api google-api-services-storage to your dependencies

2/ Then you need to create ServiceAccountAuthCredentials object from your service account ID and primary key :

serviceAccountAuthCredentials = AuthCredentials.createFor(resources.getString("authentication.p12.serviceAccountId"), pk);

3/ Finally you generate the signed URL from the ServiceAccountAuthCredentials object and the bucket and file name (no need to generate a String to sign) :

public String getSignedURL(String bucket, String fileName) throws IOException, GeneralSecurityException {
    BlobId blobId = BlobId.of(bucket, fileName);
    Blob blob = cloudStorageService.get(blobId);
    URL signedURL = blob.signUrl(durationSignedURLAvailable, TimeUnit.MINUTES, com.google.cloud.storage.Storage.SignUrlOption.signWith(serviceAccountAuthCredentials));
    return signedURL.toString();
}

This works fine for me.

Gwendal Le Cren
  • 490
  • 3
  • 17
0

Using storage.cloud.google.com is requesting cookie based authenticated downloads. Changing:

String baseURL = "https://storage.cloud.google.com/myproj/foo.txt"

to

String baseURL = "https://storage.googleapis.com/myproj/foo.txt"

should work better.

I'm not sure why you're only seeing this when using the URL created via App Engine. Maybe you're not logged in to Google when testing the App Engine application? Or running it in the local dev server?

See the reference URIs section of the docs for details on possible request URIs.

Brian Dorsey
  • 4,588
  • 24
  • 27
  • When I change the base URL, I now get: SignatureDoesNotMatchThe request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.GET 1373350049767 /spinaction/intervals/1/thumb.png. running my Java App, URL generated works and successfully returns the image. AppIdentityService.signForApp(stringToSign.getBytes()); is not doing the same thing as my signData() in my stand alone Java Application since I physically load my private key to sign with. – user1869501 Jul 09 '13 at 06:13
  • Double check that googleAccessId is set to your App Engine service account email address, and confirm that email address has been added as a team member in http://cloud.google.com/console for the project which owns the bucket. Team settings are in the gear menu in the upper right of the console. – Brian Dorsey Jul 09 '13 at 16:46