New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WindowsCryptographicException "Object was not found" at OpenIddictBuilder.AddEphemeralSigningKey on Windows Server 2008 R2 #204

Closed
atmdevnet opened this Issue Aug 27, 2016 · 13 comments

Comments

Projects
None yet
2 participants
@atmdevnet

atmdevnet commented Aug 27, 2016

I'm using latest version of OpenIdDict in web service asp.net core mvc project. When it runs on my local win 10 or 2012 server then it works like a charm, but when it try to run on win 2008 r2 server then it throws cryptographic exception at AddEphemeralSigningKey call. Is there a way to run openiddict with ephemeral signing key on win 2008 server? Maybe anyone has it running and could help?
My configuration code is:

services.AddOpenIddict<Models.AppUser, Models.AppDbContext>()
                .EnableTokenEndpoint("/connect/token")
                .AllowPasswordFlow()
                .AllowRefreshTokenFlow()
                .DisableHttpsRequirement()
                .UseJsonWebTokens()
                .AddEphemeralSigningKey();

and exception:

Application startup exception: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> Internal.Cryptography.CryptoThrowHelper+WindowsCryptographicException: Object was not found
   at System.Security.Cryptography.CngKeyLite.GenerateNewExportableKey(String algorithm, Int32 keySize)
   at System.Security.Cryptography.RSAImplementation.RSACng.GetDuplicatedKeyHandle()
   at System.Security.Cryptography.RSAImplementation.RSACng.ExportKeyBlob(Boolean includePrivateParameters)
   at System.Security.Cryptography.RSAImplementation.RSACng.ExportParameters(Boolean includePrivateParameters)
   at Microsoft.AspNetCore.Builder.OpenIdConnectServerExtensions.AddKey(IList`1 credentials, SecurityKey key)
   at Microsoft.AspNetCore.Builder.OpenIddictBuilder.<>c.&lt;AddEphemeralSigningKey&gt;b__40_0(OpenIddictOptions options)
   at Microsoft.Extensions.Options.OptionsCache`1.CreateOptions()
   at System.Threading.LazyInitializer.EnsureInitializedCore[T](T& target, Boolean& initialized, Object& syncLock, Func`1 valueFactory)
   at Microsoft.Extensions.Options.OptionsCache`1.get_Value()
   at Microsoft.AspNetCore.Builder.OpenIddictExtensions.UseOpenIddict(IApplicationBuilder app)
   at JPKService.Startup.Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
   --- End of inner exception stack trace ---
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
   at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at Microsoft.AspNetCore.Hosting.Internal.ConfigureBuilder.Invoke(Object instance, IApplicationBuilder builder)
   at Microsoft.AspNetCore.Hosting.Internal.WebHost.BuildApplication()
@PinpointTownes

This comment has been minimized.

Show comment
Hide comment
@PinpointTownes

PinpointTownes Aug 27, 2016

Contributor

Thanks for reporting this bug. TBH, I don't think it's caused by ASOS/OpenIddict (it sounds like a CoreFX bug).

Question: can you reproduce it on the full .NET Desktop framework?

Contributor

PinpointTownes commented Aug 27, 2016

Thanks for reporting this bug. TBH, I don't think it's caused by ASOS/OpenIddict (it sounds like a CoreFX bug).

Question: can you reproduce it on the full .NET Desktop framework?

@atmdevnet

This comment has been minimized.

Show comment
Hide comment
@atmdevnet

atmdevnet Aug 27, 2016

Thanks for quick feedback.
Answer: No, I can't reproduce it on the full .NET Desktop framework. When I switched project to full .net then it ran successfuly on win 2008 r2 without exception.
I didn't think exception was caused by OpenIddict. It is rather problem in AspNet.Security.OpenIdConnect.Server and System.Security.Cryptography related to older win versions.
By the way, thanks for inspiration, running my project on full .net will be my temporary solution :)

atmdevnet commented Aug 27, 2016

Thanks for quick feedback.
Answer: No, I can't reproduce it on the full .NET Desktop framework. When I switched project to full .net then it ran successfuly on win 2008 r2 without exception.
I didn't think exception was caused by OpenIddict. It is rather problem in AspNet.Security.OpenIdConnect.Server and System.Security.Cryptography related to older win versions.
By the way, thanks for inspiration, running my project on full .net will be my temporary solution :)

@PinpointTownes

This comment has been minimized.

Show comment
Hide comment
@PinpointTownes

PinpointTownes Aug 27, 2016

Contributor

Thanks for the details. Is there a chance you could run this snippet on your Windows Server 2008 RC2 machine for me?

var algorithm = RSA.Create();
if (algorithm.KeySize < 2048) {
    algorithm.KeySize = 2048;
}

var parameters = algorithm.ExportParameters(false);

I'd like to ensure it's not related to ASOS before moving this ticket to the right repo.

Contributor

PinpointTownes commented Aug 27, 2016

Thanks for the details. Is there a chance you could run this snippet on your Windows Server 2008 RC2 machine for me?

var algorithm = RSA.Create();
if (algorithm.KeySize < 2048) {
    algorithm.KeySize = 2048;
}

var parameters = algorithm.ExportParameters(false);

I'd like to ensure it's not related to ASOS before moving this ticket to the right repo.

@atmdevnet

This comment has been minimized.

Show comment
Hide comment
@atmdevnet

atmdevnet Aug 27, 2016

I was tested yesterday the same snippet on my win 2008 server R2 coming from AspNet.Security.OpenIdConnect.Server assembly and it passed the tests successfully.
RSA.Create creates RSACng object.

atmdevnet commented Aug 27, 2016

I was tested yesterday the same snippet on my win 2008 server R2 coming from AspNet.Security.OpenIdConnect.Server assembly and it passed the tests successfully.
RSA.Create creates RSACng object.

@PinpointTownes

This comment has been minimized.

Show comment
Hide comment
@PinpointTownes

PinpointTownes Aug 27, 2016

Contributor

So you get the same exception with the snippet I posted?

Contributor

PinpointTownes commented Aug 27, 2016

So you get the same exception with the snippet I posted?

@atmdevnet

This comment has been minimized.

Show comment
Hide comment
@atmdevnet

atmdevnet Aug 27, 2016

no, this snippet ran without exception.

atmdevnet commented Aug 27, 2016

no, this snippet ran without exception.

@PinpointTownes

This comment has been minimized.

Show comment
Hide comment
@PinpointTownes

PinpointTownes Aug 27, 2016

Contributor

Hum, weird. That's pretty much what AddEphemeralKey does internally:

/// <summary>
/// Adds a new ephemeral key used to sign the tokens issued by the OpenID Connect server:
/// the key is discarded when the application shuts down and tokens signed using this key
/// are automatically invalidated. This method should only be used during development.
/// On production, using a X.509 certificate stored in the machine store is recommended.
/// </summary>
/// <param name="credentials">The signing credentials.</param>
/// <param name="algorithm">The algorithm associated with the signing key.</param>
/// <returns>The signing credentials.</returns>
public static IList<SigningCredentials> AddEphemeralKey(
    [NotNull] this IList<SigningCredentials> credentials, [NotNull] string algorithm) {
    if (credentials == null) {
        throw new ArgumentNullException(nameof(credentials));
    }

    if (string.IsNullOrEmpty(algorithm)) {
        throw new ArgumentException("The algorithm cannot be null or empty.", nameof(algorithm));
    }

    switch (algorithm) {
        case SecurityAlgorithms.RsaSha256Signature:
        case SecurityAlgorithms.RsaSha384Signature:
        case SecurityAlgorithms.RsaSha512Signature: {
            // Note: a 1024-bit key might be returned by RSA.Create() on .NET Desktop/Mono,
            // where RSACryptoServiceProvider is still the default implementation and
            // where custom implementations can be registered via CryptoConfig.
            // To ensure the key size is always acceptable, replace it if necessary.
            var rsa = RSA.Create();

            if (rsa.KeySize < 2048) {
                rsa.KeySize = 2048;
            }

#if NET451
            // Note: RSACng cannot be used as it's not available on Mono.
            if (rsa.KeySize < 2048 && rsa is RSACryptoServiceProvider) {
                rsa.Dispose();
                rsa = new RSACryptoServiceProvider(2048);
            }
#endif

            if (rsa.KeySize < 2048) {
                throw new InvalidOperationException("The ephemeral key generation failed.");
            }

            // Note: the RSA instance cannot be flowed as-is due to a bug in IdentityModel that disposes
            // the underlying algorithm when it can be cast to RSACryptoServiceProvider. To work around
            // this bug, the RSA public/private parameters are manually exported and re-imported when needed.
            SecurityKey key;
#if NET451
            if (rsa is RSACryptoServiceProvider) {
                var parameters = rsa.ExportParameters(includePrivateParameters: true);
                key = new RsaSecurityKey(parameters);
                key.KeyId = key.GetKeyIdentifier();

                // Dispose the algorithm instance.
                rsa.Dispose();
            }   

            else {
#endif
                key = new RsaSecurityKey(rsa);
                key.KeyId = key.GetKeyIdentifier();
#if NET451
            }
#endif

            credentials.Add(new SigningCredentials(key, algorithm));

            return credentials;
        }

#if NETSTANDARD1_6
        case SecurityAlgorithms.EcdsaSha256Signature: {
            // Generate a new ECDSA key using the P-256 curve.
            var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);

            var key = new ECDsaSecurityKey(ecdsa);
            key.KeyId = key.GetKeyIdentifier();

            credentials.Add(new SigningCredentials(key, algorithm));

            return credentials;
        }

        case SecurityAlgorithms.EcdsaSha384Signature: {
            // Generate a new ECDSA key using the P-384 curve.
            var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP384);

            var key = new ECDsaSecurityKey(ecdsa);
            key.KeyId = key.GetKeyIdentifier();

            credentials.Add(new SigningCredentials(key, algorithm));

            return credentials;
        }

        case SecurityAlgorithms.EcdsaSha512Signature: {
            // Generate a new ECDSA key using the P-521 curve.
            var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP521);

            var key = new ECDsaSecurityKey(ecdsa);
            key.KeyId = key.GetKeyIdentifier();

            credentials.Add(new SigningCredentials(key, algorithm));

            return credentials;
        }
#endif

        default:
            throw new InvalidOperationException("The specified algorithm is not supported.");
    }
}

Did you test this snippet with .NET Desktop and .NET Core? Was it included in the same app as your OpenIddict-based app?

Contributor

PinpointTownes commented Aug 27, 2016

Hum, weird. That's pretty much what AddEphemeralKey does internally:

/// <summary>
/// Adds a new ephemeral key used to sign the tokens issued by the OpenID Connect server:
/// the key is discarded when the application shuts down and tokens signed using this key
/// are automatically invalidated. This method should only be used during development.
/// On production, using a X.509 certificate stored in the machine store is recommended.
/// </summary>
/// <param name="credentials">The signing credentials.</param>
/// <param name="algorithm">The algorithm associated with the signing key.</param>
/// <returns>The signing credentials.</returns>
public static IList<SigningCredentials> AddEphemeralKey(
    [NotNull] this IList<SigningCredentials> credentials, [NotNull] string algorithm) {
    if (credentials == null) {
        throw new ArgumentNullException(nameof(credentials));
    }

    if (string.IsNullOrEmpty(algorithm)) {
        throw new ArgumentException("The algorithm cannot be null or empty.", nameof(algorithm));
    }

    switch (algorithm) {
        case SecurityAlgorithms.RsaSha256Signature:
        case SecurityAlgorithms.RsaSha384Signature:
        case SecurityAlgorithms.RsaSha512Signature: {
            // Note: a 1024-bit key might be returned by RSA.Create() on .NET Desktop/Mono,
            // where RSACryptoServiceProvider is still the default implementation and
            // where custom implementations can be registered via CryptoConfig.
            // To ensure the key size is always acceptable, replace it if necessary.
            var rsa = RSA.Create();

            if (rsa.KeySize < 2048) {
                rsa.KeySize = 2048;
            }

#if NET451
            // Note: RSACng cannot be used as it's not available on Mono.
            if (rsa.KeySize < 2048 && rsa is RSACryptoServiceProvider) {
                rsa.Dispose();
                rsa = new RSACryptoServiceProvider(2048);
            }
#endif

            if (rsa.KeySize < 2048) {
                throw new InvalidOperationException("The ephemeral key generation failed.");
            }

            // Note: the RSA instance cannot be flowed as-is due to a bug in IdentityModel that disposes
            // the underlying algorithm when it can be cast to RSACryptoServiceProvider. To work around
            // this bug, the RSA public/private parameters are manually exported and re-imported when needed.
            SecurityKey key;
#if NET451
            if (rsa is RSACryptoServiceProvider) {
                var parameters = rsa.ExportParameters(includePrivateParameters: true);
                key = new RsaSecurityKey(parameters);
                key.KeyId = key.GetKeyIdentifier();

                // Dispose the algorithm instance.
                rsa.Dispose();
            }   

            else {
#endif
                key = new RsaSecurityKey(rsa);
                key.KeyId = key.GetKeyIdentifier();
#if NET451
            }
#endif

            credentials.Add(new SigningCredentials(key, algorithm));

            return credentials;
        }

#if NETSTANDARD1_6
        case SecurityAlgorithms.EcdsaSha256Signature: {
            // Generate a new ECDSA key using the P-256 curve.
            var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);

            var key = new ECDsaSecurityKey(ecdsa);
            key.KeyId = key.GetKeyIdentifier();

            credentials.Add(new SigningCredentials(key, algorithm));

            return credentials;
        }

        case SecurityAlgorithms.EcdsaSha384Signature: {
            // Generate a new ECDSA key using the P-384 curve.
            var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP384);

            var key = new ECDsaSecurityKey(ecdsa);
            key.KeyId = key.GetKeyIdentifier();

            credentials.Add(new SigningCredentials(key, algorithm));

            return credentials;
        }

        case SecurityAlgorithms.EcdsaSha512Signature: {
            // Generate a new ECDSA key using the P-521 curve.
            var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP521);

            var key = new ECDsaSecurityKey(ecdsa);
            key.KeyId = key.GetKeyIdentifier();

            credentials.Add(new SigningCredentials(key, algorithm));

            return credentials;
        }
#endif

        default:
            throw new InvalidOperationException("The specified algorithm is not supported.");
    }
}

Did you test this snippet with .NET Desktop and .NET Core? Was it included in the same app as your OpenIddict-based app?

@atmdevnet

This comment has been minimized.

Show comment
Hide comment
@atmdevnet

atmdevnet Aug 27, 2016

Yes, indeed, it's weird. The above code is the code I tested, hoping to find some clue. I tested this code with .net core console standalone project without referencing OpenIddict.
This is my project.json:

{
  "version": "1.0.0-*",
  "buildOptions": {
    "emitEntryPoint": true
  },

  "dependencies": {
    "AspNet.Security.OpenIdConnect.Extensions": "1.0.0-beta7",
    "AspNet.Security.OpenIdConnect.Server": "1.0.0-beta7",
    "Microsoft.NETCore.App": {
      "type": "platform",
      "version": "1.0.0"
    },
    "Microsoft.IdentityModel.Tokens": "5.0.0"
  },

  "frameworks": {
    "netcoreapp1.0": {
      "imports": "dnxcore50"
    }
  }
}

and testing code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;

namespace test
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var k = new List<Microsoft.IdentityModel.Tokens.SigningCredentials>();
            k.AddEphemeralKey();
        }
    }
}

atmdevnet commented Aug 27, 2016

Yes, indeed, it's weird. The above code is the code I tested, hoping to find some clue. I tested this code with .net core console standalone project without referencing OpenIddict.
This is my project.json:

{
  "version": "1.0.0-*",
  "buildOptions": {
    "emitEntryPoint": true
  },

  "dependencies": {
    "AspNet.Security.OpenIdConnect.Extensions": "1.0.0-beta7",
    "AspNet.Security.OpenIdConnect.Server": "1.0.0-beta7",
    "Microsoft.NETCore.App": {
      "type": "platform",
      "version": "1.0.0"
    },
    "Microsoft.IdentityModel.Tokens": "5.0.0"
  },

  "frameworks": {
    "netcoreapp1.0": {
      "imports": "dnxcore50"
    }
  }
}

and testing code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;

namespace test
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var k = new List<Microsoft.IdentityModel.Tokens.SigningCredentials>();
            k.AddEphemeralKey();
        }
    }
}
@PinpointTownes

This comment has been minimized.

Show comment
Hide comment
@PinpointTownes

PinpointTownes Aug 27, 2016

Contributor

Hum, if you can't reproduce this issue consistently, I'm not sure there's something I can do.
FWIW, my main machine uses Windows 7 (which is basically the client equivalent of Windows Server 2008). and it works like a charm.

Contributor

PinpointTownes commented Aug 27, 2016

Hum, if you can't reproduce this issue consistently, I'm not sure there's something I can do.
FWIW, my main machine uses Windows 7 (which is basically the client equivalent of Windows Server 2008). and it works like a charm.

@atmdevnet

This comment has been minimized.

Show comment
Hide comment
@atmdevnet

atmdevnet Aug 27, 2016

It's a pitty :( I did some additional tests with my entire web service .net core project with OpenIddict moved on win 2008 server r2 machine and here are results:

  • when running on Kestrel: it works correctly,
  • when running on IIS Express: it works correctly,
  • when running on IIS: it throws exception on startup.
    On IIS it is configured exactly like on my local win 10 machine and my other win 2012 server where it works correctly.

atmdevnet commented Aug 27, 2016

It's a pitty :( I did some additional tests with my entire web service .net core project with OpenIddict moved on win 2008 server r2 machine and here are results:

  • when running on Kestrel: it works correctly,
  • when running on IIS Express: it works correctly,
  • when running on IIS: it throws exception on startup.
    On IIS it is configured exactly like on my local win 10 machine and my other win 2012 server where it works correctly.
@PinpointTownes

This comment has been minimized.

Show comment
Hide comment
@PinpointTownes

PinpointTownes Aug 28, 2016

Contributor

@atmdevnet thanks for the details, it helped me find where the issue was.

When using IIS, your app process is hosted under a specific application pool identity created by IIS (at least, by default, 'coz you can change that in the pool options).

For reasons I ignore, Windows CNG doesn't properly work with these non-user accounts and returns an error when trying to create a new key: The profile for the user is a temporary profile if the "load user profile" option is enabled in IIS, Object was not found otherwise (the error you're seeing). If you host your app under your own account, everything works like a charm.

It works on .NET Desktop because RSA.Create() doesn't return a RSACng instance (which uses CNG under the hood) but a RSACryptoServiceProvider (which uses CAPI, an older API). Replacing RSA.Create by RSACng results in the same error being returned on .NET Desktop.

This issue is absolutely not caused by ASOS/OpenIddict so there's not much we can do to fix that. I guess we could avoid using RSA.Create() on Windows but I'm not sure I want to add another workaround in this code path.

@bartonjs is this behavior expected with CNG?

Contributor

PinpointTownes commented Aug 28, 2016

@atmdevnet thanks for the details, it helped me find where the issue was.

When using IIS, your app process is hosted under a specific application pool identity created by IIS (at least, by default, 'coz you can change that in the pool options).

For reasons I ignore, Windows CNG doesn't properly work with these non-user accounts and returns an error when trying to create a new key: The profile for the user is a temporary profile if the "load user profile" option is enabled in IIS, Object was not found otherwise (the error you're seeing). If you host your app under your own account, everything works like a charm.

It works on .NET Desktop because RSA.Create() doesn't return a RSACng instance (which uses CNG under the hood) but a RSACryptoServiceProvider (which uses CAPI, an older API). Replacing RSA.Create by RSACng results in the same error being returned on .NET Desktop.

This issue is absolutely not caused by ASOS/OpenIddict so there's not much we can do to fix that. I guess we could avoid using RSA.Create() on Windows but I'm not sure I want to add another workaround in this code path.

@bartonjs is this behavior expected with CNG?

@PinpointTownes

This comment has been minimized.

Show comment
Hide comment
@PinpointTownes

PinpointTownes Aug 28, 2016

Contributor

@atmdevnet note: to generate an ephemeral signing key with CNG without getting an error, you can use CngKeyCreationOptions.MachineKey to force CNG to create an ephemeral key that is not bound to the user profile.

var key = CngKey.Create(CngAlgorithm.Rsa, null, new CngKeyCreationParameters {
    KeyCreationOptions = CngKeyCreationOptions.MachineKey,
    Parameters = { new CngProperty("Length", BitConverter.GetBytes(2048), CngPropertyOptions.None) }
});

options.SigningCredentials.AddKey(new RsaSecurityKey(new RSACng(key)));

Note that it won't work on .NET Desktop due to a bug in IdentityModel: AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet#480

Contributor

PinpointTownes commented Aug 28, 2016

@atmdevnet note: to generate an ephemeral signing key with CNG without getting an error, you can use CngKeyCreationOptions.MachineKey to force CNG to create an ephemeral key that is not bound to the user profile.

var key = CngKey.Create(CngAlgorithm.Rsa, null, new CngKeyCreationParameters {
    KeyCreationOptions = CngKeyCreationOptions.MachineKey,
    Parameters = { new CngProperty("Length", BitConverter.GetBytes(2048), CngPropertyOptions.None) }
});

options.SigningCredentials.AddKey(new RsaSecurityKey(new RSACng(key)));

Note that it won't work on .NET Desktop due to a bug in IdentityModel: AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet#480

@PinpointTownes

This comment has been minimized.

Show comment
Hide comment
@PinpointTownes

PinpointTownes Sep 1, 2016

Contributor

Closing, as this is an external "by design" bug.

@atmdevnet feel free to open a new ticket via https://github.com/dotnet/corefx/issues if you think it should be fixed.

Contributor

PinpointTownes commented Sep 1, 2016

Closing, as this is an external "by design" bug.

@atmdevnet feel free to open a new ticket via https://github.com/dotnet/corefx/issues if you think it should be fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment