0

We are using a self signed certificate to create a JwtSecurityToken. We are currently manually uploading the certificate to our Azure App Service then finding it with this code

 X509Store certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
 certStore.Open(OpenFlags.ReadOnly);
 X509Certificate2Collection certCollection = certStore.Certificates.Find(
                                            X509FindType.FindByThumbprint,
                                            signingCertThumbprint,
                                            false);
 return new X509SigningCredentials(certCollection[0]);

This works fine however we want to move away from having to have the certificate installed on the machine running the appservice and instead read the certificate from Azure Key Vault. The added benefit is that it means the App Service can run locally on a developers machine without the need for a certifcate to be shared and installed.

We can get the certificate from Azure Key Vault using

var certificateClient = new CertificateClient(new Uri("https://ourkeyvault.vault.azure.net/"), new DefaultAzureCredential());

var b2cInviteCertificate = certificateClient.GetCertificate("B2CInvite");

return new X509SigningCredentials(new X509Certificate2(b2cInviteCertificate.Value.Cer));

FWIW I have also tried the overload to the X509Certificate2 ctor that takes a password.

From those credentials we create a JwtSecurityToken

JwtSecurityToken token = new JwtSecurityToken(
                    issuer.ToString(),
                    audience,
                    claims,
                    DateTime.Now,
                    DateTime.Now.AddDays(7),
                    JwtService.signingCredentials.Value);

We then use JwtSecurityTokenHandler to get the token string

JwtSecurityTokenHandler jwtHandler = new JwtSecurityTokenHandler();
return jwtHandler.WriteToken(token);

The call to WriteToken results in the folowing error message

InvalidOperationException: IDX10638: Cannot create the SignatureProvider, 'key.HasPrivateKey' is false, cannot create signatures

Why is this happening?

Pat Long - Munkii Yebee
  • 3,592
  • 2
  • 34
  • 68

2 Answers2

1

Agree with Pat Longs answer, you only get the public key when you get it as a certificate. And yes, it sure does not make any sense! Having to take the detour via secrets just feels stupid.

Anyway, the code I eventually ended up with to get a the certificate with private key from Azure Key Vault looks something like this:

    /// <summary>
    /// Load a certificate (with private key) from Azure Key Vault
    ///
    /// Getting a certificate with private key is a bit of a pain, but the code below solves it.
    /// 
    /// Get the private key for Key Vault certificate
    /// https://github.com/heaths/azsdk-sample-getcert
    /// 
    /// See also these GitHub issues: 
    /// https://github.com/Azure/azure-sdk-for-net/issues/12742
    /// https://github.com/Azure/azure-sdk-for-net/issues/12083
    /// </summary>
    /// <param name="config"></param>
    /// <param name="certificateName"></param>
    /// <returns></returns>
    public static X509Certificate2 LoadCertificate(IConfiguration config, string certificateName)
    {
        string vaultUrl = config["Vault:Url"] ?? "";
        string clientId = config["Vault:ClientId"] ?? "";
        string tenantId = config["Vault:TenantId"] ?? "";
        string secret = config["Vault:Secret"] ?? "";

        Console.WriteLine($"Loading certificate '{certificateName}' from Azure Key Vault");

        var credentials = new ClientSecretCredential(tenantId: tenantId, clientId: clientId, clientSecret: secret);
        var certClient = new CertificateClient(new Uri(vaultUrl), credentials);
        var secretClient = new SecretClient(new Uri(vaultUrl), credentials);

        var cert = GetCertificateAsync(certClient, secretClient, certificateName);

        Console.WriteLine("Certificate loaded");
        return cert;
    }


    /// <summary>
    /// Helper method to get a certificate
    /// 
    /// Source https://github.com/heaths/azsdk-sample-getcert/blob/master/Program.cs
    /// </summary>
    /// <param name="certificateClient"></param>
    /// <param name="secretClient"></param>
    /// <param name="certificateName"></param>
    /// <returns></returns>
    private static X509Certificate2 GetCertificateAsync(CertificateClient certificateClient,
                                                            SecretClient secretClient,
                                                            string certificateName)
    {

        KeyVaultCertificateWithPolicy certificate = certificateClient.GetCertificate(certificateName);

        // Return a certificate with only the public key if the private key is not exportable.
        if (certificate.Policy?.Exportable != true)
        {
            return new X509Certificate2(certificate.Cer);
        }

        // Parse the secret ID and version to retrieve the private key.
        string[] segments = certificate.SecretId.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
        if (segments.Length != 3)
        {
            throw new InvalidOperationException($"Number of segments is incorrect: {segments.Length}, URI: {certificate.SecretId}");
        }

        string secretName = segments[1];
        string secretVersion = segments[2];

        KeyVaultSecret secret = secretClient.GetSecret(secretName, secretVersion);

        // For PEM, you'll need to extract the base64-encoded message body.
        // .NET 5.0 preview introduces the System.Security.Cryptography.PemEncoding class to make this easier.
        if ("application/x-pkcs12".Equals(secret.Properties.ContentType, StringComparison.InvariantCultureIgnoreCase))
        {
            byte[] pfx = Convert.FromBase64String(secret.Value);
            return new X509Certificate2(pfx);
        }

        throw new NotSupportedException($"Only PKCS#12 is supported. Found Content-Type: {secret.Properties.ContentType}");
    }
}
Tore Nestenius
  • 16,431
  • 5
  • 30
  • 40
0

The reason this was not working was because the PrivateKey on the certificate retrived from KeyVault was Null. If you need the private key you need to pull the certificate as a "Secret".

This answer to Azure Key Vault Certificates does not have the Private Key when retrieved via IKeyVaultClient.GetCertificateAsync explains why and refers to another answer with much more detail

Pat Long - Munkii Yebee
  • 3,592
  • 2
  • 34
  • 68