google-cloud-platformgoogle-cloud-storagegcloudgsutil

GCP Storage - signing key not provided


Even though I found a similar question here:

Signing key was not provided and could not be derived on google precondition

This wouldn't lead to a solution to my problem.

I am locally authenticating to Google Storage default service account and I can easily read bucket objects as follows:

    private val storage: Storage = StorageOptions
        .newBuilder()
        .setProjectId(projectId)
        .build()
        .service

    fun read() {
        val blob = storage
            .get(BlobId.of(bucket, object))

        println(String(blob.getContent()))
    }

however when I try to generate the signed upload url with:

    fun uploadUrl(objectName: String): String = storage
        .signUrl(
            BlobInfo.newBuilder(BlobId.of(bucketName, objectName)).build(),
            15,
            TimeUnit.MINUTES,
            Storage.SignUrlOption.httpMethod(HttpMethod.PUT),
            Storage.SignUrlOption.withExtHeaders(mapOf("Content-Type" to "application/octet-stream")),
            Storage.SignUrlOption.withV4Signature()
        )
        .toString()

I am getting signing key not provided.

I find it hard to recognize what actually I am missing. I am authenticated with gcloud auth application-default login with a owner user and I am normally allowed to perform any gcloud tasks. What is a difference here?


Solution

  • To sign something, you need a private key. With a user credential, you can't, because you only have a refresh token in your environment. However, in a service account key file you have a private key. You can download it and use it, but I don't like it, for security reason.

    I wrote an article and I found a workaround in Python. I built a similar hack in Java (Sorry, I'm not kotlin dev! But I'm sure it's each to convert!).

    Storage storage = StorageOptions.getDefaultInstance().getService();
    
    Credentials credentialsToSIgn = storage.getOptions().getCredentials();
    if (credentialsToSIgn instanceof UserCredentials) {
      credentialsToSIgn = ImpersonatedCredentials.create(
        (GoogleCredentials) credentialsToSIgn,
        "SERVICE_ACCOUNT_EMAIL",
        Collections.EMPTY_LIST, 
        Collections.EMPTY_LIST, 
        3600);
    }
    System.out.println(
      storage.signUrl(
        BlobInfo.newBuilder(BlobId.of(bucketName, objectName)).build(),
        15,
        TimeUnit.MINUTES,
        Storage.SignUrlOption.httpMethod(HttpMethod.PUT),
        Storage.SignUrlOption.withExtHeaders(mapOf("Content-Type" to "application/octet-stream")),
        Storage.SignUrlOption.withV4Signature()
        Storage.SignUrlOption.signWith((ServiceAccountSigner) credentialsToSIgn)
      )
    );
    

    The ImpersonatedCredentials is only here to use the sign method in the class. This sign method uses the IAMUtils.sign method which call the Service Account Credential API, as described in my article.

    It's not a nice hack, but it works. You can put the service account email in parameter, and omit it outside your local environment to sure to not perform wrong things elsewhere.