The other day at work, I ran into a classic developer headache. I was building an Azure Function on a consumption plan and needed to sign something with a certificate. Simple enough, right? I stored the certificate and its private key safely in Azure Key Vault, as one does. But when I tried to use it, I was greeted by a lovely CryptographicException: Keyset does not exist
. Fun times.
This error is a real head-scratcher because you know the key is there. The problem is that the sandboxed environment of an Azure Function won't let your code load the private key into the default user profile store.
After a bit of digging, I realized the solution had two parts: making sure the key wasn't getting corrupted on its way into Key Vault, and then telling C# how to load it in this restricted environment.
Step 1: Stop Corrupting Your Keys in Key Vault
First things first: stop copying and pasting keys directly into the Azure portal. The UI can sometimes mess with whitespace and corrupt your key, which is a surprisingly common issue.
A much safer bet is to Base64 encode the files first. A quick PowerShell command gets the job done:
# For the public certificate (.cer)
[Convert]::ToBase64String([IO.File]::ReadAllBytes("C:\path\to\your\certificate.cer")) | Set-Clipboard
# For the private key (.key)
[Convert]::ToBase64String([IO.File]::ReadAllBytes("C:\path\to\your\private.key")) | Set-Clipboard
Now, paste the resulting single-line strings into your Key Vault secrets. No more corrupted keys.
Step 2: Load the Certificate with Magic Flags
Now for the fun part in the code. Once you fetch the Base64 strings from Key Vault, you need to load them with some special flags to get around the sandbox limitations.
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
public static class CertificateHelper
{
public static X509Certificate2 LoadCertificateFromKeyVaultSecrets(
string base64Certificate,
string base64PrivateKey)
{
// Decode the secrets from Base64
byte[] certBytes = Convert.FromBase64String(base64Certificate);
byte[] privateKeyBytes = Convert.FromBase64String(base64PrivateKey);
// Create the certificate object from the public part
var certificate = new X509Certificate2(certBytes);
// Load the private key
// This example assumes an RSA key. Adjust for other algorithms if needed.
using (var rsa = RSA.Create())
{
// Import the private key bytes.
// The method (e.g., ImportPkcs8PrivateKey, ImportRSAPrivateKey)
// depends on the format of your original key file.
rsa.ImportPkcs8PrivateKey(privateKeyBytes, out _);
// Combine the certificate with the private key
var certificateWithKey = certificate.CopyWithPrivateKey(rsa);
// Export the combined certificate to a PFX byte array (in memory)
byte[] pfxBytes = certificateWithKey.Export(X509ContentType.Pfx);
// Create the final X509Certificate2 object from the PFX data
// with the required flags. This is the crucial step.
return new X509Certificate2(
pfxBytes,
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable);
}
}
}
So, Why Does This Work?
- Base64 Encoding: This guarantees your key data stays in one piece, avoiding corruption from weird whitespace or line endings in the Azure portal.
- In-Memory PFX: We're not saving any files here. The PFX is just a temporary container created in memory to bundle the certificate and key together.
X509KeyStorageFlags.MachineKeySet
: This is the secret sauce. It tells the runtime to use the machine-level key store, which your Function App can access, instead of the locked-down user one.X509KeyStorageFlags.Exportable
: This is just good practice and allows the key to be exported later if needed, which can save you from other headaches.
And with that, the dreaded "Keyset does not exist" error was gone! Hopefully, this little trick saves you some of the frustration it caused me.