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 Certificates from Azure Key Vault in ASP.NET Core (Damien Bowden) […]
[…] Using Certificates from Azure Key Vault in ASP.NET Core – Damien Bowden […]
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.
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.
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
@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!
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);
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);
}
}
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
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