Search code examples
.netsecuritycryptographyrsax509

'X509Certificate2' does not contain a definition for 'CopyWithPrivateKey'


Attempting to use the RSA C# library but getting a weird error. I believe all the needed imports are there...

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

////

X509Certificate2 keyPair = publicX509.CopyWithPrivateKey(rsa);
    return keyPair;

This is returning the following error:

'X509Certificate2' does not contain a definition for 'CopyWithPrivateKey' and no accessible extension method 'CopyWithPrivateKey' accepting a first argument of type 'X509Certificate2' could be found.

I do not understand why this is happening because to my knowledge I have all the necessary imports. Also, when I open it in a separate project file it compiles just fine which leads me believe there's something wrong with the configurations of the project I need it in. Any advice?


Solution

  • The CopyWithPrivateKey(RSA) (extension) method was originally added in .NET Core 2.0, and was added in .NET Framework 4.7.2, per https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.rsacertificateextensions.copywithprivatekey?view=net-5.0#applies-to.

    Unfortunately, .NET Standard 2.0 is essentially the intersection of .NET Core 2.1 and .NET Framework 4.6.1 (per https://learn.microsoft.com/en-us/dotnet/standard/net-standard), which means that the RSACertificateExtensions class does not have that method if you're targeting .NET Standard 2.0.

    There are a couple of different workarounds:

    • If you don't need both .NET Framework and .NET (nee Core), switch from .NET Standard 2.0 to a framework-specific target.

    This one's pretty self explanatory.

    • If you're making a NuGet package, target all three of .NET Standard 2.0, .NET Framework (4.7.2+) and .NET Core 3.1 / .NET 5.0+

    In this case, I'd do something like

    internal static X509Certificate2 CopyWithPrivateKey(X509Certificate2 cert, RSA key)
    {
    #if NETFRAMEWORK || NET_5_0_OR_GREATER
        return cert.CopyWithPrivateKey(key);
    #else
        throw new PlatformNotSupportedException("This build does not know how to find CopyWithPrivateKey.");
    #endif
    }
    

    This works because the NuGet package will continue to offer .NET Standard 2.0 as a valid target to library projects, but an application project (which actually runs things) will prefer the appropriate framework-dependent build over the .NET Standard one. So your package is a little bigger, but everything works. (Unless someone builds their exe targeting something older than .NET Framework 4.7.2.)

    • Use reflection

    This can fail, of course, but the odds of that are pretty low (since I believe that .NET Framework 4.7.2 or 4.8 got generally pushed out via Windows Update). Basically, pay a one-time cost of figuring out where CopyWithPrivateKey lives and then invoke it as a delegate:

    private static Func<X509Certificate2, RSA, X509Certificate2> s_copyWithRsa = FindCopyWithRsa();
    
    private static Func<X509Certificate2, RSA, X509Certificate2> FindCopyWithRsa()
    {
        MethodInfo info = typeof(RSACertificateExtensions).GetMethod("CopyWithPrivateKey");
    
        if (info == null)
        {
            return (x, k) =>
                throw new PlatformNotSupportedException("The CopyWithPrivateKey method was not found");
        }
    
        return (Func<X509Certificate2, RSA, X509Certificate2>)info.CreateDelegate(
            typeof(Func<X509Certificate2, RSA, X509Certificate2>));
    }        
    
    internal static X509Certificate2 CopyWithPrivateKey(X509Certificate2 cert, RSA key)
    {
        return s_copyWithRsa(cert, key);
    }
    

    The main drawback to this approach is that it'll have problems in Ahead-of-Time (AoT) compilation with .NET 5+, since it won't see any call to RSACertificateExtensions.CopyWithPrivateKey it'll remove it.

    • Mix and match

    If you want AoT to work, and want to support callers targeting older .NET Framework (but running on newish ones), then compile with netstandard2.0 and net5.0 targets, using reflection in the netstandard2.0 variant and direct compilation in the net5.0 variant.