无法使用从 AzureKeyVault 读取的证书写入 JWTToken

Cannot write JWTToken with certificate read from AzureKeyVault

我们正在使用自签名证书创建 JwtSecurityToken。我们目前正在手动将证书上传到我们的 Azure 应用服务,然后使用此代码找到它

 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]);

这很好用,但是我们希望不再需要在机器上安装证书 运行 连接应用程序服务,而是从 Azure Key Vault 读取证书。额外的好处是,这意味着应用服务可以 运行 在开发人员计算机上本地化,而无需共享和安装证书。

我们可以使用

从 Azure Key Vault 获取证书
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 我也尝试过重载到需要密码的 X509Certificate2 ctor。

根据这些凭据,我们创建了一个 JwtSecurityToken

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

然后我们使用 JwtSecurityTokenHandler 获取令牌字符串

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

对 WriteToken 的调用导致以下错误消息

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

为什么会这样?

这不起作用的原因是因为从 KeyVault 检索到的证书上的私钥为 Null。如果您需要私钥,则需要将证书作为“秘密”提取。

to 解释原因并参考另一个更详细的答案

同意 Pat Long 的回答,只有当您将 public 密钥作为证书获取时,您才能获得它。是的,这肯定没有任何意义!不得不通过秘密绕道而行感觉很愚蠢。

无论如何,我最终得到的用于从 Azure Key Vault 获取带有私钥的证书的代码如下所示:

    /// <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}");
    }
}