Using Certificates from Azure Key Vault in ASP.NET Core

This post shows how you can create and use X509 certificates in Azure Key Vault. The certificates are created using Azure CLI and are used inside an ASP.NET Core application.

Code: StsServerIdentity/Services/Certificate

Setup using Azure CLI

Azure CLI can be used to setup the Azure Key Vault and also create certificates for an existing Key Vault. Azure CLI is documented and can be downloaded here: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli

When Azure CLI is installed and running, you need to login.

 
az login

If the login does not work, you might have to allow your default browser to open the insecure redirect URL to complete the login. For example, when using Chrome, use the following to reset the HSTS. Unsecure URLs are required for this Azure az login. Maybe this should be fixed.

 
chrome://net-internals/#hsts

Creating certificates in an Azure Key Vault

A policy is required to create certificates in Azure Key Vault. You can get the default policy from your Azure subscription using the following request:

az keyvault certificate get-default-policy | Out-File `
 -Encoding utf8 defaultpolicy.json

Your policy could look like this:

{
  "issuerParameters": {
    "certificateTransparency": null,
    "name": "Self"
  },
  "keyProperties": {
    "curve": null,
    "exportable": true,
    "keySize": 2048,
    "keyType": "RSA",
    "reuseKey": true
  },
  "lifetimeActions": [
    {
      "action": {
        "actionType": "AutoRenew"
      },
      "trigger": {
        "daysBeforeExpiry": 90
      }
    }
  ],
  "secretProperties": {
    "contentType": "application/x-pkcs12"
  },
  "x509CertificateProperties": {
    "keyUsage": [
      "cRLSign",
      "dataEncipherment",
      "digitalSignature",
      "keyEncipherment",
      "keyAgreement",
      "keyCertSign"
    ],
    "subject": "CN=CLIGetDefaultPolicy",
    "validityInMonths": 12
  }
}

Now you can create a certificate in the Azure Key Vault. Using the policy above, enter an existing Key Vault name and the name of the certificate family.

az keyvault certificate create `
  --vault-name vaultName `
  -n certificatesKeyVaultName `
  --policy `@defaultpolicy.json

This can be viewed in the Azure Portal Key Vault.

Call the command a second time so that a second certificate is created.

Using the certificates in ASP.NET Core

The following example uses the created certificates for IdentityServer4 signing credentials. We will use the Azure Key Vault to get the new certificates. The newest certificate will be used for signing, the second newest will be used for support of existing sessions. This would make it possible to load a new certificate to the Key Vault, for example in a deployment, and when the hosted application is restarted, the certificates will be updated.

A configuration class is used to setup the certificate management. This can then be used to return the two certificates.

public class CertificateConfiguration
{
	public bool UseLocalCertStore { get; set; }
	public string CertificateThumbprint { get; set; }
	public string CertificateNameKeyVault { get; set; }
	public string KeyVaultEndpoint { get; set; }
	public string DevelopmentCertificatePfx { get; set; }
	public string DevelopmentCertificatePassword { get; set; }
}
	
private static async Task<(X509Certificate2 ActiveCertificate, X509Certificate2 SecondaryCertificate)>
	GetCertificates(IWebHostEnvironment environment, IConfiguration configuration)
{
	var certificateConfiguration = new CertificateConfiguration
	{
		// Use an Azure key vault
		CertificateNameKeyVault = configuration["CertificateNameKeyVault"], 
		KeyVaultEndpoint = configuration["AzureKeyVaultEndpoint"], 

		// development certificate
		DevelopmentCertificatePfx = Path.Combine(environment.ContentRootPath, "sts_dev_cert.pfx"),
		DevelopmentCertificatePassword = "1234" //configuration["DevelopmentCertificatePassword"] 
	};

	(X509Certificate2 ActiveCertificate, X509Certificate2 SecondaryCertificate) 
		certs = await CertificateService.GetCertificates(
		certificateConfiguration).ConfigureAwait(false);

	return certs;
}

The GetCertificates can the be used to get the certificates from the Azure Key Vault. If the app.settings are configured for the Key Vault, the KeyVaultCertificateService will be used to get the certificates.

public static async Task<(X509Certificate2 ActiveCertificate, X509Certificate2 SecondaryCertificate)> GetCertificates(CertificateConfiguration certificateConfiguration)
{
	(X509Certificate2 ActiveCertificate, X509Certificate2 SecondaryCertificate) certs = (null, null);

	if (certificateConfiguration.UseLocalCertStore)
	{
		using X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
		store.Open(OpenFlags.ReadOnly);
		var storeCerts = store.Certificates.Find(
			X509FindType.FindByThumbprint, 
			certificateConfiguration.CertificateThumbprint, 
			false);
			
		certs.ActiveCertificate = storeCerts[0];
		store.Close();
	}
	else
	{
		if (!string.IsNullOrEmpty(certificateConfiguration.KeyVaultEndpoint))
		{
			var keyVaultCertificateService = new KeyVaultCertificateService(
					certificateConfiguration.KeyVaultEndpoint,
					certificateConfiguration.CertificateNameKeyVault);

			certs = await keyVaultCertificateService
				.GetCertificatesFromKeyVault().ConfigureAwait(false);
		}
	}

	// search for local PFX with password, usually local dev
	if(certs.ActiveCertificate == null)
	{
		certs.ActiveCertificate = new X509Certificate2(
			certificateConfiguration.DevelopmentCertificatePfx,
			certificateConfiguration.DevelopmentCertificatePassword);
	}

	return certs;
}

The KeyVaultCertificateService searches for the certificates and returns the two newest ones as required.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.KeyVault.Models;
using Microsoft.Azure.Services.AppAuthentication;

namespace StsServerIdentity.Services.Certificate
{
    public class KeyVaultCertificateService
    {
        private readonly string _keyVaultEndpoint;
        private readonly string _certificateName;

        public KeyVaultCertificateService(string keyVaultEndpoint, string certificateName)
        {
            if (string.IsNullOrEmpty(keyVaultEndpoint))
            {
                throw new ArgumentException("missing keyVaultEndpoint");
            }
            
            _keyVaultEndpoint = keyVaultEndpoint; // "https://damienbod.vault.azure.net"
            _certificateName = certificateName; // certificateName
        }

        public async Task<(X509Certificate2 ActiveCertificate, X509Certificate2 SecondaryCertificate)> GetCertificatesFromKeyVault()
        {
            (X509Certificate2 ActiveCertificate, X509Certificate2 SecondaryCertificate) certs = (null, null);
            var azureServiceTokenProvider = new AzureServiceTokenProvider();
            var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));

            var certificateItems = await GetAllEnabledCertificateVersionsAsync(keyVaultClient);
            var item = certificateItems.FirstOrDefault();
            if (item != null)
            {
                certs.ActiveCertificate = await GetCertificateAsync(item.Identifier.Identifier, keyVaultClient);
            }

            if (certificateItems.Count > 1)
            {
                certs.SecondaryCertificate = await GetCertificateAsync(certificateItems[1].Identifier.Identifier, keyVaultClient);
            }

            return certs;
        }

        private async Task<List<CertificateItem>> GetAllEnabledCertificateVersionsAsync( KeyVaultClient keyVaultClient)
        {
            // Get all the certificate versions (this will also get the currect active version
            var certificateVersions = await keyVaultClient.GetCertificateVersionsAsync(_keyVaultEndpoint, _certificateName);

            // Find all enabled versions of the certificate and sort them by creation date in decending order 
            return certificateVersions
              .Where(certVersion => certVersion.Attributes.Enabled.HasValue && certVersion.Attributes.Enabled.Value)
              .OrderByDescending(certVersion => certVersion.Attributes.Created)
              .ToList();
        }

        private async Task<X509Certificate2> GetCertificateAsync(string identitifier, KeyVaultClient keyVaultClient)
        {
            var certificateVersionBundle = await keyVaultClient.GetCertificateAsync(identitifier);
            var certificatePrivateKeySecretBundle = await keyVaultClient.GetSecretAsync(certificateVersionBundle.SecretIdentifier.Identifier);
            var privateKeyBytes = Convert.FromBase64String(certificatePrivateKeySecretBundle.Value);
            var certificateWithPrivateKey = new X509Certificate2(privateKeyBytes, (string)null, X509KeyStorageFlags.MachineKeySet);
            return certificateWithPrivateKey;
        }

    }
}

Now the certificates can be used in the ConfigureServices Startup method.

            
var x509Certificate2Certs = 
	GetCertificates(_environment, _configuration)
		.GetAwaiter().GetResult();	
			

var identityServer = services.AddIdentityServer()
	.AddSigningCredential(x509Certificate2Certs.ActiveCertificate)
	.AddInMemoryIdentityResources(Config.GetIdentityResources())
	.AddInMemoryApiResources(Config.GetApiResources())
	.AddInMemoryClients(Config.GetClients(stsConfig))
	.AddAspNetIdentity<ApplicationUser>()
	.AddProfileService<IdentityWithAdditionalClaimsProfileService>();

if (x509Certificate2Certs.SecondaryCertificate != null)
{
	identityServer.AddValidationKey(
		x509Certificate2Certs.SecondaryCertificate);
}

When the IdentityServer4 hosted application is started, the signing credentials created from the certificates can be viewed using the jwks endpoint

/.well-known/openid-configuration/jwks

If a new certificate is created in the Azure Key Vault, and the ASP.NET Core application is restarted, the latest certificate will be used to sign the tokens, and the previous certificate will also be supported for existing sessions.

Improvements

This shows one way how Azure Key Vault certificates can be used in an ASP.NET Core application. This could be improved in many ways. For example, it would be good if the certificates were rotated directly from the application, instead of a forced create and application restart. The async method is used in the ConfigureServices method. This could be added directly to the IdentityServer4 extension method for this by using a custom implementation. The next certificate which will be used, could also be made public before the existing certificate is replaced.

Links:

https://github.com/damienbod/IdentityServer4AspNetCoreIdentityTemplate/issues/30

https://docs.microsoft.com/en-us/cli/azure/install-azure-cli

Using Azure Key Vault with ASP.NET Core and Azure App Services

Deploying ASP.NET Core App Services using Azure Key Vault and Azure Resource Manager templates

Using Azure Key Vault from a non-Azure App

10 comments

  1. […] Using Certificates from Azure Key Vault in ASP.NET Core (Damien Bowden) […]

  2. […] Using Certificates from Azure Key Vault in ASP.NET Core – Damien Bowden […]

  3. Hi Damien! Great article. Thanks!
    I am currently working on an user chat End-to-End Encrypting concept like https://developer.virgilsecurity.com/docs/e3kit/fundamentals/end-to-end-encryption/. Do you think Azure Key Vault can be used here as a certificate store that hosts one certificate for each chat user? This probably means Azure Key Vault might host hundreds or thousands of certificates.

  4. Hi Damien, Do you know how you would achieve the above with the new `Azure.Security.KeyVault` libraries. There appears no way to extract the secret by SecretId, so its hard to get the pfx component of the Key Vault certificate.

    https://docs.microsoft.com/en-us/dotnet/api/overview/azure/security.keyvault.certificates-readme?view=azure-dotnet

    https://docs.microsoft.com/en-us/dotnet/api/overview/azure/security.keyvault.secrets-readme?view=azure-dotnet#retrieve-a-secret

    Any advice on this would be great.

  5. Hi Damien,

    Just wondering how you would achieve the same using the new `Azure.Security.KeyVault` libraries:

    1. azure.security.keyvault.certificates
    https://docs.microsoft.com/en-us/dotnet/api/overview/azure/security.keyvault.certificates-readme?view=azure-dotnet

    2. azure.security.keyvault.secrets
    https://docs.microsoft.com/en-us/dotnet/api/overview/azure/security.keyvault.secrets-readme?view=azure-dotnet#retrieve-a-secret

    Particularly with regards to getting the PFX content from the certificate SecretId. The new libraries don’t seem to offer a way to get the Secret by SecretId or SecretIdentifier, only by name.

    Kind regards,
    Sunil

  6. @sunil @sunillath We are taking the following approach. (With apologies to Damien; we’re getting into stackoverflow territory here…)

    I think there may be some confusion with storing Private Keys in Azure. Yes, they are a `secret`, but they are foremost a `key`.

    Azure Key Vault has a “Secrets” store, and a “Key” store. We’ve been able to upload Private Keys from our public certs, as a stand-alone Key resource in Azure. You do need the private key password to create the upload. And you give it a name in the upload form. Example: “SunilSsoPrivateKey”.

    Inside your app, you need to authenticate to Azure. There are two ways to do this: 1. clientId & clientSecret OR 2. certificate. Your infosec team will have an opinion on this 🙂

    Once you have an Azure KeyVaultClient, then you can access the Key resource by its name. “SunilSsoPrivateKey”.
    Finally, we use the returned Key to create an RSA object and sign the JWTs.
    Hope this helps!

    1. Thanks Benxamin, Yes this is helpful, it reinforces what I put in place, which makes use of the certificate name / secret name.

      KeyVaultSecret certificatePfxContent = await secretClient.GetSecretAsync(certificateName).ConfigureAwait(false);

      byte[] certificateWithPrivateKeyDecoded = Convert.FromBase64String(certificatePfxContent.Value);

      var certificate = new X509Certificate2(certificateWithPrivateKeyDecoded, (string)null);

  7. Hi Damien,

    Thanks for the code. I used it and updated it to use the new packages rather than the depreciated one.

    Weirdly, I could not get GetPropertiesOfSecretVersionsAsync to work as documented here
    https://techcommunity.microsoft.com/t5/apps-on-azure/keyvault-secrets-rotation-management-in-bulk/ba-p/2145339

    public class KeyVaultCertificateService
    {
    private readonly string _keyVaultEndpoint;
    private readonly string _certificateName;

    public KeyVaultCertificateService(string certificateKeyVaultName, string certificateName)
    {
    if (string.IsNullOrEmpty(certificateKeyVaultName))
    {
    throw new ArgumentException(“missing CertificateKeyVaultName”);
    }
    if (string.IsNullOrEmpty(certificateName))
    {
    throw new ArgumentException(“missing CertificateName”);
    }

    _keyVaultEndpoint = $”https://{certificateKeyVaultName}.vault.azure.net/”;
    _certificateName = certificateName;
    }

    public async Task GetCertificatesFromKeyVault()
    {
    (X509Certificate2 ActiveCertificate, X509Certificate2 SecondaryCertificate) certs = (null, null);

    var secretClient = new SecretClient(new Uri(_keyVaultEndpoint), new DefaultAzureCredential());

    var certificateItems = await GetAllEnabledCertificateVersionsAsync(secretClient);
    var item = certificateItems.FirstOrDefault();
    if (item != null)
    {
    certs.ActiveCertificate = await GetCertificateAsync(item.Name, secretClient);
    }

    if (certificateItems.Count > 1)
    {
    certs.SecondaryCertificate = await GetCertificateAsync(certificateItems[1].Name, secretClient);
    }

    return certs;
    }

    private async Task<List> GetAllEnabledCertificateVersionsAsync(SecretClient secretClient)
    {
    // Find all enabled versions of the certificate and sort them by creation date in decending order
    return secretClient.GetPropertiesOfSecretVersions(_certificateName)
    .Where(p => (p.Enabled.GetValueOrDefault()))
    .OrderByDescending(p => p.CreatedOn.GetValueOrDefault())
    .ToList();
    }

    private async Task GetCertificateAsync(string identitifier, SecretClient secretClient)
    {
    KeyVaultSecret certSecret = await secretClient.GetSecretAsync(identitifier);
    var privateKeyBytes = Convert.FromBase64String(certSecret.Value);

    return new X509Certificate2(privateKeyBytes, (string)null, X509KeyStorageFlags.MachineKeySet);
    }
    }

    1. This should work if the Key Vault has allowed the access and Azure Credential you use from your dev is allowed to access. Per default, this is the user from your visual studio

      Greetings Damien

      1. Yeah, I have access and the app service will be granted access. I’ve added a question to stack overflow https://stackoverflow.com/questions/69850510/getpropertiesofsecretversionsasync-does-not-contain-a-definition-of-whereawait

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: