How to verify xml signature (used in SOAP requests) without usage of SignedXml (which is not available in dotnet core)?
I am trying like this, but it gives me false all the time:
public static void CheckSignature(XElement responseXml, MyResponseType response)
{
string originalDigestValue = Convert.ToBase64String(response.Signature.SignedInfo.Reference.FirstOrDefault().DigestValue);
var originalSignatureValue = response.Signature.SignatureValue.Value;
X509DataType certificateData = (X509DataType)response.Signature.KeyInfo.Items[0];
X509Certificate2 certificate = new X509Certificate2((byte[])certificateData.Items[0]);
//for calculating digest value
//responseXml.Descendants(nm + "Signature").SingleOrDefault().Remove();
//var digestValue = Convert.ToBase64String(SHA1.Create().ComputeHash(System.Text.Encoding.UTF8.GetBytes(responseXml.Document.ToString())));
XNamespace nm = #"http://www.w3.org/2000/09/xmldsig#";
var signedInfoNode = responseXml.Descendants(nm + "SignedInfo").SingleOrDefault();
var signedInfo = signedInfoNode.ToString().Trim();
byte[] signedInfoBytes = Encoding.UTF8.GetBytes(signedInfo);
var hash = SHA1.Create().ComputeHash(signedInfoBytes);
RSA rsa = certificate.GetRSAPublicKey();
try
{
Console.WriteLine("Signed Info: \n" + signedInfo);
Console.WriteLine("Verification: \n" + rsa.VerifyData(signedInfoBytes, originalSignatureValue, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1));
Console.WriteLine("Verification hash: \n" + rsa.VerifyData(hash, originalSignatureValue, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1));
}
catch (Exception exc)
{
//
}
}
xmldsig is a very large, very complicated spec. You can try to implement it if you like, the complicated bits are turning the XML document into bytes for doing signing and verification (the canonicalization (or c14n) spec is separate, and large).
SignedXml should be available now with .NET Core 2.0 Preview 1, and upgrading is definitely your easiest bet.
Related
I'm writing a .NET 6 application for Windows that is intended to extract the private key from a PFX file containing an RSA cert/key bundle.
public static Boolean ToCertAndKey(String pfxFilePath, String? unlockPassword, String certFilePath, String keyFilePath, String? keyPassword, out String error) {
try {
error = String.Empty;
using var bundle = new X509Certificate2(pfxFilePath, unlockPassword);
RSA key = bundle.GetRSAPrivateKey();
Byte[] publicKeyBytes = key.ExportSubjectPublicKeyInfo();
Byte[] privateKeyBytes;
//We fail here.
if (String.IsNullOrEmpty(keyPassword)) {
privateKeyBytes = key.ExportPkcs8PrivateKey();
} else {
privateKeyBytes = key.ExportEncryptedPkcs8PrivateKey(keyPassword,
new PbeParameters(
PbeEncryptionAlgorithm.Aes256Cbc,
HashAlgorithmName.SHA256,
iterationCount: 1));
}
String encodedCert = new(PemEncoding.Write("PUBLIC KEY", publicKeyBytes));
File.WriteAllText(certFilePath, encodedCert);
String encodedKey = new(PemEncoding.Write("PRIVATE KEY", privateKeyBytes));
File.WriteAllText(keyFilePath, encodedKey);
return true;
} catch (Exception ex) {
error = $"An exception occurred: '{ex.Message}'\r\n\r\nStack Trace:\r\n{ex.StackTrace}";
return false;
}
}
It fails at both ExportPkcs8PrivateKey (When I don't specify a password to encrypt the key) and ExportEncryptedPkcs8PrivateKey (when I do) with the same exception text:
WindowsCryptographicException: The requested operation is not supported
I came across this answer however, I'm still receiving the same exception at RSA.ExportEncryptedPkcs8PrivateKey.
There doesn't appear to be anything wrong with the PFX files I've been testing with; I'm able to import them into my certstore via the UI or PowerShell with no issues.
Hoping someone else has run into this issue.
You need to mark the keys as exportable.
Change
using var bundle = new X509Certificate2(pfxFilePath, unlockPassword);
to
using var bundle = new X509Certificate2(pfxFilePath, unlockPassword, X509KeyStorageFlags.Exportable);
I'm running this code successfully on my Windows machine (Win 10 x64, running dotnet 4.7.2). It generates an EC keypair ("P-256"), hashes the plaintext with SHA-256, signs the hash with the ec private key and verifies the signature against the hashed plaintext with the ec public key.
I'm getting this output so everything works fine:
EC signature curve secp256r1 / P-256 string
dataToSign: The quick brown fox jumps over the lazy dog
* * * sign the plaintext with the EC private key * * *
EC keysize: 256
signature (Base64): cwLBRSt1vtO33tHWcTdx1OTu9lBFXHEJgvdRyDUynLLE5eMakUZUAKLwaJvYoS7NBylx2Zz0+G6dvgJ6xv5qNA==
* * *verify the signature against hash of plaintext with the EC public key * * *
signature verified: True
Now I'm trying to find any online compiler that is been able to run the code. My favorite compiler
(https://repl.it/, Mono C# compiler version 6.8.0.123, full code: https://repl.it/#javacrypto/EcSignatureFull#main.cs) is running into this error:
Unhandled Exception:
System.NotImplementedException: The method or operation is not implemented.
at EcSignatureString.Main () [0x00036] in <13e2ad358a924efc874a89efad35ffe7>:0
[ERROR] FATAL UNHANDLED EXCEPTION: System.NotImplementedException: The method or operation is not implemented.
at EcSignatureString.Main () [0x00036] in <13e2ad358a924efc874a89efad35ffe7>:0
exit status 1
Using another platform (https://dotnetfiddle.net/, Compiler .net 5, full code: https://dotnetfiddle.net/lSPpjz) is giving this similar error:
Unhandled exception. System.PlatformNotSupportedException: Windows Cryptography Next Generation (CNG) is not supported on this platform.
at System.Security.Cryptography.ECDsaCng..ctor(Int32 keySize)
at EcSignatureString.Main()
Command terminated by signal 6
So my question: is there any online compiler available that is been able to run the code?
I assume my question might be a slice off-topic for SO - in this case - is there any other stackexchange-site that would be a better place for my question?
Warning: the following code has no exception handling and is for educational purpose only:
using System;
using System.Security.Cryptography;
class EcSignatureString {
static void Main() {
Console.WriteLine("EC signature curve secp256r1 / P-256 string");
string dataToSignString = "The quick brown fox jumps over the lazy dog";
byte[] dataToSign = System.Text.Encoding.UTF8.GetBytes(dataToSignString);
Console.WriteLine("dataToSign: " + dataToSignString);
try {
Console.WriteLine("\n* * * sign the plaintext with the EC private key * * *");
ECDsaCng ecDsaKeypair = new ECDsaCng(256);
Console.WriteLine("EC keysize: " + ecDsaKeypair.KeySize);
byte[] hashedData = null;
byte[] signature = null;
// create new instance of SHA256 hash algorithm to compute hash
HashAlgorithm hashAlgo = new SHA256Managed();
hashedData = hashAlgo.ComputeHash(dataToSign);
// sign Data using private key
signature = ecDsaKeypair.SignHash(hashedData);
string signatureBase64 = Convert.ToBase64String(signature);
Console.WriteLine("signature (Base64): " + signatureBase64);
// get public key from private key
string ecDsaPublicKeyParametersXml = ecDsaKeypair.ToXmlString(ECKeyXmlFormat.Rfc4050);
// verify
Console.WriteLine("\n* * *verify the signature against hash of plaintext with the EC public key * * *");
ECDsaCng ecDsaVerify = new ECDsaCng();
bool signatureVerified = false;
ecDsaVerify.FromXmlString(ecDsaPublicKeyParametersXml, ECKeyXmlFormat.Rfc4050);
signatureVerified = ecDsaVerify.VerifyHash(hashedData, signature);
Console.WriteLine("signature verified: " + signatureVerified);
}
catch(ArgumentNullException) {
Console.WriteLine("The data was not signed or verified");
}
}
}
Microsoft has decided that encryption and hashing must be fully delegated to the OS (in .NET Framework it was half and half), so now .NET 5 (and .NET Core) has multiple backends for encryption (for example for ECDsa it has ECDsaCng that uses Windows services and ECDsaOpenSsl for Linux/MacOs that uses OpenSsl (see MSDN)
Now... the solution for your problem is to use the ECDsa class and let it select its backend. There are some problems with it. You can't easily export the keys to xml format, nor you can easily export them to PEM format. You can easily export them to a byte[], and you can easily import them from PEM format. This isn't really a big problem, because rarely you'll need to generate keys, and normally your program receives its keys from an external source, or if it generates them itself, it can save them to binary format to reuse them later.
var dataToSignString = "Hello world!";
var dataToSign = Encoding.UTF8.GetBytes(dataToSignString);
Console.WriteLine("dataToSign: " + dataToSignString);
try
{
Console.WriteLine("\n* * * sign the plaintext with the EC private key * * *");
var ecDsaKeypair = ECDsa.Create(ECCurve.NamedCurves.nistP256);
// Normally here:
//ecDsaKeypair.ImportFromPem()
Console.WriteLine("EC keysize: " + ecDsaKeypair.KeySize);
byte[] hashedData = null;
byte[] signature = null;
// create new instance of SHA256 hash algorithm to compute hash
HashAlgorithm hashAlgo = new SHA256Managed();
hashedData = hashAlgo.ComputeHash(dataToSign);
// sign Data using private key
signature = ecDsaKeypair.SignHash(hashedData);
string signatureBase64 = Convert.ToBase64String(signature);
Console.WriteLine("signature (Base64): " + signatureBase64);
// get public key from private key
string ecDsaPublicKeyParameters = Convert.ToBase64String(ecDsaKeypair.ExportSubjectPublicKeyInfo());
// verify
Console.WriteLine("\n* * *verify the signature against hash of plaintext with the EC public key * * *");
var ecDsaVerify = ECDsa.Create(ECCurve.NamedCurves.nistP256);
bool signatureVerified = false;
// Normally here:
//ecDsaKeypair.ImportFromPem()
var publicKey = Convert.FromBase64String(ecDsaPublicKeyParameters);
ecDsaVerify.ImportSubjectPublicKeyInfo(publicKey, out _);
signatureVerified = ecDsaVerify.VerifyHash(hashedData, signature);
Console.WriteLine("signature verified: " + signatureVerified);
}
catch (ArgumentNullException)
{
Console.WriteLine("The data was not signed or verified");
}
About the From/ToXmlFormat, the current comment on them on the github of .NET Core is:
// There is currently not a standard XML format for ECC keys, so we will not implement the default
// To/FromXmlString so that we're not tied to one format when a standard one does exist. Instead we'll
// use an overload which allows the user to specify the format they'd like to serialize into.
Mmmh from some tests done, exporting in PEM format seems to be quite easy:
public static IEnumerable<string> Split(string str, int chunkSize)
{
for (int i = 0; i < str.Length; i += chunkSize)
{
yield return str.Substring(i, Math.Min(chunkSize, str.Length - i));
}
}
and then
string b64privateKey = Convert.ToBase64String(ecDsaKeypair.ExportPkcs8PrivateKey());
b64privateKey = string.Join("\r\n", Split(b64privateKey, 64));
string pemPrivateKey = "-----BEGIN PRIVATE KEY-----\r\n" + b64privateKey + "\r\n-----END PRIVATE KEY-----";
or
string b64publicKey = Convert.ToBase64String(ecDsaKeypair.ExportSubjectPublicKeyInfo());
b64publicKey = string.Join("\r\n", Split(b64publicKey, 64));
string pemPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" + b64publicKey + "\r\n-----END PUBLIC KEY-----";
(note that I had to split the string manually in blocks of 64 characters, that is the exact number given in the rfc7468, because Convert.ToBase64String() supports only the 76 line length)
In the past I have been making secure TcpListener by exporting a PFX certificate with a password, but would like to know if this step could be skipped.
I'm not using commercial SSL certificates, and have a Root CA, that I use to issue server certificates. These server certificates require additional steps when hosting a TcpListener in C# (I guess because the CSR wasn't used)... but what if I do have the Private Key, and the Certificate that OpenSSL generates/uses.
sslCertificate = new X509Certificate2("myExportedCert.pfx", "1234");
So this is great, however I have to issue an openssl command to make a pfx file from the Certificate and the Private Key, then make up some password. Then include this password in my code.
I was wondering if this step was quite necessary. Is there a way to make up a X509Certificate2 from the Cert, and then apply the Private Key. The constructor arguments allow the Cert only part, but encrypting fails then because there is no private key.
Also, I don't want to rely on OpenSSL or IIS to export the pfx.... seems clumsy.
Ideally i would like:
sslCertificate = new X509Certificate2("myCert.crt");
sslCertificate.ApplyPrivateKey(keyBytes) // <= or "private.key" or whatever
sslStream.AuthenticateAsServer(sslCertificate, false, SslProtocols.Default, false);
There are a couple of different things you're asking for, with different levels of ease.
Attaching a private key to a certificate
Starting in .NET Framework 4.7.2 or .NET Core 2.0 you can combine a cert and a key. It doesn't modify the certificate object, but rather produces a new cert object which knows about the key.
using (X509Certificate2 pubOnly = new X509Certificate2("myCert.crt"))
using (X509Certificate2 pubPrivEphemeral = pubOnly.CopyWithPrivateKey(privateKey))
{
// Export as PFX and re-import if you want "normal PFX private key lifetime"
// (this step is currently required for SslStream, but not for most other things
// using certificates)
return new X509Certificate2(pubPrivEphemeral.Export(X509ContentType.Pfx));
}
on .NET Framework (but not .NET Core) if your private key is RSACryptoServiceProvider or DSACryptoServiceProvider you can use cert.PrivateKey = key, but that has complex side-effects and is discouraged.
Loading the private key
This one is harder, unless you've already solved it.
For the most part the answer for this is in Digital signature in c# without using BouncyCastle, but if you can move to .NET Core 3.0 things get a lot easier.
PKCS#8 PrivateKeyInfo
Starting in .NET Core 3.0 you can do this relatively simply:
using (RSA rsa = RSA.Create())
{
rsa.ImportPkcs8PrivateKey(binaryEncoding, out _);
// do stuff with the key now
}
(of course, if you had a PEM you need to "de-PEM" it, by extracting the contents between the BEGIN and END delimiters and running it through Convert.FromBase64String in order to get binaryEncoding).
PKCS#8 EncryptedPrivateKeyInfo
Starting in .NET Core 3.0 you can do this relatively simply:
using (RSA rsa = RSA.Create())
{
rsa.ImportEncryptedPkcs8PrivateKey(password, binaryEncoding, out _);
// do stuff with the key now
}
(as above, you need to "de-PEM" it first, if it was PEM).
PKCS#1 RSAPrivateKey
Starting in .NET Core 3.0 you can do this relatively simply:
using (RSA rsa = RSA.Create())
{
rsa.ImportRSAPrivateKey(binaryEncoding, out _);
// do stuff with the key now
}
(same "de-PEM" if PEM).
In the end i did this, and it works fine:
...
if (!File.Exists(pfx)) {
// Generate PFX
string arguments = "openssl pkcs12 -export -in " + certPath + "" + certFile + ".crt -inkey " + certPath + "" + certFile + ".key -out " + certPath + "" + certFile + ".pfx -passout pass:" + pfxPassword;
ProcessStartInfo opensslPsi = new ProcessStartInfo("sudo", arguments);
opensslPsi.UseShellExecute = false;
opensslPsi.RedirectStandardOutput = true;
using (Process p = Process.Start(opensslPsi)) {
p.WaitForExit();
}
// Set Permission
ProcessStartInfo chmodPsi = new ProcessStartInfo("sudo", "chmod 644 " + certPath + "" + certFile + ".pfx");
chmodPsi.UseShellExecute = false;
chmodPsi.RedirectStandardOutput = true;
using (Process p = Process.Start(chmodPsi)) {
p.WaitForExit();
}
}
sslCertificate = new X509Certificate2(pfx, pfxPassword);
...
I have been wrapping my head around this problem for some time now. I am to implement a signature check, which is already implemented using c# but we need to implement it on iOS also.
The c# code looks like this.
var signedData = version + "#" + string.Join("#", deltas.Select(s => s.Key + "=" + s.Value));
var signature = Convert.FromBase64String(vertification);
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{ //TODO: Configuration
rsa.FromXmlString("<RSAKeyValue><Modulus>a long text here</Modulus><Exponent>a small text here</Exponent></RSAKeyValue>");
var valid = rsa.VerifyData(Encoding.UTF8.GetBytes(signedData), new SHA256Managed(), signature);
return valid;
}
A little catch is that we can't use any cocoa pods or external libraries.
So I'm trying to do a REST API call to AWS' SNS service, but I keep getting an IncompleteSignature error. I based myself on http://www.jokecamp.com/blog/examples-of-creating-base64-hashes-using-hmac-sha256-in-different-languages/#csharp on how to create the signature and http://docs.aws.amazon.com/AmazonSimpleDB/latest/DeveloperGuide/HMACAuth.html to find out what to sign.
Here's the test code I came up with:
static void Main(string[] args)
{
string region = "us-west-2";
string msg = "This is a test!";
string secret = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
string key = "XXXXXXXXXXXXXXX";
string arn = "arn:aws:sns:us-west-2:xxxxxxxxxx:snstest1";
string query = "Action=Publish&Message=" + HttpUtility.UrlEncode(msg) + "&MessageStructure=json&TargetArn=" + HttpUtility.UrlEncode(arn) + "&SignatureMethod=HmacSHA256&AWSAccessKeyId=" + key + "&SignatureVersion=2&Timestamp=" + HttpUtility.UrlEncode(DateTime.UtcNow.ToString("o"));
string tosign = "GET\nsns." + region + ".amazonaws.com\n/\n" + query;
System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
byte[] keyByte = encoding.GetBytes(secret);
byte[] messageBytes = encoding.GetBytes(tosign);
var hmacsha256 = new HMACSHA256(keyByte);
byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
query += "&signature=" + HttpUtility.UrlEncode(Convert.ToBase64String(hashmessage));
Console.WriteLine("REST Call: https://sns." + region + ".amazonaws.com/?" + query);
}
Any idea what might be wrong?
EDIT: I tried changing the signature part with the code from http://wiki.alphasoftware.com/~alphafiv/DotNet+Example%3A+Digital+Hash it uses CharArray instead of the byte[], not sure which is right, it produces a different signature but it still doesn't work with AWS.
EDIT2: After long tries I finally figured out that AWS expects Signature= and not signature=, but now I'm getting a SignatureDoesNotMatch error, so I need to figure that out next. Also I don't know why this kind of question would get downvoted. Once I figure out the syntax, an AWS API call would be trivial to do in any app. If you use the AWS .NET SDK you're adding 6 megs to your binary. How is that not a worthwhile endeavor?
SOLUTION:
This code works and will send a SNS notification without the AWS SDK:
static void Main(string[] args)
{
string region = "us-west-2";
string msg = "Test test: sfdfds\nfsd: sdsda\n";
string secret = "XXXXXXXXXXXXXXXXXXX";
string key = "ZZZZZZZZZZZ";
string arn = "arn:aws:sns:us-west-2:YYYYYYYYYYY:snstest1";
string query = "AWSAccessKeyId=" + Uri.EscapeDataString(key) + "&Action=Publish&Message=" + Uri.EscapeDataString(msg) + "&SignatureMethod=HmacSHA256&SignatureVersion=2&TargetArn=" + Uri.EscapeDataString(arn) + "&Timestamp=" + Uri.EscapeDataString(System.DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"));
string tosign = "GET\nsns." + region + ".amazonaws.com\n/\n" + query;
Console.WriteLine(tosign + "\n");
UTF8Encoding encoding = new UTF8Encoding();
HMACSHA256 hmac = new HMACSHA256(encoding.GetBytes(secret));
string signature = Convert.ToBase64String(hmac.ComputeHash(encoding.GetBytes(tosign)));
query += "&Signature=" + Uri.EscapeDataString(signature);
Console.WriteLine("REST Call: https://sns." + region + ".amazonaws.com/?" + query);
}
There is nothing wrong with rolling your own solution rather than using the SDKs. In fact, I prefer it, because in addition to more lightweight code, you are more likely to understand problems with unexpected behavior because you are working with the native interface.
Here's what you are missing:
Add the query string parameters ... sorted using lexicographic byte ordering
http://docs.aws.amazon.com/general/latest/gr/signature-version-2.html
For example, TargetArn should not be before SignatureMethod. They all need to be sorted. There is only one possible correct signature for any given message, so the sort order is critical.