Authenticating Mandrill Inbound Webhook Requests in .NET - c#

I'm using Mandrill Inbound Webhooks to call a method in my WCF API. The request is coming through, I can successfully parse it, etc.
My problem lies in getting the value of the X-Mandrill-Signature header to match the signature that I'm generating (based on the process detailed here: https://mandrill.zendesk.com/hc/en-us/articles/205583257-Authenticating-webhook-requests).
This is what I'm currently doing:
List<string> keys = HttpContext.Current.Request.Params.AllKeys.ToList();
keys.Sort();
string url = "MyMandrillWebhookURL";
string MandrillKey = "MyMandrillWebhookKey"
foreach (var key in keys)
{
url += key;
url += HttpContext.Current.Request.Params[key];
}
byte[] byteKey = System.Text.Encoding.ASCII.GetBytes(MandrillKey);
byte[] byteValue = System.Text.Encoding.ASCII.GetBytes(url);
HMACSHA1 myhmacsha1 = new HMACSHA1(byteKey);
byte[] hashValue = myhmacsha1.ComputeHash(byteValue);
string generatedSignature = Convert.ToBase64String(hashValue);
And generatedSignature does not match the value for X-Mandrill-Signature
I know that the Mandrill docs indicate that the encoding needs to be done in binary and not hexadecimal (and I think my code does that, but correct me if I'm wrong), but, beyond that I can't make heads or tails of what my issue is. Any help is greatly appreciated.

The problem is with how you're retrieving the keys in your validation. You only need to use the request's POST variables alphabetically by key, not all the Request parameters. There is only one POST variable, mandrill_events that needs to be used in the signature generation.
string url = "MyMandrillWebhookURL";
string MandrillKey = "MyMandrillWebhookKey"
url += "mandrill_events";
url += mandrillEvents;
byte[] byteKey = System.Text.Encoding.ASCII.GetBytes(MandrillKey);
byte[] byteValue = System.Text.Encoding.ASCII.GetBytes(url);
...

Related

Convert Node JS to ASP.NET C# for HMAC_SHA256 Hash

I'm trying to convert this Node JS code to ASP.NET C#:
const crypto = require('crypto');
/**
* Validates a billing service webhook
*
* #param {string} req Node request object, where 'req.body' is a Node
* Buffer object containing the request body
* #param {string} secret the secret string saved in the service App
*/
const isValidSignature = (req, secret) => {
const fsSignature = req.headers['X-FS-Signature'];
const computedSignature = crypto.createHmac('sha256', secret)
.update(req.body)
.digest()
.toString('base64');
return fsSignature === computedSignature;
}
Here is my attempt in C#
private bool CheckNotificationValidContextual(string varRequestHashValue, string varMessageBody) // #2047
{
// This involves a test of the webhook functionality using ngroks and Postman
// to send values that were previously generated from a test webhook from billing service
// to a local development copy of the seller application (running in Visual Studio)
// where the inputs are:
// varRequestHashValue = Request.Headers["X-Fs-Signature"];
// varMessageBody = new System.IO.StreamReader(Request.InputStream).ReadToEnd();
// get the local copy of the webhook secret key from the local web config file
var AMPTK_FSP_HMAC_SHA256_Key = ConfigVal.AMPTK_FSP_HMAC_SHA256();
// convert the local copy of the secret key to a byte array
byte[] AMPTK_keyBytes = Encoding.UTF8.GetBytes(AMPTK_FSP_HMAC_SHA256_Key);
// create a hash object with the local copy of the secret key
var _hashObjectOfLocalKey = new HMACSHA256(AMPTK_keyBytes);
// convert the input webhook message body to a byte array
byte[] _messageBodyByteArray = Encoding.UTF8.GetBytes(varMessageBody);
// create a hash byte array of the message body byte array
// using the hash object based on the local copy of the webhook secret
byte[] _computedMessageHashBytes = _hashObjectOfLocalKey.ComputeHash(_messageBodyByteArray);
// convert the hash byte array of the message body to a string
string _stringOfComputedMessageHashBytes = Encoding.UTF8.GetString(_computedMessageHashBytes, 0, _computedMessageHashBytes.Length);
// remove dashes and convert to lowercase
_stringOfComputedMessageHashBytes= BitConverter.ToString(_computedMessageHashBytes).Replace("-", "").ToLower();
// compare the string of the computed message body hash
// to the received webhook secret hash value from Request.Headers["X-Fs-Signature"]
if (_stringOfComputedMessageHashBytes == varRequestHashValue)
{
return true;
}
else
{
return false;
}
}
The C# code compiles and runs ok.
The result I'm looking for is:
prNdADI26M0ov5x6ZlMr2J2zzB8z2TJRBDy+8gjPttk=
What I'm getting from the C# code "_stringOfComputedMessageHashBytes"
is this:
f37cdae653e167e36c8ed17e44ffa456832dbb7dcec1d00dc1b44a1234965e73
I've checked the inputs carefully (could still be wrong).
Question: have I translated the Node code properly to C#, and if not, how can I improve it? Or what else might be off?
Thanks!
I checked your C# code for computing hash, in general looks ok, you used correct library and correct class HMACSHA256 to calculate HMAC_SHA256 hash. The problem seems on the part that you were trying to convert the computed hash value to a base 64 string. You can use Convert.ToBase64String method to convert your byte array to base 64 string.
Here below you can find your updated C# code(I did minor improvements and updated the code to find _stringOfComputedMessageHashBytes).
I tested the hash results generated from node.js and C#, now they are both generating the same hash result:
private bool CheckNotificationValidContextual(string varRequestHashValue, string varMessageBody) // #2047
{
// This involves a test of the webhook functionality using ngroks and Postman
// to send values that were previously generated from a test webhook from billing service
// to a local development copy of the seller application (running in Visual Studio)
// where the inputs are:
// varRequestHashValue = Request.Headers["X-Fs-Signature"];
// varMessageBody = new System.IO.StreamReader(Request.InputStream).ReadToEnd();
// get the local copy of the webhook secret key from the local web config file
var AMPTK_FSP_HMAC_SHA256_Key = ConfigVal.AMPTK_FSP_HMAC_SHA256();
// convert the local copy of the secret key to a byte array
var AMPTK_keyBytes = Encoding.UTF8.GetBytes(AMPTK_FSP_HMAC_SHA256_Key);
// create a hash object with the local copy of the secret key
using (var _hashObjectOfLocalKey = new HMACSHA256(AMPTK_keyBytes))
{
// convert the input webhook message body to a byte array
var _messageBodyByteArray = Encoding.UTF8.GetBytes(varMessageBody);
// create a hash byte array of the message body byte array
// using the hash object based on the local copy of the webhook secret
var _computedMessageHashBytes = _hashObjectOfLocalKey.ComputeHash(_messageBodyByteArray);
var _stringOfComputedMessageHashBytes = Convert.ToBase64String(_computedMessageHashBytes);
// compare the string of the computed message body hash
// to the received webhook secret hash value from Request.Headers["X-Fs-Signature"]
return _stringOfComputedMessageHashBytes == varRequestHashValue;
}
}

SAML Redirect sign or verify failing to produce correct signature

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.

Encrypt Json result on WebApi Backend

I finished my web api with data encriptions, but now, i have to encripty the Json result. When i put the querystring on the browser, now i have the answer (example) :
[{"SUSPID":"111","IVNOME":"teste","IVMAE":"teste","IVPAI":"teste","IVDATANASC":"02/07/1970","IVRG":"0000 (IFP)","ICPF":"Não Cadastrado"}]
I cannot show this...i have to show like (ENCRYPTED): [{"SUSPID":"AUAUAUA","IVNOME":"UAUAU","IVMAE":"UAUAU", ......]
I am seeing some examples, but i am not finding one that is what i need
Part of the code on my service (Cliente-side):
var response = await client.GetAsync(urllink);
var JsonResult = response.Content.ReadAsStringAsync().Result;
if (typeof(T) == typeof(string))
return null;
var rootobject = JsonConvert.DeserializeObject<T>(JsonResult);
return rootobject;
And at my controller (web api BackEnd), i return this dataset:
return lretorno.Tables[0].AsEnumerable().Select(row => new Envolvido
{
SUSPID = Convert.ToString(row["SUSPID"]),
IVNOME = Convert.ToString(row["SUSPNOME"]),
IVMAE = Convert.ToString(row["SUSPMAE"]),
IVPAI = Convert.ToString(row["SUSPPAI"]),
IVDATANASC = Convert.ToString(row["SUSPDATANASC"]).Replace(" 00:00:00", ""),
IVRG = Convert.ToString(row["RG"]),
ICPF = Convert.ToString(row["CPF"]),
MANDADO = Convert.ToInt16(row["TEMMANDADO"]),
OCORRENCIA = Convert.ToInt16(row["TEMOCORRENCIA"]),
});
I cannot understand where i have to encripty and where i have to decrypt on the code.
If you really must do some additional encryption on top of https, for instance if you want to help stop automated man-in-the-middle you can do something like the following... this would require the people in between (Government, ISP, Telco, Endpoint network admin's) to do a second man in the middle attack by figuring out specifically how you are passing your extra public key. In addition to this you could also include the "pk" parameter inside your JSON before its encrypted... and then when you decrypt the json you can compare it against the public-key that you sent, if they don't match then for sure there was a man-in-the-middle. I used the built in RSACryptoServiceProvider.
CLIENT-SIDE
// Generate private and public keys (use any asymmetric crypto/key size you want)
RSACryptoServiceProvider rsaKeys = new RSACryptoServiceProvider();
var privateXmlKeys = rsaKeys.ToXmlString(true);
var publicXmlKeys = rsaKeys.ToXmlString(false);
// Make the request for the json data from the server, and also pass along the public xml keys encoded as base64
var response = await http.GetAsync(new Uri(String.Format("https://example.com/data?id=777&pk=\"{0}\"", Convert.ToBase64String(Encoding.ASCII.GetBytes(publicXmlKeys)))));
var encryptedJsonBytes = await response.Content.ReadAsByteArrayAsync();
// Decrypt the bytes using the private key generated earlier
RSACryptoServiceProvider rsaDecrypt = new RSACryptoServiceProvider();
rsaDecrypt.FromXmlString(privateXmlKeys);
byte[] decryptedBytes = rsaDecrypt.Decrypt(encryptedJsonBytes, false);
// Now change from bytes to string
string jsonString = Encoding.ASCII.GetString(decryptedBytes);
// TODO: For extra validation, parse json, get the public key out that the server
// had used to encrypt, and compare with the "pk" you sent "publicXmlKeys",
// if these values do not match there was an attack.
SERVER-SIDE
// Assuming you have your JSON string already
string json = "{\"key\":\"secret_value\"}";
// Get the "pk" request parameter from the http request however you need to
string base64PublicKey = request.getParameter("pk");
string publicXmlKey = Encoding.ASCII.GetString(Convert.FromBase64String(base64PublicKey));
// TODO: If you want the extra validation, insert "publicXmlKey" into the json value before
// converting it to bytes
// var jo = parse(json); jo.pk = publicXmlKey; json = jo.ToString();
// Convert the string to bytes
byte[] jsonBytes = Encoding.ASCII.GetBytes(json);
// Encrypt the json using the public key provided by the client
RSACryptoServiceProvider rsaEncrypt = new RSACryptoServiceProvider();
rsaEncrypt.FromXmlString(publicXmlKey);
byte[] encryptedJsonBytes = rsaEncrypt.Encrypt(jsonBytes, false);
// Send the encrypted json back to the client
return encryptedJsonBytes;
If you need to protect against man in the middle attacks then I suggest you turn off the computer or pre-share the key via a different method not over the internet, phone, or mail and then do not embed it into your application :P Take a look into Off-the-record, End-to-end-encryption and Diffie–Hellman key exchange
Basically I have some generic ideas about this sort of cases for you. If you knows about the Man in the middle attack, only in E2E connection you can rely on your encryption algorithm, That's because only these End-points have the private and public key and the attacker can not spoof, But in these cases(like your case) the attacker can simply have your public key and even your encrypted block which you're trying to send to your webservice, That's because all you have in client-side is in the javascript resources that everyones can read.
So the only solution I can give you is that take your webservices on the HTTPS protocols which normally handle these kind of issues and you don't need to any encryption.
Regards.

Working algorithm for PasswordDigest in WS-Security

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.

How to read string from HttpRequest form data in correct encoding

Today I have done a service to receive emails from SendGrid and finally have sent an email with a text "At long last", first time in non-English language during testing. Unfortunately, the encoding has become a problem that I cannot fix.
In a ServiceStack service I have a string property (in an input object that is posted to the service from SendGrid) in an encoding that is different from UTF8 or Unicode (KOI8-R in my case).
public class SengGridEmail : IReturn<SengGridEmailResponse>
{
public string Text { get; set; }
}
When I try to convert this string to UTF8 I get ????s, probably because when I access the Text property it is already converted into Unicode (.NET's internal string representation). This question and answer illustrate the issue.
My question is how to get original KOI8-R bytes within ServiceStack service or ASP.NEt MVC controller, so that I could convert it to UTF8 text?
Update:
Accessing base.Request.FormData["text"] doesn't help
var originalEncoding = Encoding.GetEncoding("KOI8-R");
var originalBytes = originalEncoding.GetBytes(base.Request.FormData["text"]);
But if I take base64 string from the original sent mail and convert it to byte[], and then convert those bytes to UTF8 string - it works. Either base.Request.FormData["text"] is already in Unicode .NET string format, or (less likely) it is something on SendGrid side.
Update 2:
Here is a unit test that shows what is happening:
[Test]
public void EncodingTest()
{
const string originalString = "наконец-то\r\n";
const string base64Koi = "zsHLz87Fwy3Uzw0K";
const string charset = "KOI8-R";
var originalBytes = base64Koi.FromBase64String(); // KOI bytes
var originalEncoding = Encoding.GetEncoding(charset); // KOI Encoding
var originalText = originalEncoding.GetString(originalBytes); // this is initial string correctly converted to .NET representation
Assert.AreEqual(originalString, originalText);
var unicodeEncoding = Encoding.UTF8;
var originalWrongString = unicodeEncoding.GetString(originalBytes); // this is how the KOI string is represented in .NET, equals to base.Request.FormData["text"]
var originalWrongBytes = originalEncoding.GetBytes(originalWrongString);
var unicodeBytes = Encoding.Convert(originalEncoding, unicodeEncoding, originalBytes);
var result = unicodeEncoding.GetString(unicodeBytes);
var unicodeWrongBytes = Encoding.Convert(originalEncoding, unicodeEncoding, originalWrongBytes);
var wrongResult = unicodeEncoding.GetString(unicodeWrongBytes); // this is what I see in DB
Assert.AreEqual(originalString, result);
Assert.AreEqual(originalString, wrongResult); // I want this to pass!
}
Discovered two underlying problems for my problem.
The first is from SendGrid - they post multi-part data without specifying content-type for non-unicode elements.
The second is from ServiceStack - currently it doesn't support encoding other than utf-8 for multi-part data.
Update:
SendGrid helpdesk promised to look into the issue, ServiceStack now fully support custom charsets in multi-part data.
As for initial question itself, one could access buffered stream in ServiceStack as described here: Can ServiceStack Runner Get Request Body?.

Categories