Runtime: X509Certificate2: Unclear error message when file does not exist

Created on 22 Aug 2016  路  11Comments  路  Source: dotnet/runtime

This issue is similar to dotnet/runtime#17601

An exception with an obscure error message is thrown when creating a X509Certificate2 using a path to a file which does not exist.

Consider the following code:

using System;
using System.Security.Cryptography.X509Certificates;

namespace ConsoleApplication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            new X509Certificate2(fileName: "invalid.cer");
        }
    }
}

on Windows, the following exception is thrown:

Unhandled Exception: Internal.Cryptography.CryptoThrowHelper+WindowsCryptographicException: The system cannot find the file specified
   at Internal.Cryptography.Pal.CertificatePal.FromBlobOrFile(Byte[] rawData, String fileName, String password, X509KeyStorageFlags keyStorageFlags)
   at System.Security.Cryptography.X509Certificates.X509Certificate..ctor(String fileName, String password, X509KeyStorageFlags keyStorageFlags)
   at System.Security.Cryptography.X509Certificates.X509Certificate2..ctor(String fileName)
   at ConsoleApplication.Program.Main(String[] args)

whereas the following exception is thrown on Linux and OS X:

nhandled Exception: Interop+Crypto+OpenSslCryptographicException: error:2006D080:BIO routines:BIO_new_file:no such file
   at Interop.Crypto.CheckValidOpenSslHandle(SafeHandle handle)
   at Internal.Cryptography.Pal.CertificatePal.FromFile(String fileName, String password, X509KeyStorageFlags keyStorageFlags)
   at System.Security.Cryptography.X509Certificates.X509Certificate..ctor(String fileName, String password, X509KeyStorageFlags keyStorageFlags)
   at System.Security.Cryptography.X509Certificates.X509Certificate2..ctor(String fileName)
   at ConsoleApplication.Program.Main(String[] args)

As such, this confirms to the MSDN specifications of the X509Certificate2 constructor because all it says is that a CryptographicException should be thrown - which is the case.

It seems that the error message contains too much information - I'd argue it should only read "no such file" or a user-friendly string, and the underlying OpenSSL error code (2006D080) could go on a different property of the OpenSslCryptographicException class.

area-System.Security

Most helpful comment

I found the solution here:

http://www.daves-blog.net/post/2014/06/16/X509Certificate-The-System-cannot-find-the-file-specified.aspx

Apparently, I need to enable the Load User Profile on the Application Pool. However, the error now changes to this:

crit: IdentityServer4.Hosting.IdentityServerMiddleware[0]
  Unhandled exception: Internal.Cryptography.CryptoThrowHelper+WindowsCryptographicException: Keyset does not exist
     at System.Security.Cryptography.CngKey.Open(String keyName, CngProvider provider, CngKeyOpenOptions openOptions)
     at System.Security.Cryptography.CngKey.Open(String keyName, CngProvider provider)
     at Internal.Cryptography.Pal.CertificatePal.GetPrivateKey[T](Func`2 createCsp, Func`2 createCng)
     at Internal.Cryptography.Pal.CertificatePal.GetRSAPrivateKey()

Looking at the issue now.

Okay, so the issue is permission to read the cert. All we have to do just go to the certificate and grant Read (_only_) for IIS_IUSRS.

All 11 comments

It seems that the error message contains too much information - I'd argue it should only read "no such file" or a user-friendly string

This is the opaque error string OpenSSL hands back from its ERR_error_string_n function; the .NET library isn't actually composing this string, and rather is just asking OpenSSL for an error message for the error code and passing that along.

Well, I guess what I'm saying is that the output of ERR_error_string_n is too verbose to go into the Exception.Message field.

The documentation for ERR_error_string_n says it generates a string in this format:

error:[error code]:[library name]:[function name]:[reason string]

Wouldn't it make more sense for the exception message to be just the reason string?
You can call the OpenSSL ERR_reason_error_string function to get the reason string.

The error code, library name and function name can also be retrieved individually if I read the OpenSSL docs properly; to the extent that they are useful for the upstream code, they could perhaps be exposed on seperate properties of the OpenSslCryptographicException class?

The OpenSslCryptographicException class isn't public; so there wouldn't really be a lot of benefit of making the pieces be in separate fields. (And we don't really want it to be public; because that just encourages "works-on-my-box-but-not-on-yours" type programming).

I don't know that I've ever really heard anyone say that they wished there was less information in an exception message before. We certainly -could- reduce it to just the error reason, leaving the code in the HResult field. That is possibly what we would have done if we'd split the CryptographicException class from the get-go (originally Message was the only place that contained the error code).

I know that as a developer I've often wished that NetFx (Windows) CryptographicExceptions contained the error code in the Message so that when I found random ones in service logs I could look them up by value and/or search for symbolic references.

To quote MSDN (Exception.Message):

Error messages target the developer who is handling the exception. The text of the Message property should completely describe the error and, when possible, should also explain how to correct the error.

To me, the current format is better at "completely describing the error" than if it was just the reason string.

@bartonjs I see your point; among others, having a maximum amount of information available in log files is very valid.

I guess what threw me off when I saw the error message at first, is that was so specific. That made me think it was some very low-level OpenSSL stuff, not just a file not found issue. And, I'm apparently not alone. dotnet/runtime#17601 was created by someone else for the same error; it was resolved by you stating the obvious: that it was a file not found error.

Now, about the OpenSslCryptographicException, I'm a bit confused here.
As I understand it, the OpenSsl implementation of System.Security.Cryptography doesn't pretend to be a 100% compatible implementation of what we have on Windows. For example, there's the RSACng versus RSAOpenSsl, which as a consumer of the Crypto API I can special case for, and the error messages which are returned (for example, when a file is not found) are different as well.

So let's say I want to handle the CryptographicException and figure out if it's something I can either compensate for or provide a better error message for. For example, let's say I want to catch the file not found condition and rethrow it as a FileNotFoundException.

I assumed I could write code like this:

catch (CrypographicException ex)
{
   var cngException = ex as CngException;

   if (cngException != null)
   {
       if (cngException.HResult == ERROR_FILE_NOT_FOUND)
       {
           throw new FileNotFoundException($"The file {file} does not exist");
       }
   }

   var openSslException = ex as OpenSslException;

   if (openSslException != null)
   {
       if (openSslException.HResult == 0x2006D080)
       {
           throw new FileNotFoundException($"The file {file} does not exist");
       }
   }

   // If all else fails, rethrow the original exception
   throw;
}

(Yes, this is a silly example, as I could do a File.Exists check before calling the code, but you get the point)

Since I cannot do this, what would the recommended way be to do error handling? Parse the error message and try to determine whether it's an OpenSSL/CNG error message and take things from there?

If this kind of error handling is what you refer to by "works-on-my-box-but-not-on-yours" type programming", I get your point. You could argue that these are really implementation details which as a caller, I should not worry about. After all: my code would break if a new crypto implementation gets added?

On the other hand, because the crypto API abstraction is "leaky" (there are plenty of locations where the underlying API is exposed, such as in the error messages or the RSACng vs RSAOpenSsl classes), I would say that for me, as a caller, it's valid to worry about the implementation details.

So net:

  • The more information the better
  • "File not found" is probably a special case (because it's such as simple error condition) where a clearer error message would help; but I understand this may not be the top priority :-)
  • I do think surfacing the error data in a structured way, and having an Exception class per Crypto implementation, would be very benificial

Right now the way to do it would be

``` C#
catch (CrypographicException ex)
{
if (ex.HResult == ERROR_FILE_NOT_FOUND || ex.HResult == 0x2006D080)
{
throw new FileNotFoundException($"The file {file} does not exist");
}

// If all else fails, rethrow the original exception
throw;
}
```

Optionally involving RuntimeInformation.IsOSPlatform(OSPlatform.Windows).

But, the feedback is appreciated. It's a hard balance of implementation detail hiding and a leaky abstraction. But right now a) it's hard to create an OpenSSL-specific exception at the right layer, and b) we're not sure that we want people writing catch (OpenSslCryptographicException).

The original report was about the message from a file not found when opening a certificate. While the current format of exception strings from OpenSSL error codes is different from what we have for error codes on Windows it isn't a violation of the exception guidelines.

As for making the OpenSslCryptographicException type public: Right now the Algorithms and X509Certificates libraries are trying to be platform agnostic. Leaking rich platform-specific exception types out of those two libraries is counter to our current design plan. If there's evidence that our information hiding is dangerous or disruptive that should be captured in a fresh issue and can be addressed individually.

But the string change currently has no action, so closing this issue.

Hi,

I'm getting the same issue while deploying IdentityServer 4 to Azure VM (Windows Server 2012 R2).

crit: IdentityServer4.Hosting.IdentityServerMiddleware[0]
      Unhandled exception: Internal.Cryptography.CryptoThrowHelper+WindowsCryptographicException: The system cannot find the file specified
         at System.Security.Cryptography.CngKey.Open(String keyName, CngProvider provider, CngKeyOpenOptions openOptions)
         at System.Security.Cryptography.CngKey.Open(String keyName, CngProvider provider)
         at Internal.Cryptography.Pal.CertificatePal.GetPrivateKey[T](Func`2 createCsp, Func`2 createCng)
         at Internal.Cryptography.Pal.CertificatePal.GetRSAPrivateKey()

This will work on local dev computer and I cannot figure it out why. I install the cert on My (Local Machine) store on the VM.

Can anyone help on this. Thanks.

I found the solution here:

http://www.daves-blog.net/post/2014/06/16/X509Certificate-The-System-cannot-find-the-file-specified.aspx

Apparently, I need to enable the Load User Profile on the Application Pool. However, the error now changes to this:

crit: IdentityServer4.Hosting.IdentityServerMiddleware[0]
  Unhandled exception: Internal.Cryptography.CryptoThrowHelper+WindowsCryptographicException: Keyset does not exist
     at System.Security.Cryptography.CngKey.Open(String keyName, CngProvider provider, CngKeyOpenOptions openOptions)
     at System.Security.Cryptography.CngKey.Open(String keyName, CngProvider provider)
     at Internal.Cryptography.Pal.CertificatePal.GetPrivateKey[T](Func`2 createCsp, Func`2 createCng)
     at Internal.Cryptography.Pal.CertificatePal.GetRSAPrivateKey()

Looking at the issue now.

Okay, so the issue is permission to read the cert. All we have to do just go to the certificate and grant Read (_only_) for IIS_IUSRS.

@rizamarhaban I have the exact same issue as you but my certificate is not stored locally so how can I give read access to it? I simply load the certificate in memory via AWS

Is there a reason cng needs to create a file? Why can't it do it all in memory?

@amartens181 It can. It uses the filesystem only when creating or loading persisted (named) private keys. Surprise filesystem actions generally come from loading a PFX, because the default is to create temporary persisted private keys. To leave the disk out of the picture you can specify X509KeyStorageFlags.EphemeralKeySet (though some components on Windows use the key name to access a key, and they don't work with ephemeral private keys).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

terrajobst picture terrajobst  路  158Comments

terrajobst picture terrajobst  路  193Comments

ebickle picture ebickle  路  318Comments

iSazonov picture iSazonov  路  139Comments

syeshchenko picture syeshchenko  路  199Comments