Using an X509 private key to sign data in dotnet core v2 (SHA256)

10,852

Solution 1

If you MUST stick with 4.5, your .NET Framework code is as good as it gets. (Well, you could eliminate the usage of the XML format and just use ExportParameters directly)

In .NET 4.6 the problem was solved with the soft-deprecation (which just means I tell everyone on StackOverflow to not use it) of the PrivateKey property:

using (RSA rsa = certificate.GetRSAPrivateKey())
{
    return rsa.SignData(dataToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}

This is the same code you should write for .NET Core (all versions). Part of the reason for the refactoring was to get people off of the RSACryptoServiceProvider type, which doesn't work well on non-Windows systems.

The verification code would be

using (RSA rsa = certificate.GetRSAPublicKey())
{
    return rsa.VerifyData(
        dataToSign,
        signature,
        HashAlgorithmName.SHA256,
        RSASignaturePadding.Pkcs1);
}

Much less code, stronger type-safety, doesn't have the PROV_RSA_FULL problem, no key exporting/importing...

Solution 2

Solution

To be able to verify in .NET 4.5 the data signed using a X509 RSA private key in dotnet core v2.0

Verification code (.NET 4.5)

public void VerifySignedData(byte[] originalData, byte[] signedData, X509Certificate2 certificate)
{
    using (var rsa = (RSACryptoServiceProvider)certificate.PublicKey.Key)
    {
        if (rsa.VerifyData(originalData, CryptoConfig.MapNameToOID("SHA256"), signedData))
        {
            Console.WriteLine("RSA-SHA256 signature verified");
        }
        else
        {
            Console.WriteLine("RSA-SHA256 signature failed to verify");
        }
    }
}

Signing Code (dotnet core v2.0)

private byte[] SignData(X509Certificate2 certificate, byte[] dataToSign)
{
    // get xml params from current private key
    var rsa = (RSA)certificate.PrivateKey;
    var xml = RSAHelper.ToXmlString(rsa, true);
    var parameters = RSAHelper.GetParametersFromXmlString(rsa, xml);

    // generate new private key in correct format
    var cspParams = new CspParameters()
    {
        ProviderType = 24,
        ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider"
    };
    var rsaCryptoServiceProvider = new RSACryptoServiceProvider(cspParams);
    rsaCryptoServiceProvider.ImportParameters(parameters);

    // sign data
    var signedBytes = rsaCryptoServiceProvider.SignData(dataToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

    return signedBytes;
}

Helper Class

public static class RSAHelper
{
    public static RSAParameters GetParametersFromXmlString(RSA rsa, string xmlString)
    {
        RSAParameters parameters = new RSAParameters();

        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.LoadXml(xmlString);

        if (xmlDoc.DocumentElement.Name.Equals("RSAKeyValue"))
        {
            foreach (XmlNode node in xmlDoc.DocumentElement.ChildNodes)
            {
                switch (node.Name)
                {
                    case "Modulus": parameters.Modulus = Convert.FromBase64String(node.InnerText); break;
                    case "Exponent": parameters.Exponent = Convert.FromBase64String(node.InnerText); break;
                    case "P": parameters.P = Convert.FromBase64String(node.InnerText); break;
                    case "Q": parameters.Q = Convert.FromBase64String(node.InnerText); break;
                    case "DP": parameters.DP = Convert.FromBase64String(node.InnerText); break;
                    case "DQ": parameters.DQ = Convert.FromBase64String(node.InnerText); break;
                    case "InverseQ": parameters.InverseQ = Convert.FromBase64String(node.InnerText); break;
                    case "D": parameters.D = Convert.FromBase64String(node.InnerText); break;
                }
            }
        }
        else
        {
            throw new Exception("Invalid XML RSA key.");
        }

        return parameters;
    }

    public static string ToXmlString(RSA rsa, bool includePrivateParameters)
    {
        RSAParameters parameters = rsa.ExportParameters(includePrivateParameters);

        return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent><P>{2}</P><Q>{3}</Q><DP>{4}</DP><DQ>{5}</DQ><InverseQ>{6}</InverseQ><D>{7}</D></RSAKeyValue>",
            Convert.ToBase64String(parameters.Modulus),
            Convert.ToBase64String(parameters.Exponent),
            Convert.ToBase64String(parameters.P),
            Convert.ToBase64String(parameters.Q),
            Convert.ToBase64String(parameters.DP),
            Convert.ToBase64String(parameters.DQ),
            Convert.ToBase64String(parameters.InverseQ),
            Convert.ToBase64String(parameters.D));
    }
}
Share:
10,852
Steve Westwood
Author by

Steve Westwood

Updated on June 20, 2022

Comments

  • Steve Westwood
    Steve Westwood almost 2 years

    I'm having trouble reproducing some cryptographic functionality in dotnet core v2.0. This is code ported from a .NET 4.5 project

    .NET 4.5 code

    public byte[] SignData(byte[] dataToSign, X509Certificate2 certificate)
    {
        var rsaCryptoServiceProvider = new RSACryptoServiceProvider();
        var xml = certificate.PrivateKey.ToXmlString(true);
        rsaCryptoServiceProvider.FromXmlString(xml);
        var signedBytes = rsaCryptoServiceProvider.SignData(dataToSign, CryptoConfig.MapNameToOID("SHA256"));
        return signedBytes;
    }
    

    In dotnet core the ToXmlString() and FromXmlString() methods are not implemented, so I used a helper class workaround. Aside from that the dotnet core implementation works but, given the same input data and certificate it produces a different outcome.

    dotnet core v2.0 code

    public byte[] SignData(byte[] dataToSign, X509Certificate2 certificate)
    {
        var rsaCryptoServiceProvider = new RSACryptoServiceProvider();
        var rsa = (RSA)certificate.PrivateKey;
        var xml = RSAHelper.ToXmlString(rsa);
        var parameters = RSAHelper.GetParametersFromXmlString(rsa, xml);
        rsaCryptoServiceProvider.ImportParameters(parameters);
        SHA256 alg = SHA256.Create();
        var signedBytes = rsaCryptoServiceProvider.SignData(dataToSign, alg);
        return signedBytes;
    }
    

    EDIT

    The dotnet core signed data fails a signature verification check in the .NET 4.5 codebase. Theoretically it should make no difference what the signing method was, so this should work but doesn't.

    public void VerifySignature(byte[] signedData, byte[] unsignedData, X509Certificate2 certificate)
        using (RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)certificate.PublicKey.Key)
        {
            if (rsa.VerifyData(unsignedData, CryptoConfig.MapNameToOID("SHA256"), signedData))
            {
                Console.WriteLine("RSA-SHA256 signature verified");
            }
            else
            {
                Console.WriteLine("RSA-SHA256 signature failed to verify");
            }
        }
    }
    

    Does anyone know if there are compatibility issues between the two methods of signing data?

    EDIT 2

    For clarification this is what both code snippets are attempting:

    1. Taking a X509 certificate's private key which is RSA-FULL and cannot sign using SHA256 encoding
    2. Creating a new private key which is RSA-AES which can sign using SHA256 encoding
    3. Import your X509 private key into this new private key
    4. Signing the required data using this private key.

    The complication comes when attempting the same thing in .NEt4.5 and dotnet core v2.0.

    Seems there are differences between frameworks, libraries and OS's. This answer states that the RSACryptoServiceProvider object relies on the CryptoAPI of the machine the software is on in .NET 4.5, and this informative post shows you the difference on how this is implemented in different environments/frameworks.

    I'm still working on a solution based on this information but am left the central issue, namely that signed data using this dotnet core above cannot be verified by the .NET 4.5 implementation.

    • Maarten Bodewes
      Maarten Bodewes over 6 years
      The correct way of testing signatures is to verify them. Not all signature schemes are deterministic. Without code / test data this question is deemed off topic - well, by me anyway; the code given probably should run, but we cannot tell if there aren't any other errors.
    • bartonjs
      bartonjs over 6 years
      Your code, as written here, makes a new key, exports it, imports it, and signs with it. So each run you'd have a different key, therefore a different signature. Also, don't use RSACryptoServiceProvider, use RSA.Create().
    • Steve Westwood
      Steve Westwood over 6 years
      @MaartenBodewes I've added the failing verification step for clarification.
    • Maarten Bodewes
      Maarten Bodewes over 6 years
      What if there is an error in your RSAHelper class? We still cannot debug it. Look at it from our side: imagine what you would need to debug this. And note that SO is not really an online debugger to begin with; often it is hard to answer "this code is wrong, somewhere, somehow".
    • Maarten Bodewes
      Maarten Bodewes over 6 years
      Are you sure they are not implemented? It seems to me that they are at least documented for .NET core
    • Steve Westwood
      Steve Westwood over 6 years
      Unfortunately those methods are not properly implemented - the RSAHelper is based on the advised workaround. See the related github issue
  • Steve Westwood
    Steve Westwood over 6 years
    This remains code untested for *nix or Mac OS environments. From what I've read, there may be issues.
  • Steve Westwood
    Steve Westwood over 6 years
    this is the way to do it for dotnet core, it is much easier and transportable, trouble is I am interacting with a system that verifies using the .NET 4.5 way above, and there seems to be no way to sign data on a non-windows dotnet core app (in docker for example) that will be verified successfully by older framework that is expecting Microsoft AES & RSA Crypto Service Provider private keys.
  • bartonjs
    bartonjs over 6 years
    .NET Core 2.0 made the RSACryptoServiceProvider type work on non-Windows systems so long as you avoid anything Windows-specific (like asking for its name or loading by name). So the 4.5-and-older code should just work. If your signature doesn't work out between Core and Framework it means you aren't using the same key; possibly you're reading the wrong data, and possibly there's a bug in your import/export code.
  • Steve Westwood
    Steve Westwood over 6 years
    Oddly, now I've converted the original PROV_RSA_FULL cert manually to a Microsoft AES & RSA Crypto Service Provider CSP .pfx so that I can sign the data with SHA256, using your above method and on non-windows environments, now I can't verify that signed data in .net core using your method above. Original verification code in .NET v4.5 continues to verify ok.
  • bartonjs
    bartonjs over 6 years
    @SteveWestwood Weird, yesterday someone brought to my attention a signature that CoreFx said was good that .NET Framework RSACryptoServiceProvider said was bad, but they were manually creating the key. GetRSAPublicKey shouldn't be affected by the CSP of the private key, so nothing weird should happen here. You're welcome to send me a repro with just the public portion of the certificate (and the data or the hash of the data, and the signature) via email. My SO profile links to my GH profile, which has it visible.
  • Steve Westwood
    Steve Westwood over 6 years
    Thanks, I'll try and do that... You might see something simple that I missed :)
  • Steve Westwood
    Steve Westwood over 6 years
    Actually, scratch that, it was just my own stupidity... Was using the incorrect unsigned data to verify against! You method, as above, works perfectly, thanks! (And sorry about the false alarm!)