4

I'm trying to get a certificate from Azure Keyvault, and then use it to call a REST API which requires a certificate for its authentication.

I've tried doing this locally - I have the .pfx file on disk, I load it into a byte array, and then create my certificate from it:

X509Certificate2 x509 = new X509Certificate2(File.ReadAllBytes(path), password);

and then use that certificate in RestSharp to do my REST call:

IRestClient client = new RestClient(url);
client.ClientCertificates = new X509CertificateCollection { x509 };

var request = new RestRequest(lastUrlPart, Method.GET);
request.AddHeader("Cache-Control", "no-cache");
request.AddHeader("Accept", "application/json");
request.AddHeader("Content-Type", "application/json");

IRestResponse response = client.Execute(request);

if (response.IsSuccessful)
{
    // read out the response and process it
}

works like a charm.

Now I'm trying to do the same, but fetching the certificate from Azure Keyvault. I've created an app registration in Azure AD, created my keyvault, and gave my app registration's service identity access to the keyvault. I've uploaded my certificate into the keyvault. So far, so good.

I've found this code to fetch the certificate from the Keyvault:

var azureServiceTokenProvider = new AzureServiceTokenProvider();

var kv = new KeyVaultClient(async (authority, resource, scope) =>
                                        {
                                            var authContext = new AuthenticationContext(authority, TokenCache.DefaultShared);
                                            var clientCred = new ClientCredential(clientAppId, clientSecret);
                                            var result = await authContext.AcquireTokenAsync(resource, clientCred);

                                            if (result == null)
                                            {
                                                throw new InvalidOperationException("Failed to obtain the JWT token");
                                            }

                                            return result.AccessToken;
                                        });

string certIdentifier = "https://mykeyvault.vault.azure.net/certificates/Certificate-TEST/14753af7586445fe9d57efa136ac090c";

var vaultCertificate = kv.GetCertificateAsync(certIdentifier).GetAwaiter().GetResult();

This also works - I can access the keyvault with my app identity, and I can fetch the certificate from the keyvault, and the X.509 thumbprint is valid - but now this is a CertificateBundle from the Microsoft.Azure.KeyVault.Models namespace - how do I "convert" that into a "regular" X509Certificate2 object so that I can use it for the REST call?

I've tried several things, for instance

X509Certificate2 x509 = new X509Certificate2(vaultCertificate.Cer);

but nothing works - when I place my REST call, I get a HTTP 403 - Forbidden error back....

What am I missing?? How can I fetch a certificate from Azure Keyvault in a format that can be used to authenticate in a subsequent REST call??

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459

3 Answers3

20

Of course - right after I posted this question, I stumbled across the solution....

This blog post by Matt Small explains it in great details (and with really nice, and working code examples, too).

Basically, in order to use the certificate for authentication, you need to have the private key, too - and when you do a GetCertificateAsync, you only get back the public information of the certificate.

You need to fetch the certificate as a secret and then base64 decode it - then you get all the necessary bits and the REST call works.

GOSH !! Why is this so darn convoluted?? Why can't there just be a includePrivate: bool parameter on the GetCertificateAsync call to tell Keyvault if you need just the public, or public and private parts of your certificate?

You store a certificate - but to get it, you need to fetch a secret .... that's just all wrong, and violates the Basic Principle Of Least Surprise! MS, if you're listening - this API really needs a bit more work to make it more approachable to novice Azure devs!!

And PS: this of course also means the user/identity you're using to access the keyvault now needs permissions to read secrets - not just certificates.....

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
1

Try the following code:

var cert = kvc.GetCertificateAsync(baseUrl, "Demo").ConfigureAwait(false).GetAwaiter().GetResult();
var cert_content = cert.Cer;
X509Certificate2 x509 = new X509Certificate2(cert_content);

You can easily get the raw bytes of your certificate from the CertificateBundle, and then use the raw bytes to create your X509Certificate2 instance.

Jack Jia
  • 5,268
  • 1
  • 12
  • 14
  • I had tried that myself (and it's part of my post, too) - but that **didn't** work - see my response - that's the way to go. – marc_s Mar 25 '20 at 09:35
  • It seems that you need the private key. But, you can only retrieve the public certificate from the Azure Key Vault Certificate. – Jack Jia Mar 25 '20 at 09:39
  • So, the only solution should be using the secret to store the whole pfx file content, and then get it and use it to create an X509Certificate2 object. – Jack Jia Mar 25 '20 at 09:42
  • 3
    The certificate is stored as a **certificate** in the Azure Keyvault - but you must retrieve **as a secret** in order to get both public and private components of it. I know - weird and not really clear - I hope MS is listening and improving this Keyvault client API !! – marc_s Mar 25 '20 at 09:47
  • Yes. I think so too. – Jack Jia Mar 25 '20 at 09:51
0

I was doing it with the generally recommended approach of using the secret e.g:

var creds = new DefaultAzureCredential();

var keyVaultUrl = new Uri($"https://{keyVaultName}.vault.azure.net");
var certClient = new CertificateClient(keyVaultUrl, creds);

var certResp = certClient.GetCertificate(certificateName);
var identifer = new KeyVaultSecretIdentifier(certResp.Value.SecretId);

var secretClient = new SecretClient(keyVaultUrl, creds);
var secretResp = secretClient.GetSecret(identifer.Name, identifer.Version);

byte[] privateKeyBytes = Convert.FromBase64String(secretResp.Value.Value);

var cert = new X509Certificate2(privateKeyBytes);

But then I stumbled across DownloadCertificate() which seems to work with less code, i.e.:

var creds = new DefaultAzureCredential();

var keyVaultUrl = new Uri($"https://{keyVaultName}.vault.azure.net");
var certClient = new CertificateClient(keyVaultUrl, creds);

var cert = certClient.DownloadCertificate(certificateName);

As far as I can tell it works fine in my tests, I'd be interested to know why this isn't the recommended approach.

Update

When I released this code to production I hit: WindowsCryptographicException: The system cannot find the file specified

After a bit of digging around, I came across these questions:

  1. CryptographicException was unhandled: System cannot find the specified file
  2. X509Certificate Constructor Exception

It looks like I could solve the problem with this, but in the end I stuck with the original code, because I don't like configuring special setups for a given site - it always comes back to bite.

My final solution looks like this:

public static X509Certificate2 LoadFromKeyVault(string keyVaultName, string certificateName)
{
    if (string.IsNullOrEmpty(keyVaultName))
    {
        throw new ArgumentNullException($"{nameof(keyVaultName)} cannot be empty");
    }

    var creds = new DefaultAzureCredential();

    var keyVaultUrl = new Uri($"https://{keyVaultName}.vault.azure.net");
    var certClient = new CertificateClient(keyVaultUrl, creds);

    var certResp = certClient.GetCertificate(certificateName);
    var identifer = new KeyVaultSecretIdentifier(certResp.Value.SecretId);

    var secretClient = new SecretClient(keyVaultUrl, creds);
    var secretResp = secretClient.GetSecret(identifer.Name, identifer.Version);

    byte[] privateKeyBytes = Convert.FromBase64String(secretResp.Value.Value);

    var result = new X509Certificate2(privateKeyBytes, string.Empty, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
    return result;
}
tappetyclick
  • 472
  • 2
  • 14