Google Cloud Using a CryptoAPI-backed key as service account key

Using service account keys to authenticate a service account is generally discouraged, but sometimes difficult to avoid. The most common way to use service account keys is to create a new key by using the Cloud Console or gcloud, and then to save the key file on the machine where it’s needed.

That process sounds simple enough, but in reality it’s often more complex:

  • There might be multiple persons involved: As the user deploying the application, you might lack the permission to create the service account yourself, so you have to ask another person or team to create one for you.
  • The machine where you download the key might not be the machine that needs the key: For example, you might download the service account key on your notebook, and then scp it to the server that actually needs the key.

Whenever the process involves multiple machines or users, there is an increased risk that copies of the service account key remain in mailboxes, chat histories, or other locations. In such scenarios, it can be more secure to upload a service account key:

  1. As the user deploying the application, create a self-signed certificate that uses an RSA 2048-bit key pair on the target machine. To create the certificate, you can use openssl, certutil, New-SelfSignedCertificate, or other operating system tools.
  2. Pass the certificate file to the user who has the permission to upload the certificate while keeping the private key on the target machine. When passing the certificate, make sure that it can’t be replaced or tampered with, but you don’t need to keep it confidential.
  3. As the user who has the necessary permissions to manage service account keys, upload the certificate to associate it with a service account.

By following this process, you avoid passing the private key and instead only exchange public information between users.

Creating and uploading a certificate

If you use certificate keys on Windows, you typically want those certificate to be stored and managed in the Windows certificate store. Instead of using openssl to demonstrate how uploading and using a service account key works, let’s make things a little more interesting: Let’s use a certificate that’s stored in the Windows certificate store and uses a CryptoAPI-based key:

  1. Create a self-signed certificate:

    $Certificate = New-SelfSignedCertificate `
      -KeyUsage DigitalSignature `
      -FriendlyName "Sample CryptoAPI service account key" `
      -Subject "Sample CryptoAPI service account key" `
      -KeyExportPolicy NonExportable `
      -CertStoreLocation "cert:\CurrentUser\My" `
      -Provider "Microsoft Base Cryptographic Provider v1.0" `
      -KeySpec Signature
    

    Keep in mind that the certificate store only stores the certificate, the associated private key is stored elsewhere, either by a CryptoAPI Cryptographic Service Provider or by a CNG Key Storage Provider.

    Microsoft Base Cryptographic Provider v1.0 is a CryptoAPI provider, so the command above will cause the key to be managed and stored by the CryptoAPI.

    We’re not ever planning to export the private key from CryptoAPI, so we can set the export-policy to NonExportable.

  2. Next, we export the certificate (but not the private key!) as a PEM file. This step is necessary because gcloud can’t read certificates directly from the certificate store.

    Windows does not include (to my knowledge, anyway) any cmdlet to export a certificate as PEM, but it’s not difficult to do manually:

    "-----BEGIN CERTIFICATE-----" | Out-File -Encoding ASCII cryptoapi-cert.cer
    [Convert]::ToBase64String($Certificate.RawData, [Base64FormattingOptions]::InsertLineBreaks) | 
        Out-File -Encoding ASCII cryptoapi-cert.cer -Append
    "-----END CERTIFICATE-----" | Out-File -Encoding ASCII cryptoapi-cert.cer -Append
    

    The PEM file only contains public information, so it’s safe to copy it to another machine, or to hand it to a colleague so that they can associate it with a service account.

  3. Now create a service account:

    gcloud iam service-accounts create sa-with-cryptoapi-cert
    
  4. Upload the certificate and associate it with the service account:

    gcloud iam service-accounts keys upload cryptoapi-cert.cer `
     --iam-account  [email protected][PROJECT-ID].iam.gserviceaccount.com
    

The uploaded key now shows up in the Cloud Console:

Key in the Cloud Console

Authenticating using the certificate in .NET Core

To use the service account key in a .NET Core application, we need to:

  1. Load the certificate from the certificate store
  2. Create a ServiceAccountCredential and initialize it using the certificate
  3. Specify which scopes we need a credential for

Here’s how that looks like in C#:

static void Main(string[] args)
{
  var emailAddress = "[email protected][PROJECT-ID].iam.gserviceaccount.com";

  //
  // Look up the certificate in the key store.
  //
  using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
  {
    store.Open(OpenFlags.ReadOnly);

    using (var certificate = store.Certificates
        .Cast<X509Certificate2>()
        .Where(c => c.FriendlyName == "Sample CryptoAPI service account key")
        .FirstOrDefault() 
      ?? throw new Exception("Certificate not found"))
    {
      //
      // Initialize a ServiceAccountCredential with the certificate.
      //
      var credential = new ServiceAccountCredential(
        new ServiceAccountCredential.Initializer(emailAddress)
        {
          Scopes = new[] { "https://www.googleapis.com/auth/cloud-platform" }
        }
        .FromCertificate(certificate));

      //
      // Request an access token.
      //
      var accessToken = credential.GetAccessTokenForRequestAsync().Result;

      Console.WriteLine("Authenticated as {0}", emailAddress);
      Console.WriteLine("Token is: {0}", accessToken);
    }
  }
}

Easy, but what about .NET Framework?

Authenticating using the certificate in .NET Framework

If we try to run the same code in .NET Framework, it blows up:

Unhandled Exception: System.Security.Cryptography.CryptographicException: Key not valid for use in specified state.

   at System.Security.Cryptography.CryptographicException.ThrowCryptographicException(Int32 hr)
   at System.Security.Cryptography.Utils.ExportCspBlob(SafeKeyHandle hKey, Int32 blobType, ObjectHandleOnStack retBlob)
   at System.Security.Cryptography.Utils.ExportCspBlobHelper(Boolean includePrivateParameters, CspParameters parameters, SafeKeyHandle safeKeyHandle)
   at Google.Apis.Auth.OAuth2.ServiceAccountCredential.Initializer.FromCertificate(X509Certificate2 certificate) in C:\Apiary\2021-03-18.17-02-24\Src\Support\Google.Apis.Auth\OAuth2\ServiceAccountCredential.cs:line 128
   at CryptoApiNetFx.Program.Main(String[] args) in C:\...\Program.cs:line 33

What’s happening here? On .NET Framework, ServiceAccountCredential tries to export the certificate’s private key, which fails because we marked the key as NonExportable. To run the code on .NET Framework, we have to re-create the certificate without the -KeyExportPolicy NonExportable flag. That’s certainly not great – but if we’re worried about non-exportability, maybe we should not be using CryptoAPI to start with.

Update: This issue has been fixed in v1.52.0 of the Google.Apis libraries, so the workaround isn't necessary anymore if you're using the latest version.

But it’s 2021, should’t we be using CNG instead of CryptoAPI?

We should, but unfortunately, the .NET library has limited support for CNG-based keys because it has to maintain backward-compatibility with .NET 4.5. It’s still possible to use CNG, it just requires a bit more legwork as we’ll see in the next post.

Any opinions expressed on this blog are Johannes' own. Refer to the respective vendor’s product documentation for authoritative information.
« Back to home