0
votes

I am using signed-urls to give my clients temporary access to google cloud storage objects
I have a service account json which looks like this:

{
  "type": "service_account",
  "project_id": "my-project",
  "private_key_id": "abcdef1234567890",
  "private_key": "-----BEGIN PRIVATE KEY-----\n<key...>\n-----END PRIVATE KEY-----\n",
  "client_email": "[email protected]",
  "client_id": "1234567890",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/my-app%my-project.iam.gserviceaccount.com"
}

and here is how I create the signed url -code written in elixir (code from gcs_signer lib)

 def sign_url(private_key, client_email, bucket, object) do
    verb = "GET"
    md5_digest = ""
    content_type = ""
    expires = DateTime.utc_now() |> DateTime.to_unix() |> Kernel.+(1 * 3600)
    resource = "/#{bucket}/#{object}"

    signature = [verb, md5_digest, content_type, expires, resource]
                |> Enum.join("\n") |> generate_signature(private_key)

    url = "https://storage.googleapis.com#{resource}"
    qs = %{
           "GoogleAccessId" => client_email,
           "Expires" => expires,
           "Signature" => signature
         } |> URI.encode_query

    Enum.join([url, "?", qs])
  end

  defp generate_signature(string, private_key) do
    private_key = process_key(private_key)

    string
    |> :public_key.sign(:sha256, private_key)
    |> Base.encode64
  end

  defp process_key(private_key) do
    private_key
    |> :public_key.pem_decode
    |> (fn [x] -> x end).()
    |> :public_key.pem_entry_decode
    |> normalize_private_key
  end

  defp normalize_private_key(private_key) do
    # grab privateKey from the record tuple
    private_key
    |> elem(3)
    |> (fn pk -> :public_key.der_decode(:RSAPrivateKey, pk) end).()
  end

in this was I create a signed url using the private_key from the json file

for security reasons we moved to service-accounts-for-instances instead of using json credentials

my question is how to create a signed url when using service-accounts-for-instances when I don't have a json credential?
the only thing I have is the servie_account_email which look like this: [email protected]
Should I use the signBlob api? if so how would my curl requests look like?

2
When you use (reference) Google Cloud credentials (service account) on Google Compute Engine (any compute service), you do not have access to the Private Key. This means that you must either use Google Cloud libraries or write your own code to use SignBlob (which means that Google is accessing the private key for you). This link provides information on the REST API which you can then map to curl parameters: cloud.google.com/iam/docs/reference/rest/v1/… - John Hanley
To see how the CLI generates API calls add the command line option --log-http. You can then trace an entire presigned URL generated by gcloud. - John Hanley

2 Answers

1
votes

I tried to reproduce your use case:

  1. Create two service accounts:

    gcloud iam service-accounts create signblob --description signblob
    gcloud iam service-accounts create signforme --description signforme
    # signblob will sign for signforme
    
  2. Set the IAM role (roles/iam.serviceAccountTokenCreator) for the signblob service account:

    gcloud projects add-iam-policy-binding myproject --member [email protected] --role roles/iam.serviceAccountTokenCreator
    
  3. Create a VM as a service account using signblob service:

    gcloud compute instances create instance-10 --zone=us-central1-a [email protected] 
    
  4. SSH to the instances just created:

    gcloud compute ssh instance-10 --zone=us-central1-a
    
  5. Create a file on the instance:

    nano file
    cat file
    # This is a file 
    
  6. Sign the file (blob) using gcloud tool for signforme service account using --log-http flag:

    gcloud iam service-accounts sign-blob --iam-account [email protected] file output --log-http
    
  7. Output:

     signed blob [file] as [output] for [[email protected]] 
    

This is the curl command that I run on the VM instance-10 created before:

      curl --request POST 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/signblob-source%40myproject.iam.gserviceaccount.com:signBlob?prettyPrint=true&key=[API KEY]' --header 'Authorization: Bearer [ACCESS TOKEN]'  --header 'Accept: application/json'  --header 'Content-Type: application/json' --data '{"payload":"VGhpcyBpcyBhIGZpbGUgCg=="}'   --compressed

Where ACCESS TOKEN is gcloud auth application-default print-access-token

Output:

   {"keyId": ,"signedBlob":}
1
votes

here is my code in elixir returns an encrypted link in the following format: https://storage.googleapis.com/my-bucket/my-file?Expires=1576437298&GoogleAccessId=my-gsa%40project.iam.gserviceaccount.com&Signature=FUgBzvfFCa0YAL
following google api for signBlob

@base_url @https://storage.googleapis.com

def generate_encrypted_url() do
    gcp_service_account = "[email protected]"
    bucket = "my-bucket", 
    object ="my-file"
    get_signed_url(gcp_service_account, bucket, object)
end



  def get_signed_url(gcp_service_account, bucket, object) do
    %Tesla.Client{pre: [{Tesla.Middleware.Headers, :call, [auth_headers]}]} = get_connection()
    headers = [{"Content-Type", "application/json"}] ++ auth_headers
    url = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/#{gcp_service_account}:signBlob"
    expires = DateTime.utc_now() |> DateTime.to_unix() |> Kernel.+(1 * 3600)
    resource = "/#{bucket}/#{object}"
    signature = ["GET", "", "", expires, resource] |> Enum.join("\n") |> Base.encode64()
    body = %{"payload" => signature} |> Poison.encode!()
    {:ok, %{status_code: 200, body: result}} = HTTPoison.post(url, body, headers)

    %{"signedBlob" => signed_blob} = Poison.decode!(result)
    qs = %{
           "GoogleAccessId" => gcp_service_account,
           "Expires" => expires,
           "Signature" => signed_blob
         } |> URI.encode_query
    Enum.join(["#{@base_url}#{resource}", "?", qs])
  end

  def get_connection() do
    {:ok, token} = Goth.Token.for_scope("https://www.googleapis.com/auth/cloud-platform")
    GoogleApi.Storage.V1.Connection.new(token.token)
  end