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.

gcloud iam service-accounts create signblob-1 --display-name="Test service account for Signblob"

The name format for a service account is:

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:

gcloud iam service-accounts describe signblob-1@development-123456.iam.gserviceaccount.com --format=json

This generates output similar to:

{
  "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:

https://www.googleapis.com/robot/v1/metadata/x509/

The service account email address.

signblob-1@development-123456.iam.gserviceaccount.com

Open the combined URL in a web browser.

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:

{
  "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:

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:

This is test data.

My text editor appends \r\n at the end of the line.

Sign the data:

gcloud iam service-accounts sign-blob data.in data.out \
--iam-account=signblob-1@development-123456.iam.gserviceaccount.com

Which outputs:

signed blob [data.in] as [data.out] for [signblob-1@development-123456.iam.gserviceaccount.com] using key [3dca8be066d98115296c7730361452e56bca472b]

Notice the value for using key

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:

base64 data.out

Which outputs:

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:

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.
$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.

$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:

openssl dgst -sha256 -verify public_key.pem -signature data.out data.in

The PHP code to perform the same signature validation as openssl:

// 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:

<?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:

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.

<?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]

<?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]

<?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.