IAM Signblob and Service Accounts
A Google Cloud Service Account contains an RSA key pair. When Google Cloud creates a service account an RSA key pair managed by Google Cloud is created. When you create a service account key, another RSA key pair is created. You can see the RSA public key in a web browser.
Let’s create a service account with no permissions and no keys.
1 |
gcloud iam service-accounts create signblob-1 --display-name="Test service account for Signblob" |
The name format for a service account is:
1 |
NAME@PROJECT_ID.iam.gserviceaccount.com |
Now, display the details of the new service account. Assuming the NAME is signblob-1 and the Project ID is development-123456:
1 |
gcloud iam service-accounts describe signblob-1@development-123456.iam.gserviceaccount.com --format=json |
This generates output similar to:
1 2 3 4 5 6 7 8 9 |
{ "displayName": "Test service account for Signblob", "email": "signblob-1@development-123456.iam.gserviceaccount.com", "etag": "MDEwMjE5MjA=", "name": "projects/development-123456/serviceAccounts/signblob-1@development-219304.iam.gserviceaccount.com", "oauth2ClientId": "116061841092976422609", "projectId": "development-123456", "uniqueId": "116061841092976400609" } |
Let’s view the RSA public key for the Google Cloud managed RSA key pair in a web browser. The URL is built from a base URL plus the service account email address.
The base URL is:
1 |
https://www.googleapis.com/robot/v1/metadata/x509/ |
The service account email address.
1 |
signblob-1@development-123456.iam.gserviceaccount.com |
Open the combined URL in a web browser.
1 |
https://www.googleapis.com/robot/v1/metadata/x509/signblob-1@development-123456.iam.gserviceaccount.com |
The browser will display the Google Cloud managed service account key ID and the RSA public key:
1 2 3 |
{ "3dca8be066d98115296c7730361452e56bca472b": "-----BEGIN CERTIFICATE-----\nMIIDOjCCAiKgAwIBAgIIYqnuYRUsfvIwDQYJKoZIhvcNAQEFBQAwQDE+MDwGA1UE\nAxM1c2lnbmJsb2ItMS5kZXZlbG9wbWVudC0yMTkzMDQuaWFtLmdzZXJ2aWNlYWNj\nb3VudC5jb20wHhcNMjEwODI0MjI0NTUwWhcNMjMwOTAyMDQ0OTM4WjBAMT4wPAYD\nVQQDEzVzaWduYmxvYi0xLmRldmVsb3BtZW50LTIxOTMwNC5pYW0uZ3NlcnZpY2Vh\nY2NvdW50LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKVv6smd\nKeI/9M/Nk9gGmo52R75UOTixkGDABcTAUAVf54mCk6q626YTM9ASksBymwW4W+E+\nSjOQkUOGGOntSUPj7tojlPkDCL7ubfPTf7IoRxWZ2DcPyM6wmjbLUQsEuExbPgxX\ncVyrtq1rl0gUAVFEmZ+AJDxL+tRxH6SnYLxg914OYIE5i7mnTGzQSq+UpjTzgfVW\ny7hmuQWZLAF47fctXQWwmrJAloPPu942lvs+HRfptSs17K1CA5QJDIahPDRj8y0q\nCEp+vkiHEOEq+W7/69NiAScfYeufySRxBvKZ9SuJiC+6C98FDGkB1UtCldFMTA+4\n7Ujd1eEIgRas5Z0CAwEAAaM4MDYwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMC\nB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQEFBQADggEBAAxe\nH4QIhCEH8tg+q3m9yCxY4neYEU14YYa8Dt38QzGKN4gay/dcyxiJRsnQhmf78qj2\nqmq9p0ANps10ICcQh579Cij4mX1GUu2YTPC94nHB/Fmr6s1ImU9wkyJrweROyzf6\nW5FK1it8BDXbEps9Osx67xBsVErrWgnMtXoxBPciTOsZqYkde4IZbtKaDs/ZDvPC\n5ggEjPxY1aAVVtAeI4JJNLoEKYFKRTr9NHezVhsdEMFbEnId5u5T4KYBDshmr7TJ\npUKTM6s1Ti1sBmAGZVIjdSV6FkPnX7CAOctx7+ny/ZXQPucnR0aLUo4Glaim+hP8\nvgKj9o6SOzACiJiTxag=\n-----END CERTIFICATE-----\n" } |
Remember, we have not yet created or downloaded a service account key.
Additional formats are RAW and JWK:
1 2 |
https://www.googleapis.com/robot/v1/metadata/raw/signblob-1@development-123456.iam.gserviceaccount.com https://www.googleapis.com/robot/v1/metadata/jwk/signblob-1@development-123456.iam.gserviceaccount.com |
Let’s now use the service account to sign some data. Create a file named data.in. Enter the following text in the file:
1 |
This is test data. |
My text editor appends \r\n at the end of the line.
Sign the data:
1 2 |
gcloud iam service-accounts sign-blob data.in data.out \ --iam-account=signblob-1@development-123456.iam.gserviceaccount.com |
Which outputs:
1 |
signed blob [data.in] as [data.out] for [signblob-1@development-123456.iam.gserviceaccount.com] using key [3dca8be066d98115296c7730361452e56bca472b] |
Notice the value for using key
1 |
3dca8be066d98115296c7730361452e56bca472b |
That is the same key ID as the Google Cloud managed service account key ID.
The signature is written to data.out as binary data.
Base-64 encode the contents of data.out:
1 |
base64 data.out |
Which outputs:
1 2 3 4 5 |
YeUcr830QRk+7vC2TNlxk+goPiudAsZaF41iWsjc4UDDQ9tdVAKdXmEJeSNXRggW1D7KLXVm33qQ Mgvsdgfqfonka4xHWyrMqxvnfHAgNrZT+kgLVREPBY1JcSArkCHO83ZZtg3miNhQYLY5UI0hC+Mb GGUenONlP8XCG8o+SFkxkjpGtv+F1j7+ouMIhV0bsjAVnGFc/Fc9FaJqx2DBlru6qKhISnwno/GM b5//Hua8UCnF6/fW+bIHHEdXNilC2TEAM5ditkCR+THh4FhXeKBsyZw3q6/cGbOXa/C8yAvlm4Tf iBN/KSRC1yUKud1R1hdh2YJqAJmgl5k219KV3A== |
The signature can be verified with openssl provided we have the RSA public key:
1 |
openssl dgst -sha256 -verify public_key.pem -signature data.out data.in |
The public key is extracted from the certificate URL above. Let’s write some code to fetch the RSA public certificate.
Key points in the following code:
- Line 1 specifies the service account key ID discussed above. Replace with the value from your service account.
- Line 2 specifies the service account email address discussed above. Replace with the value from your service account.
- Lines 10-14 build the URL where Google stores the service account RSA public certificates.
- Lines 39 compares the $key_id with the $key to locate the certificate.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
$key_id = "3dca8be066d98115296c7730361452e56bca472b"; $sa_email = "signblob-1@development-123456.iam.gserviceaccount.com"; // Return false on error // Return $certificate on success function get_certificate($key_id, $sa_email) { // The Google Cloud certificate base URL $base_url = "https://www.googleapis.com"; // Build the path based upon service account email address $path = "/robot/v1/metadata/x509/"; $path .= $sa_email; // create the HTTP client $client = new Client([ 'base_uri' => $base_url ]); // Issue request to list all instances in all zones $response = $client->get($path); // Check for response error if ($response->getStatusCode() != 200) { printf("Error:\n"); printf("Status Code: %s\n", $response->getStatusCode()); print($response->getBody()); return false; } $body = $response->GetBody(); $data = json_decode($body, true); foreach($data as $key => $cert) { if (strcmp($key_id, $key) !== 0) { continue; } return $cert; } return false; } |
The above code locates the Google Cloud managed service account key and returns the public certificate. Now, let’s extract the RSA public key so that the openssl example can verify the signature. This function writes the RSA public key to public_key.pem.
1 2 3 4 5 6 7 8 9 10 |
$public_filename = "public_key.pem"; function write_public_key($cert, $public_filename) { $pub_key = openssl_pkey_get_public($cert); $keyData = openssl_pkey_get_details($pub_key); file_put_contents($public_filename, $keyData['key']); } |
Verify the signature written to data.out for the file data.in:
1 |
openssl dgst -sha256 -verify public_key.pem -signature data.out data.in |
The PHP code to perform the same signature validation as openssl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Return 1 on success // Return 0 on verify failure // Return -1 on error function verify_signature($contents, $signature, $cert) { $pub_key = openssl_pkey_get_public($cert); $ok = openssl_verify($contents, $signature, $pub_key, OPENSSL_ALGO_SHA256); openssl_free_key($pub_key); return $ok; } |
Combine the above code fragments into a complete program to verify data blob signature created with Google Cloud IAM Signblob:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
<?php require 'vendor/autoload.php'; use GuzzleHttp\Client; // Replace with values from your service account $key_id = "3dca8be066d98115296c7730361452e56bca472b"; $sa_email = "signblob-1@development-123456.iam.gserviceaccount.com"; $data_filename = "data.in"; $signature_filename = "data.out"; $public_filename = "public_key.pem"; // Return false on error // Return $certificate on success function get_certificate($key_id, $sa_email) { // The Google Cloud certificate base URL $base_url = "https://www.googleapis.com"; // Build the path based upon service account email address $path = "/robot/v1/metadata/x509/"; $path .= $sa_email; // create the HTTP client $client = new Client([ 'base_uri' => $base_url ]); // Issue request to list all instances in all zones $response = $client->get($path); // Check for response error if ($response->getStatusCode() != 200) { printf("Error:\n"); printf("Status Code: %s\n", $response->getStatusCode()); print($response->getBody()); return false; } $body = $response->GetBody(); $data = json_decode($body, true); foreach($data as $key => $cert) { if (strcmp($key_id, $key) !== 0) { continue; } return $cert; } return false; } function write_public_key($cert, $public_filename) { $pub_key = openssl_pkey_get_public($cert); $keyData = openssl_pkey_get_details($pub_key); file_put_contents($public_filename, $keyData['key']); } // Return 1 on success // Return 0 on verify failure // Return -1 on error function verify_signature($contents, $signature, $cert) { $pub_key = openssl_pkey_get_public($cert); $ok = openssl_verify($contents, $signature, $pub_key, OPENSSL_ALGO_SHA256); openssl_free_key($pub_key); return $ok; } function main() { global $key_id; global $sa_email; global $data_filename; global $signature_filename; global $public_filename; $cert = get_certificate($key_id, $sa_email); if ($cert === false) { printf("Error: Cannot find certificate with key ID %s\n", $key_id); exit(1); } write_public_key($cert, $public_filename); $contents = file_get_contents($data_filename); $signature = file_get_contents($signature_filename); $status = verify_signature($contents, $signature, $cert); if ($status === 1) { echo "Verify success" . PHP_EOL; exit(0); } if ($status === 0) { echo "Verify failed" . PHP_EOL; exit(1); } echo "Verify error" . PHP_EOL; exit(2); } main(); |
The next step is to sign a data blob in PHP. This is very easy to do. This method does not use delegates, which I will cover in this article as well.
Review this Google Cloud CLI command:
1 2 |
gcloud iam service-accounts sign-blob data.in sign_rest_data.out \ --iam-account=signblob-1@development-123456.iam.gserviceaccount.com |
The equivalent command written in PHP calling the REST API. The difference is the specification of a service account file, /config/service-account.json.
The CLI uses the same REST API. However, this API is now deprecated. [documentation] The replacement API includes parameters for delegates. [documentation] This example uses the deprecated API. After this example, I show how to use the new API.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
<?php require 'vendor/autoload.php'; use Google\Auth\CredentialsLoader; use GuzzleHttp\Client; $data_filename = "data.in"; $signature_filename = "sign_rest_data.out"; $scopes = ["https://www.googleapis.com/auth/cloud-platform"]; $sa_file = "/config/service-account.json"; $signer_email = "signblob-1@development-123456.iam.gserviceaccount.com"; // Fetch an OAuth Access Token from a service account JSON key file. function get_service_account_credentials($sa_file) { global $scopes; $contents = file_get_contents($sa_file); $json = json_decode($contents, true); $creds = CredentialsLoader::makeCredentials($scopes, $json); $tokens = $creds->fetchAuthToken(); if (!array_key_exists('access_token', $tokens)) { printf("Error: Cannot fetch an OAuth Access Token\n"); exit(1); } $access_token = $tokens['access_token']; return $access_token; } // Read a file and return the contents Base64 encoded function get_contents_base64_encoded($filename) { $contents = file_get_contents($filename); return base64_encode($contents); } // Sign a blob of data that has been Base64 encoded. function signBlob($data, $access_token, $signer_email) { $uri = "https://iam.googleapis.com/v1/projects/-/serviceAccounts/"; $uri .= $signer_email; $uri .= ":signBlob?alt=json"; $payload = [ "bytesToSign" => $data ]; $body = json_encode($payload); $client = new Client(); $response = $client->post( $uri, [ 'body' => $body, 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, 'accept' => 'application/json', 'content-type' => 'application/json' ] ] ); // Check for response error if ($response->getStatusCode() != 200) { printf("Error:\n"); printf("Status Code: %s\n", $response->getStatusCode()); print($response->getBody()); exit(1); } return $response->GetBody(); } // Fetch an OAuth Access Token $access_token = get_service_account_credentials($sa_file); // Base64 encode a file $data = get_contents_base64_encoded($data_filename); // Sign the Base64 encoded data $body = signBlob($data, $access_token, $signer_email); // Load the API response $json = json_decode($body, true); // Get the Key ID and Signing Signature $key_id = $json['keyId']; $signature = $json['signature']; // $signature is Base64 encoded. Decode to binary $data = base64_decode($signature); echo 'Signing with key ID: ' . $key_id . PHP_EOL; echo 'Writing signature to ' . $signature_filename . PHP_EOL; file_put_contents($signature_filename, $data); |
This version uses the new REST API [documentation]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
<?php require 'vendor/autoload.php'; use Google\Auth\CredentialsLoader; use GuzzleHttp\Client; $data_filename = "data.in"; $signature_filename = "sign_rest_data_2.out"; $scopes = ["https://www.googleapis.com/auth/cloud-platform"]; $sa_file = "/config/service-account.json"; $signer_email = "signblob-1@development-123456.iam.gserviceaccount.com"; // Fetch an OAuth Access Token from a service account JSON key file. function get_service_account_credentials($sa_file) { global $scopes; $contents = file_get_contents($sa_file); $json = json_decode($contents, true); $creds = CredentialsLoader::makeCredentials($scopes, $json); $tokens = $creds->fetchAuthToken(); if (!array_key_exists('access_token', $tokens)) { printf("Error: Cannot fetch an OAuth Access Token\n"); exit(1); } $access_token = $tokens['access_token']; return $access_token; } // Read a file and return the contents Base64 encoded function get_contents_base64_encoded($filename) { $contents = file_get_contents($filename); return base64_encode($contents); } // Sign a blob of data that has been Base64 encoded. function signBlob($data, $access_token, $signer_email) { $uri = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/"; $uri .= $signer_email; $uri .= ":signBlob?alt=json"; $payload = [ "delegates" => [], "payload" => $data ]; $body = json_encode($payload); $client = new Client(); $response = $client->post( $uri, [ 'body' => $body, 'headers' => [ 'Authorization' => 'Bearer ' . $access_token, 'accept' => 'application/json', 'content-type' => 'application/json' ] ] ); // Check for response error if ($response->getStatusCode() != 200) { printf("Error:\n"); printf("Status Code: %s\n", $response->getStatusCode()); print($response->getBody()); exit(1); } return $response->GetBody(); } // Fetch an OAuth Access Token $access_token = get_service_account_credentials($sa_file); // Base64 encode a file $data = get_contents_base64_encoded($data_filename); // Sign the Base64 encoded data $body = signBlob($data, $access_token, $signer_email); // Load the API response $json = json_decode($body, true); // Get the Key ID and Signing Signature $key_id = $json['keyId']; $signature = $json['signedBlob']; // $signature is Base64 encoded. Decode to binary $data = base64_decode($signature); echo 'Signing with key ID: ' . $key_id . PHP_EOL; echo 'Writing signature to ' . $signature_filename . PHP_EOL; file_put_contents($signature_filename, $data); |
This example signs data using a service account’s own private key. The service account requires no permissions or roles. This example uses the ServiceAccountCredentials::signBlob method. [source code link] [documentation]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
<?php require 'vendor/autoload.php'; use Google\Auth\CredentialsLoader; $data_filename = "data.in"; // Input file - data to sign $signature_filename = "sign_data.out"; // Ouput file - signature $scopes = ["https://www.googleapis.com/auth/cloud-platform"]; // The service account requires no roles/permissions // to sign data using its own private key $sa_file = "/config/signblob-1.json"; $key_id = ""; // This value is derived from the service account JSON file // Sign a blob of data using the specified service account function signData($sa_file, $data) { global $scopes; global $key_id; $contents = file_get_contents($sa_file); $json = json_decode($contents, true); // Save the Key ID $key_id = $json['private_key_id']; $creds = CredentialsLoader::makeCredentials($scopes, $json); $signature = $creds->signBlob($data); return $signature; } function get_contents($filename) { return file_get_contents($filename); } // Read file $data = get_contents($data_filename); // Sign the data $signature = signData($sa_file, $data); // $signature is Base64 encoded. Decode to binary $data = base64_decode($signature); echo 'Signing with key ID: ' . $key_id . PHP_EOL; echo 'Writing signature to ' . $signature_filename . PHP_EOL; file_put_contents($signature_filename, $data); |
Important Facts
- A service account requires no roles/permissions to sign data using its own private key.
- A user identity or a service account can sign data using another service account.
- The permission iam.serviceAccounts.signBlob is required.
- That permission is contained in the role Service Account Token Creator (roles/iam.
serviceAccountTokenCreator).
- For the previous point, the requestor using another service account for signing is called a delegate. The action is called a delegated request. [documentation]
- A service account has a Google Cloud managed private key that is used by Google Cloud when signing with another service account.
- Google Cloud publishes the public certificate for each service account private key including the managed private key.
Summary
We discussed a number of advanced concepts. Some of these items are not documented but are easily discernable if you understand PKI, certificates, and signing data.
- How to sign a data blob with the Google Cloud CLI.
- How to sign a data blob in PHP.
- How to verify the signature with openssl.
- How to verify the signature in PHP.
- Discussed concepts related to service account key IDs, certificates, and where they are stored.
Photography Credits
Heidi Mustonen just started a new photography company in Seattle, WA. Her company in-TENSE Photography has some amazing pictures. I asked her for some images to include with my new articles. Check out her new website.
I design software for enterprise-class systems and data centers. My background is 30+ years in storage (SCSI, FC, iSCSI, disk arrays, imaging) virtualization. 20+ years in identity, security, and forensics.
For the past 14+ years, I have been working in the cloud (AWS, Azure, Google, Alibaba, IBM, Oracle) designing hybrid and multi-cloud software solutions. I am an MVP/GDE with several.
Leave a Reply