I was hoping I might get some help here so that I might finally solve this frustrating problem.
On the java side of things they sign with the following code:
public static void main(String[] args) throws Exception {
if (args.length < 2)
printInfoAndExit();
String cmd = args[0];
Security.addProvider(new BouncyCastleProvider());
Signature signature = Signature.getInstance("SHA1withRSA", "BC");
if ("sign".equalsIgnoreCase(cmd)) {
String pemFileName = args[1];
String dataFileName = args[2];
byte[] data = readFile(dataFileName);
FileReader fr = new FileReader(new File(pemFileName));
PEMReader pemReader = new PEMReader(fr);
KeyPair keyPair = (KeyPair) pemReader.readObject();
fr.close();
signature.initSign(keyPair.getPrivate());
signature.update(data);
byte[] signatureBytes = signature.sign();
writeFile(signatureBytes, dataFileName + ".signed");
String encoded = Base64.encode(signatureBytes);
writeFile(encoded.getBytes(), dataFileName + ".signed.base64");
} else {
printInfoAndExit();
}
}
When I receive the data I have their public key and try to verify with the following C# code:
public static bool Verify(String msg, String signature, String publicKey)
{
RsaKeyParameters remotepubkey = GetRsaPublicKey(publicKey);
ISigner signer = SignerUtilities.GetSigner("SHA1withRSA");
signer.Init(false, remotepubkey);
byte[] sigBytes = Convert.FromBase64String(signature);
byte[] msgBytes = Encoding.Default.GetBytes(msg);
signer.BlockUpdate(msgBytes, 0, msgBytes.Length);
return signer.VerifySignature(sigBytes);
}
This is not working!! I can however verify the data with openssl:
openssl dgst -sha1 -verify public_key.pem -signature data.txt.signed data.txt
The question is, what am I missing to make this work?
NOTE: I don't have a problem with the keys, that is working correctly but somehow there is a difference between how java and .net works with RSA?
**Edit 1 : **In this particular scenario all I had to do was change the GetSigner to
ISigner signer = SignerUtilities.GetSigner("RSA");
Could someone tell me the difference between SHA1withRSA and RSA?
The problem was actually solved on the Java side. They had some issues with their side of things.
You could have an encoding problem with your message data. You've converted the original file data into a unicode string, and are trying to convert it back to raw bytes. Depending on the encoding of the file, and if it's even text at all, your msgBytes could be different from the actual file contents.
Read the raw bytes from the file instead of a string. You don't show the code for actually reading the file data, but I assume you're reading it as text.
Related
I'm not sure how this issue is happening, the key that I'm attempting to pass to the CSP was originally a Base64Encoded string which I've tried passing in using ImportSubjectInfoKey() as well. Each time when debugging I have exported the parameters and I am able to get my public key back as a Base64 string so as far as I know it's a valid key. However once it hits the VerifyData method it breaks with a Bad Key exception. While debugging, I did notice that rsa1.CspKeyContainerInfo was mentioning an error "Exportable: {key does not exist}". Is this where my issue is coming from? I've included the public key below as an XML string if anyone can see an issue.
private static bool VerifyData(string paymentToken, string signature)
{
var rsa1 = new RSACryptoServiceProvider(2048);
string publicKey = #"
<RSAKeyValue><Modulus>zIU140G9rFe6ouNFuhCxIj3Ps3ELUV9w4XTnDsti8kcSTXMf0z6LMNVIqXaZYFbSYXAZRmuM3XNmoSWmMZzPBMl2/C7uC0wyNdrYdPw0uzU2wfr8MQbnvW0yQgQ/cSHNDUZR+n/s2ipXTdNmbRd4z+k+qXxw00xMDmiJu5iMHyYo24x284lTZ3+4dgL4xFlrtjgcb/NGHBpVPQTCbBfEQcmylCwzbTUdBJlAo5ezpziOJ6CNf9FDS1hvRKRvNl7Hx8To6vQZJTwdCT5RWDC2JYL0oSdPV+SZmlfHQQe33p81MiRl4cjp5AwMVKyAosDihGT810WFYhK431EIB/NR/w==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>";
rsa1.FromXmlString(publicKey);
// Converts back to my original Base64 encoded public key so it seems valid.
var x = rsa1.ExportSubjectPublicKeyInfo();
var a = Convert.ToBase64String(x);
RSAPKCS1SignatureDeformatter rsaDeformatter = new(rsa1);
rsaDeformatter.SetHashAlgorithm("SHA256");
var paymentTokenAsBytes = Encoding.UTF8.GetBytes(paymentToken);
var signatureAsBytes = Convert.FromBase64String(signature);
bool verified = false;
try
{
if (rsaDeformatter.VerifySignature(paymentTokenAsBytes, signatureAsBytes))
{
Console.WriteLine("The signature is valid.");
verified = true;
}
else
{
Console.WriteLine("The signature is not valid.");
verified = false;
}
} catch(CryptographicException ex)
{
Debug.WriteLine($"What is going on!! {ex} ");
}
return verified;
Error:
Internal.Cryptography.CryptoThrowHelper+WindowsCryptographicException: Bad Key.
at Internal.NativeCrypto.CapiHelper.CreateHashHandle(SafeProvHandle hProv, Byte[] hash, Int32
calgHash)
at Internal.NativeCrypto.CapiHelper.VerifySign(SafeProvHandle hProv, SafeKeyHandle hKey, Int32
calgKey, Int32 calgHash, Byte[] hash, Byte[] signature)
at System.Security.Cryptography.RSACryptoServiceProvider.VerifyHash(Byte[] hash, Byte[] signature,
HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)
at System.Security.Cryptography.RSAPKCS1SignatureDeformatter.VerifySignature(Byte[] rgbHash, Byte[]
rgbSignature)
at Go.Server.Controllers.CitizenWebhook.VerifyData(String paymentToken, String signature) in
C:\Users\Dylan\source\repos\GO5050PLATFORM\Go\Server\Controllers\Webhooks\CitizenWebhook.cs:line 108
The posted code fails because VerifySignature() doesn't expect the raw data but the hashed data, see also this example from the documentation.
With the following change:
var paymentTokenAsBytes = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(paymentToken));
the verification is successful.
If the key is available in X.509/SPKI format (DER encoded), it can alternatively be imported with ImportSubjectPublicKeyInfo(). For verification, the methods VerifyData() (hashes the data implicitly) and VerifyHash() (expects the already hashed data) can be used alternatively.
I am using the following code to successfully produce a signature for my SAML. I have the POST sorted using XML sign, however REDIRECT is completely different. I am unable to produce the same signature as https://www.samltool.com/sign_logout_req.php and when I try and validate a genuine signature it is failing.
I took it back to basics and tried to see if I could sign it in the same manner and I cannot which suggests something is wrong with the way I am forming the data.
The following details (into samltool.com):
<saml:LogoutRequest ID="_02380F63816E0E92D6537758C37FE05F" Version="2.0" IssueInstant="2017-06-21T15:34:59.911Z" Destination="https://myteststs.net/appname/auth/" xmlns:saml="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://w.sp-app.com</saml:Issuer><saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">1869374313</saml:NameID></saml:LogoutRequest>
Private key (testcert)
MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAL13oqZ+pNa/fYIc+GYBrENOQ9rKWMeQEL9iDJyj7DFrQA40jOCY1UiOT2uLYvIwfqTHMuPmmlOLSyqCumZbKYP6KIM0pe+vJcJO6Nxg81gmN3jx3GbnDsmhi54oAmj3JC/Z/WbliqUXjlIAUlzLmMll7/vy2V5ec/gxHBpuRWBjAgMBAAECgYAWiWn/+vV5k9qGhRKo8479jIw2tLX9uu27Dhso8LiyIitC5U5Skutfz4mz5acV3t3ZlNZBVJdL07hTrKwma7aSx1r6UwTtW002ZZzytEVn7G7ytOIXkT+q/TuooCR8aa88vwhUFPqCSOuZgOPH9ytqAkzDCaNgVKdhQgRgjxfOBQJBAOSu4t5AgFJUBOcYOEOm6+v8R1CqedfyOgya3g1gsA4VnuG+ms233ZxWSPkiMnoUpEh8gBnZyk6ZZSlk668rwBcCQQDUGYg7wVLqhPAyjfM74oaJgohyQfQK6rPnzlKoGbdDR0QRN545ATBsETi2GSIYAHgkgLDJw3/lw1wX1dXzFuWVAkABmR9IwlajPKcUHl02S9JWQdsVuztCwRSaxfJLUaOpVYlYtoZKbcCEuS2lYBHOPJqxTv1uMNFzHytP0L686KddAkART6gr4GKJG6KTLbzNhXafoJTMZo+pmHBomhFrAPZROm7WzOhQFMXD/D/ZtQFwXhFwQUSsoxU8Ro6sr1pQBe1lAkBlXndo3Bm6AITDDsJZYg10XiBMNj4743t0pV6jayf9UTRZHu2GI9AWoU3/FTQt34zbPz6TjlNuJnwMHwfCFk1F
x.509
MIICMTCCAZqgAwIBAgIQcuFBQn5d27JBvbkCO+utKjANBgkqhkiG9w0BAQUFADBXMVUwUwYDVQQDHkwAewAyAEYAOAA3ADkANQA4ADUALQA3AEMANQA0AC0ANAA1ADAARAAtADgAOABGAEIALQBBADMARgA3ADEAMwA2ADQANgBFAEMANgB9MB4XDTE2MDEwODExMTU0OFoXDTE3MDEwNzE3MTU0OFowVzFVMFMGA1UEAx5MAHsAMgBGADgANwA5ADUAOAA1AC0ANwBDADUANAAtADQANQAwAEQALQA4ADgARgBCAC0AQQAzAEYANwAxADMANgA0ADYARQBDADYAfTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAvXeipn6k1r99ghz4ZgGsQ05D2spYx5AQv2IMnKPsMWtADjSM4JjVSI5Pa4ti8jB+pMcy4+aaU4tLKoK6Zlspg/oogzSl768lwk7o3GDzWCY3ePHcZucOyaGLnigCaPckL9n9ZuWKpReOUgBSXMuYyWXv+/LZXl5z+DEcGm5FYGMCAwEAATANBgkqhkiG9w0BAQUFAAOBgQBDXaccjXBrBhxp1fcEDm7MotKvgh8DxQAACk/Uxb4r2R6+LcePUxQTcxWVmyCQO0NR017FRf/fLFHmM9HZI3lwx5ka4xBnSOu8mejQ0KOYt4yf2VQG6pWGa046Ntip+KB/yDQKXQ3RHprsshe33MFlEWpDJyo6jyDpDUqLjPBvtg==
RelayState:
RELAYTEST
SigAlg:
#rsa-sha1
So... using the ssotool it produces the signature :
IG4VDmVwQRZWa75NmwjtqKlPVdCx6tm73gL7j3xvrqXsfirunUtr626SBmQJ4mke77bYzXg8D1hAy5EREOhz2QH23j47XexqbVSNTtAkZV7KP1/lO8K01tiQr8SGJqzdFor/FZZscIDFlw3cBLXhGSwWK9i0qO/e55qkgxJS9OA=
However.. using the code below (and many.. many.. variations) I cannot get it to produce that same signature. Please note the samlrequest is base64encoded with compression as recommended (but you can deflate it to the same output). I have followed this specification too (3.4.4.1):https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
static byte[] Sign(string data, string certSubject)
{
// Access Personal (MY) certificate store of current user
X509Store my = new X509Store(StoreName.My, StoreLocation.LocalMachine);
my.Open(OpenFlags.ReadOnly);
// Find the certificate we’ll use to sign
RSACryptoServiceProvider csp = null;
foreach (X509Certificate2 cert in my.Certificates)
{
if (cert.Subject.Contains(certSubject))
{
// Get its associated CSP and private key
csp = (RSACryptoServiceProvider)cert.PrivateKey;
}
}
if (csp == null)
{
throw new Exception("No valid cert was found");
}
string certAlgorithm = csp.SignatureAlgorithm;
// Hash the data
SHA1Managed sha1 = new SHA1Managed();
UnicodeEncoding encoding = new UnicodeEncoding();
byte[] dataRaw = encoding.GetBytes(data);
byte[] hash = sha1.ComputeHash(dataRaw);
// Sign the hash
return csp.SignHash(hash, CryptoConfig.MapNameToOID("SHA1"));
}
static bool Verify(string text, byte[] signature, string certPublic)
{
// Load the certificate we’ll use to verify the signature from a file
X509Certificate2 appSigningX509Certificate = null;
var appSigningCertificateBytes = Convert.FromBase64String(certPublic);
appSigningX509Certificate = new X509Certificate2(appSigningCertificateBytes);
// Get its associated CSP and public key
RSACryptoServiceProvider csp = (RSACryptoServiceProvider)appSigningX509Certificate.PublicKey.Key;
// Hash the data
SHA1Managed sha1 = new SHA1Managed();
UnicodeEncoding encoding = new UnicodeEncoding();
byte[] data = encoding.GetBytes(text);
byte[] hash = sha1.ComputeHash(data);
// Verify the signature with the hash
return csp.VerifyHash(hash, CryptoConfig.MapNameToOID("SHA1"), signature);
}
static void Main(string[] args)
{
// Usage sample
try
{
string cert = "MIICMTCCAZqgAwIBAgIQcuFBQn5d27JBvbkCO+utKjANBgkqhkiG9w0BAQUFADBXMVUwUwYDVQQDHkwAewAyAEYAOAA3ADkANQA4ADUALQA3AEMANQA0AC0ANAA1ADAARAAtADgAOABGAEIALQBBADMARgA3ADEAMwA2ADQANgBFAEMANgB9MB4XDTE2MDEwODExMTU0OFoXDTE3MDEwNzE3MTU0OFowVzFVMFMGA1UEAx5MAHsAMgBGADgANwA5ADUAOAA1AC0ANwBDADUANAAtADQANQAwAEQALQA4ADgARgBCAC0AQQAzAEYANwAxADMANgA0ADYARQBDADYAfTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAvXeipn6k1r99ghz4ZgGsQ05D2spYx5AQv2IMnKPsMWtADjSM4JjVSI5Pa4ti8jB+pMcy4+aaU4tLKoK6Zlspg/oogzSl768lwk7o3GDzWCY3ePHcZucOyaGLnigCaPckL9n9ZuWKpReOUgBSXMuYyWXv+/LZXl5z+DEcGm5FYGMCAwEAATANBgkqhkiG9w0BAQUFAAOBgQBDXaccjXBrBhxp1fcEDm7MotKvgh8DxQAACk/Uxb4r2R6+LcePUxQTcxWVmyCQO0NR017FRf/fLFHmM9HZI3lwx5ka4xBnSOu8mejQ0KOYt4yf2VQG6pWGa046Ntip+KB/yDQKXQ3RHprsshe33MFlEWpDJyo6jyDpDUqLjPBvtg==";
string samlRequestCompressed = "nZFPS8QwEMW/Ssl906TZ/gttQWwLhdWDyh68SKjBLbRJ7ExQv71traA38TiPee/9hilATaM82Rfr8U6/eg0YdHVJnlgkMtYmIuNJw5o8qpNYpGmcXYu0bVjckuCsZxisKUlEGQk6AK87A6gMLhLj6YElh4g/8FiKo4xzmnP+SIJ6aRiMws15QXQgwxC1Uab/MGrSdFBTP1r/TI3GUDm3iqE7KI+XkATv02hArtAl8bORVsEAct0Bib28v7o5yYVHutmi7e1IqmK7cMOb/2xXAHpeGUn1zfhGYaFwjvZ2KsIfoXvD7RLS1f9p4FmSi/QouNhzv6Kqffr1nOoT";
string relaystate = "RELAYTEST";
string algorithm = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
string data = String.Empty;
if (String.IsNullOrEmpty(relaystate))
{
data = String.Format("SAMLRequest={0}&SigAlg={1}", HttpUtility.UrlEncode(samlRequestCompressed), HttpUtility.UrlEncode(algorithm));
}
else
{
data = String.Format("SAMLRequest={0}&RelayState={1}&SigAlg={2}", HttpUtility.UrlEncode(samlRequestCompressed,Encoding.UTF8), HttpUtility.UrlEncode(relaystate,Encoding.UTF8), HttpUtility.UrlEncode(algorithm,Encoding.UTF8));
}
// Sign text
byte[] signature = Sign(data, "{2F879585-7C54-450D-88FB-A3F713646EC6}");
string b64encodedSig = Convert.ToBase64String(signature);
string expectedSig = "IG4VDmVwQRZWa75NmwjtqKlPVdCx6tm73gL7j3xvrqXsfirunUtr626SBmQJ4mke77bYzXg8D1hAy5EREOhz2QH23j47XexqbVSNTtAkZV7KP1/lO8K01tiQr8SGJqzdFor/FZZscIDFlw3cBLXhGSwWK9i0qO/e55qkgxJS9OA=";
if (b64encodedSig != expectedSig)
{
Console.WriteLine("Not what i expected");
Environment.Exit(0);
}
// Verify signature. Testcert.cer corresponds to “cn=my cert subject”
if (Verify(data, signature, cert))
{
Console.WriteLine("Signature verified");
}
else
{
Console.WriteLine("ERROR: Signature not valid!");
}
}
catch (Exception ex)
{
Console.WriteLine("EXCEPTION: " +ex.Message);
}
Console.ReadKey();
}
I cannot seem to understand how to produce the sign data in the same way. I have also confirmed the locally installed certificate is exactly the same as the one above.
I have resolved this now.
The solution was that the HttpUtility.URLEncode is not encoding it to the same standard as the SAML standards (or OneLogin). I figured it out by looking at the compressed data and that matched but the URL Encoded one different.
The answer was to use Uri.EscapeString.
In order to get the same signature as OneLogin, you have to use the same URL encoding as them. Other URL encodings will result in different signatures, but they are perfectly valid as well.
See the SAML specification (3.4.4.1):
Further, note that URL-encoding is not canonical; that is, there are multiple legal encodings for a given value. The relying party MUST therefore perform the verification step using the original URL-encoded values it received on the query string. It is not sufficient to re-encode the parameters after they have been processed by software because the resulting encoding may not match the signer's encoding.
As noted in another answer to this question, OneLogin seems to use URL encoding matching System.Net.WebUtility.UrlEncode() in .NET -- but note also that they do not URL encode the base64-encoded signature itself, even though it can contain characters such as [+/=]. The SAML specification is not very clear, but seems to suggest that this is wrong,
Note that some characters in the base64-encoded signature value may themselves require URL-encoding before being added.
This last bit is crucial to be able to validate your signature with their tools.
I am encrypting the message in .NET with RSACryptoServiceProvider with private key. (PKCS#1 v1.5)
When I try to decrypt in .NET with the following code that uses public key everything works fine:
private static string Decrypt(string key, string content)
{
byte[] rgb = Convert.FromBase64String(content);
var cryptoServiceProvider = new RSACryptoServiceProvider(new CspParameters()
{
ProviderType = 1
});
cryptoServiceProvider.ImportCspBlob(Convert.FromBase64String(key));
return Convert.ToBase64String(cryptoServiceProvider.Decrypt(rgb, false));
}
When on the other hand I try to find an algorithm to make the same decrypt method in Android, I am failing to decrypt it properly with public key. I exported the modulus and exponent from public key in .NET in order to load it properly on Android.
The method in Android is here:
public String Decrypt(String input) {
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
String modulusString = "mmGn1IXB+/NEm1ecLiUzgz7g2L6L5EE5DUcptppTNwZSqxeYKn0AuAccupL0iyX3LMPw6Dl9pjPXDjk93TQwYwyGgZaXOSRDQd/W2Y93g8erpGBRm/Olt7QN2GYhxP8Vn+cWUbNuikdD4yMfYX9NeD9UNt5WJGFf+jRkLk0zRK0A7ZIS+q0NvGJ/CgaRuoe3x4Mh1qYP9ZWNRw8rsDbZ6N2zyUa3Hk/WJkptRa6jrzc937r3QYF3eDTurVJZHwC7c3TJ474/8up3YNREnpK1p7hqwQ78fn35Tw4ZyTNxCevVJfYtc7pKHHiwfk36OxtOIesfKlMnHMs4vMWJm79ctixqAe3i9aFbbRj710dKAfZZ0FnwSnTpsoKO5g7N8mKY8nVpZej7tcLdTL44JqWEqnQkocRqgO/p3R8V/6To/OjQGf0r6ut9y/LnlM5qalnKJ1gFg1D7gCzZJ150TX4AO5kGSAFRyjkwGxnR0WLKf+BDZ8T/syOrFOrzg6b05OxiECwCvLWk0AaQiJkdu2uHbsFUj3J2BcwDYm/kZiD0Ri886xHqZMNExZshlIqiecqCskQhaMVC1+aCm+IFf16Qg/+eMYCd+3jm/deezT4rcMBOV/M+muownGYQ9WOdjEK53h9oVheahD3LqCW8MizABFimvXR3wAgkIUvhocVhSN0=";
String exponentString = "AQAB";
byte[] modulusBytes = Base64.decode(modulusString.getBytes("UTF-8"), Base64.DEFAULT);
byte[] dBytes = Base64.decode(exponentString.getBytes("UTF-8"), Base64.DEFAULT);
BigInteger modulus = new BigInteger(1, modulusBytes);
BigInteger d = new BigInteger(1, dBytes);
RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, d);
PublicKey key = keyFactory.generatePublic(keySpec);
//at one point I read somewhere that .net reverses the byte array so that it needs to be reversed for java, but who knows any more
/*byte[] inputArrayReversed = Base64.decode(input.getBytes("UTF-8"), Base64.DEFAULT);
for (int i = 0; i < inputArrayReversed.length / 2; i++) {
byte temp = inputArrayReversed[i];
inputArrayReversed[i] = inputArrayReversed[inputArrayReversed.length - 1];
inputArrayReversed[inputArrayReversed.length - 1] = temp;
}*/
byte[] decryptedText = null;
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, key);
decryptedText = cipher.doFinal(Base64.decode(input.getBytes("UTF-8"), Base64.DEFAULT));
return Base64.encodeToString(decryptedText, Base64.NO_WRAP);
//return new String(decryptedText, "UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
Actually I tried also with different algorithms specified in Cypher class, also tried many other combinations, tried using SpongyCastle instead of built in Android RSA providers, but nothing worked. If anybody has any clue to point me in right direction, I would be absolutely grateful.
First hint is that decrypted string from .NET comes as around 25 characters long, and when I get Android to return decrypted string without exceptions it is usually much longer, around 500 bytes.
Second hint deleted
Third hint I also tried spongycastle, but it didn't help that much
Anyways, thank you in advance for any help!!!
UPDATE 1
Second hint is deleted because was wrong, disregard it. Now I have one question if the following can prove that the public key is loaded correctly, just to rule that problem out.
BigInteger modulus and exponent in the upper Android code and the following BigIntegers in .NET show equal integer values.
var parameters = csp.ExportParameters(false);
var modulusInteger = new BigInteger(parameters.Modulus.Reverse().Concat(new byte[] { 0 }).ToArray());
var exponentInteger = new BigInteger(parameters.Exponent.Reverse().Concat(new byte[] { 0 }).ToArray());
UPDATE 2
This and This SO answers provide some interesting clues
Heeh, the mistake was one of the basics, we had an architecture where we were doing encryption with public key and decryption with private key. The problem was in the architecture itself because as we initially set it up, we were sending private keys to all our client apps, which is big security flaw.
My mistake was that I assumed that on the client we have public key and actually from private key all the time I was trying to load the public key and then do decrypt.
If I knew the PKI in depth and communicated a bit better with my colleague, I could have noticed few things:
Decrypt can be done with private key only, while one the other hand verify can be done with public key, so when I saw Decrypt being used on client in .NET, I should have assumed that on the client we have private key (which is a security flaw in the end in the way we want to use PKI)
Few things that I already knew or learnt and want to share with others:
Private key should be kept secret, whether you want to have it on server or preferably only on one client because public key can easily be guessed from private key and then someone can easily repeat your whole encryption process easily and breach your security
PKI works for two scenarios:
First scenario is when you want to Encrypt something and that only specific person/computer can Decrypt it. In first scenario as you see, many stakeholders can have someone's Public key and send messages to him and that only he can read them with his Private key. Second scenario is when you want to be sure that the message that came to you was not altered and was sent by specific person/computer. In that case you Sign data with Private key and Verify it on the other end with Public key. The only process that is suitable for us is Sign <-> Verify because we send plain text license with signature in it, and thus on the client we want to be sure that nobody tampered with the plain text license and that it came from us.
In your code, if Decrypt or Verify functions throw exceptions in 50% of the time it is because of loading the incorrect key or incorrectly loading the correct key and in the other 50% it is because you are using the incorrect algorithm or because algorithm parameters are incorrectly set or because the algorithm implementations between platforms are incompatible (the last one is very rare)
.NET server code
public string Sign(string privateKey, string data)
{
_rsaProvider.ImportCspBlob(Convert.FromBase64String(privateKey));
//// Write the message to a byte array using UTF8 as the encoding.
var encoder = new UTF8Encoding();
byte[] byteData = encoder.GetBytes(data);
//// Sign the data, using SHA512 as the hashing algorithm
byte[] encryptedBytes = _rsaProvider.SignData(byteData, new SHA1CryptoServiceProvider());
return Convert.ToBase64String(encryptedBytes);
}
.NET client code (Win Mobile)
private bool Verify(string key, string signature, string data)
{
CspParameters cspParams = new CspParameters { ProviderType = 1 };
RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider(cspParams);
rsaProvider.ImportCspBlob(Convert.FromBase64String(key));
byte[] signatureBytes = Convert.FromBase64String(signature);
var encoder = new UTF8Encoding();
byte[] dataBytes = encoder.GetBytes(data);
return rsaProvider.VerifyData(dataBytes, new SHA1CryptoServiceProvider(), signatureBytes);
}
Android client code:
public boolean Verify(RSAPublicKey key, String signature, String data)
{
try
{
Signature sign = Signature.getInstance("SHA1withRSA");
sign.initVerify(key);
sign.update(data.getBytes("UTF-8"));
return sign.verify(Base64.decode(signature.getBytes("UTF-8"), Base64.NO_WRAP));
}
catch (Exception e)
{
e.printStackTrace();
}
return false;
}
in .NET public key is exported in xml format with following code:
public string ExportPublicToXML(string publicKey)
{
RSACryptoServiceProvider csp = new RSACryptoServiceProvider(new CspParameters()
{
ProviderType = 1
});
csp.ImportCspBlob(Convert.FromBase64String(publicKey));
return csp.ToXmlString(false);
}
and then modulus and exponent are used in Android to load public key:
private RSAPublicKey GetPublicKey(String keyXmlString) throws InvalidKeySpecException, UnsupportedEncodingException, NoSuchAlgorithmException
{
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
String modulusString = keyXmlString.substring(keyXmlString.indexOf("<Modulus>"), keyXmlString.indexOf("</Modulus>")).replace("<Modulus>", "");
String exponentString = keyXmlString.substring(keyXmlString.indexOf("<Exponent>"), keyXmlString.indexOf("</Exponent>")).replace("<Exponent>", "");
byte[] modulusBytes = Base64.decode(modulusString.getBytes("UTF-8"), Base64.DEFAULT);
byte[] dBytes = Base64.decode(exponentString.getBytes("UTF-8"), Base64.DEFAULT);
BigInteger modulus = new BigInteger(1, modulusBytes);
BigInteger d = new BigInteger(1, dBytes);
RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus, d);
return (RSAPublicKey) keyFactory.generatePublic(keySpec);
}
I have a need to create a HMACSHA1 in C# to work with a Java web service. I believe that the Java implementation may be flawed because I've tested my result extensively using some online services. I don't know Java very well and I need to make my C# code produce the same has as the Java code, even if it is not technically correct.
Java Version:
public static byte[] computeSignature(String algorithm, byte[] data, byte[] sharedSecret) {
try {
SecretKey secretKey = new SecretKeySpec(Base64.decode(sharedSecret), algorithm);
Mac mac = Mac.getInstance(algorithm);
mac.init(secretKey);
mac.update(data);
return Base64.encode(mac.doFinal());
} catch (NoSuchAlgorithmException e) {
throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
} catch (InvalidKeyException e) {
throw new WebApplicationException(e, Response.Status.INTERNAL_SERVER_ERROR);
}
}
C# Version:
private string GetHash(string message, string key)
{
message = message.Replace("\r", "");
Encoding encoding = new UTF8Encoding();
var keyByte = encoding.GetBytes(key);
var hmacsha1 = new HMACSHA1(keyByte);
hmacsha1.Initialize();
byte[] messageBytes = encoding.GetBytes(message);
var hashmessage = hmacsha1.ComputeHash(messageBytes);
var sigB64 = Convert.ToBase64String(hashmessage);
return sigB64;
}
Using the following message and secret key:
Message: The quick brown fox jumps over the lazy dog
Secret: key
I get the following results:
C#: 3nybhbi3iqa8ino29wqQcBydtNk=
Java: K6f3B61fGHxBLeMQZYPDER1mjeg=
I'm fairly certain the Base64 encoding is where the difference is happening, but I'm not sure.
So, my question is -- How can I make my C# code produce the same output as the Java example?
P.S. The java code has the following import:
com.sun.jersey.core.util.Base64;
Edit:
Here is how the java function gets called:
String signature = new String(HmacUtils.computeSignature("HmacSHA1", canonicalRepresentation.getBytes(), sharedSecret.getBytes()));
String authorization = "HMAC " + publicKey + " " + signature;
clientRequest.getHeaders().put(HttpHeaders.AUTHORIZATION, Lists.<Object>newArrayList(authorization));
I'm having trouble with WS-Security, and creating a nonce and password digest that is correct.
I am successfully using SoapUI to send data to an Oracle system. So I'm able to intercept SoapUI's call (change proxy to 127.0.0.1 port 8888 to use Fiddler where it fails because it's over SSL) - intercepting is important because these values can only be used once. I can then grab the nonce, created timestamp and password digest put them into my code (I've only got 30 seconds to do this as the values don't last!) and I get a success.
So I know it's nothing else - just the Password Digest.
The values I use are the following:
Nonce: UIYifr1SPoNlrmmKGSVOug==
Created Timestamp: 2009-12-03T16:14:49Z
Password: test8
Required Password Digest: yf2yatQzoaNaC8BflCMatVch/B8=
I know the algorithm for creating the Digest is:
Password_Digest = Base64 ( SHA-1 ( nonce + created + password ) )
using the following code (from Rick Strahl's post)
protected string GetSHA1String(string phrase)
{
SHA1CryptoServiceProvider sha1Hasher = new SHA1CryptoServiceProvider();
byte[] hashedDataBytes = sha1Hasher.ComputeHash(Encoding.UTF8.GetBytes(phrase));
return Convert.ToBase64String(hashedDataBytes);
}
I get:
GetSHA1String("UIYifr1SPoNlrmmKGSVOug==" + "2009-12-03T16:14:49Z" + "test8") = "YoQKI3ERlMDGEXHlztIelsgL50M="
I have tried various SHA1 methods, all return the same results (which is a good thing I guess!):
SHA1 sha1 = SHA1.Create();
SHA1 sha1 = SHA1Managed.Create();
// Bouncy Castle:
protected string GetSHA1usingBouncyCastle(string phrase)
{
IDigest digest = new Sha1Digest();
byte[] resBuf = new byte[digest.GetDigestSize()];
byte[] bytes = Encoding.UTF8.GetBytes(phrase);
digest.BlockUpdate(bytes, 0, bytes.Length);
digest.DoFinal(resBuf, 0);
return Convert.ToBase64String(resBuf);
}
Any ideas on how to get the correct hash?
The problem was the nonce.
I was trying to use a nonce that had already been Base64 encoded. If you want to use a Nonce that is in the form "UIYifr1SPoNlrmmKGSVOug==" then you need to decode it.
Convert.FromBase64String("UIYifr1SPoNlrmmKGSVOug==")
which is a byte array.
So we need a new method:
public string CreatePasswordDigest(byte[] nonce, string createdTime, string password)
{
// combine three byte arrays into one
byte[] time = Encoding.UTF8.GetBytes(createdTime);
byte[] pwd = Encoding.UTF8.GetBytes(password);
byte[] operand = new byte[nonce.Length + time.Length + pwd.Length];
Array.Copy(nonce, operand, nonce.Length);
Array.Copy(time, 0, operand, nonce.Length, time.Length);
Array.Copy(pwd, 0, operand, nonce.Length + time.Length, pwd.Length);
// create the hash
var sha1Hasher = new SHA1CryptoServiceProvider();
byte[] hashedDataBytes = sha1Hasher.ComputeHash(operand);
return Convert.ToBase64String(hashedDataBytes);
}
CreatePasswordDigest(Convert.FromBase64String("UIYifr1SPoNlrmmKGSVOug=="), "2009-12-03T16:14:49Z", "test8")
which returns yf2yatQzoaNaC8BflCMatVch/B8= as we want.
Remember to use the same createdTime in the digest as you put in the XML, this might sound obvious, but some people include milliseconds on their timestamps and some don't - it doesn't matter, it just needs to be consistent.
Also the Id field in the UsernameToken XML doesn't matter - it doesn't need to change.
Here's a method to create a Nonce like the one above, if you don't want to use GUIDs like Rick uses:
private byte[] CreateNonce()
{
var Rand = new RNGCryptoServiceProvider();
//make random octets
byte[] buf = new byte[0x10];
Rand.GetBytes(buf);
return buf;
}
I hope that helps someone - it took me lots of frustration, trial and error, searching web pages, and general head/wall banging.