Search code examples
c#identityserver4ef-core-6.0duende-identity-serverduende

Duende Identity Server v6 throwing "WindowsCryptographicException: Key does not exist"


I'm upgrading from identity server 4 to duende identity server 6.

When running locally I can reference a key up on Azure Key Vault no problems via "DefaultAzureCredential" and it sets it correctly.

BUT :(

Then the site runs (site and identity server all running as one) and I login it throws an exception "WindowsCryptographicException: Key does not exist"

            var identityServiceBuild = services.AddIdentityServer(options =>
            {
                options.KeyManagement.Enabled = false;
            });

            string keyVaultName = "mykeyvault";
            var kvUri = "https://" + keyVaultName + ".vault.azure.net";

            var keyClient = new KeyClient(new Uri(kvUri), new DefaultAzureCredential());
            var keyResponse = keyClient.GetKey("devidentityserver");

            RSA rsa = keyResponse.Value.Key.ToRSA();
            key = new RsaSecurityKey(rsa) { KeyId = keyResponse.Value.Properties.Version };

           identityServiceBuild.AddSigningCredential(key, IdentityServerConstants.RsaSigningAlgorithm.RS256);

Here is more of the stack trace if that helps:

Internal.Cryptography.CryptoThrowHelper+WindowsCryptographicException: Key does not exist.
   at Internal.Cryptography.CngCommon.SignHash(SafeNCryptKeyHandle keyHandle, ReadOnlySpan`1 hash, AsymmetricPaddingMode paddingMode, Void* pPaddingInfo, Int32 estimatedSize)
   at System.Security.Cryptography.RSAImplementation.RSACng.SignHash(Byte[] hash, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)
   at Microsoft.IdentityModel.Tokens.AsymmetricSignatureProvider.Sign(Byte[] input)
   at Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities.CreateEncodedSignature(String input, SigningCredentials signingCredentials)
   at Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.CreateTokenPrivate(JObject payload, SigningCredentials signingCredentials, EncryptingCredentials encryptingCredentials, String compressionAlgorithm, IDictionary`2 additionalHeaderClaims, String tokenType)
   at Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.CreateToken(String payload, SigningCredentials signingCredentials, IDictionary`2 additionalHeaderClaims)
   at Duende.IdentityServer.Services.DefaultTokenCreationService.CreateJwtAsync(Token token, String payload, Dictionary`2 headerElements) in /_/src/IdentityServer/Services/Default/DefaultTokenCreationService.cs:line 138

Is it because it's expecting the key to be persisted as a row in the "Keys" SQL table? I'm thinking not because I've disabled key management via KeyManagement.Enabled = false?

Help!


Solution

  • The problem is that this line:

    var keyResponse = keyClient.GetKey("devidentityserver");
    

    Only will return the public key from KeyVault, to get the private key, you need to get it as a secret. (yes, its odd).

    This is the code I use in my setup to get the key from keyvault:

            /// <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:ClientSecret"] ?? "";
    
                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}");
            }
        }
    }