Using an X509 private key to sign data in dotnet core v2 (SHA256)
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));
}
}
Steve Westwood
Updated on June 20, 2022Comments
-
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()
andFromXmlString()
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:
- Taking a X509 certificate's private key which is RSA-FULL and cannot sign using SHA256 encoding
- Creating a new private key which is RSA-AES which can sign using SHA256 encoding
- Import your X509 private key into this new private key
- 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 over 6 yearsThe 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 over 6 yearsYour 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
, useRSA.Create()
. -
Steve Westwood over 6 years@MaartenBodewes I've added the failing verification step for clarification.
-
Maarten Bodewes over 6 yearsWhat 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 over 6 yearsAre you sure they are not implemented? It seems to me that they are at least documented for .NET core
-
Steve Westwood over 6 yearsUnfortunately those methods are not properly implemented - the
RSAHelper
is based on the advised workaround. See the related github issue
-
Steve Westwood over 6 yearsThis remains code untested for *nix or Mac OS environments. From what I've read, there may be issues.
-
Steve Westwood over 6 yearsthis 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 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 over 6 yearsOddly, now I've converted the original
PROV_RSA_FULL
cert manually to aMicrosoft 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 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 over 6 yearsThanks, I'll try and do that... You might see something simple that I missed :)
-
Steve Westwood over 6 yearsActually, 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!)