Receiving AS2 messages with .NET

By request, this is a follow up to my “Send an AS2 message with .NET” from July 2010. This time we will be receiving AS2 (Applicability Statement 2) messages.

Start with an “ASP.NET Web Application” and then add a “Generic Handler” to it, call it AS2Listener.ashx.

Add the following code to the ProcessRequest method:


public void ProcessRequest(HttpContext context)
{
    string sTo = context.Request.Headers["AS2-To"];
    string sFrom = context.Request.Headers["AS2-From"];
    string sMessageID = context.Request.Headers["Message-ID"];

    if (context.Request.HttpMethod == "POST" || context.Request.HttpMethod == "PUT" ||
       (context.Request.HttpMethod == "GET" && context.Request.QueryString.Count > 0))
    {

        if (sFrom == null || sTo == null)
        {
            //Invalid AS2 Request.
            //Section 6.2 The AS2-To and AS2-From header fields MUST be present
            //    in all AS2 messages
            if (!(context.Request.HttpMethod == "GET" && context.Request.QueryString[0].Length == 0))
            {
                AS2Receive.BadRequest(context.Response, "Invalid or unauthorized AS2 request received.");
            }
        }
        else
        {
            AS2Receive.Process(context.Request, WebConfigurationManager.AppSettings["DropLocation"]);
        }
    }
    else
    {
        AS2Receive.GetMessage(context.Response);
    }
}

Now you’ll need to create your AS2Receive class. The simple methods are AS2Receive.BadRequest and AS2Receive.GetMessage:


public static void GetMessage(HttpResponse response)
{
    response.StatusCode = 200;
    response.StatusDescription = "Okay";

    response.Write(@"<!DOCTYPE HTML PUBLIC ""-//W3C//DTD HTML 3.2 Final//EN"">"
    + @"<HTML><HEAD><TITLE>Generic AS2 Receiver</TITLE></HEAD>"
    + @"<BODY><H1>200 Okay</H1><HR>This is to inform you that the AS2 interface is working and is "
    + @"accessable from your location.  This is the standard response to all who would send a GET "
    + @"request to this page instead of the POST context.Request defined by the AS2 Draft Specifications.<HR></BODY></HTML>");
}

public static void BadRequest(HttpResponse response, string message)
{
    response.StatusCode = (int)HttpStatusCode.BadRequest;
    response.StatusDescription = "Bad context.Request";

    response.Write(@"<!DOCTYPE HTML PUBLIC ""-//W3C//DTD HTML 3.2 Final//EN"">"
    + @"<HTML><HEAD><TITLE>400 Bad context.Request</TITLE></HEAD>"
    + @"<BODY><H1>400 Bad context.Request</H1><HR>There was a error processing this context.Request.  The reason given by the server was:"
    + @"<P><font size=-1>" + message + @"</Font><HR></BODY></HTML>");
}

The one that does all the work is AS2Receive.Process. In this simple example all it will do is read the message and write it to a new text file in the given dropLocation folder.

public static void Process(HttpRequest request, string dropLocation)
{
    string filename = ParseFilename(request.Headers["Subject"]);

    byte[] data = request.BinaryRead(request.TotalBytes);
    bool isEncrypted = request.ContentType.Contains("application/pkcs7-mime");
    bool isSigned = request.ContentType.Contains("application/pkcs7-signature");

    string message = string.Empty;

    if (isSigned)
    {
        string messageWithMIMEHeaders = System.Text.ASCIIEncoding.ASCII.GetString(data);
        string contentType = request.Headers["Content-Type"];

        message = AS2MIMEUtilities.ExtractPayload(messageWithMIMEHeaders, contentType);
    }
    else if (isEncrypted) // encrypted and signed inside
    {
        byte[] decryptedData = AS2Encryption.Decrypt(data);

        string messageWithContentTypeLineAndMIMEHeaders = System.Text.ASCIIEncoding.ASCII.GetString(decryptedData);

        // when encrypted, the Content-Type line is actually stored in the start of the message
        int firstBlankLineInMessage = messageWithContentTypeLineAndMIMEHeaders.IndexOf(Environment.NewLine + Environment.NewLine);
        string contentType = messageWithContentTypeLineAndMIMEHeaders.Substring(0, firstBlankLineInMessage);

        message = AS2MIMEUtilities.ExtractPayload(messageWithContentTypeLineAndMIMEHeaders, contentType);
    }
    else // not signed and not encrypted
    {
        message = System.Text.ASCIIEncoding.ASCII.GetString(data);
    }

    System.IO.File.WriteAllText(dropLocation + filename, message);
}

We have some logic at the start to figure out if the message is signed, or if it’s encrypted and signed. Note that this code always assumes that if a message is encrypted it is also signed, and doesn’t allow for the message to be encrypted but not signed – although that is a valid AS2 scenario.

Let’s deal with receiving signed messages first, in AS2MIMEUtilities:

public class AS2MIMEUtilities
{
    public const string MESSAGE_SEPARATOR = "\r\n\r\n";

    /// <summary>
    /// Extracts the payload from a signed message, by looking for boundaries
    /// Ignores signatures and does checking - should really validate the signature
    /// </summary>
    public static string ExtractPayload(string message, string contentType)
    {
        string boundary = GetBoundaryFromContentType(contentType);

        if (!boundary.StartsWith("--"))
            boundary = "--" + boundary;

        int firstBoundary = message.IndexOf(boundary);
        int blankLineAfterBoundary = message.IndexOf(MESSAGE_SEPARATOR, firstBoundary) + (MESSAGE_SEPARATOR).Length;
        int nextBoundary = message.IndexOf(MESSAGE_SEPARATOR + boundary, blankLineAfterBoundary);
        int payloadLength = nextBoundary - blankLineAfterBoundary;

        return message.Substring(blankLineAfterBoundary, payloadLength);
}

/// <summary>
/// Extracts the boundary from a Content-Type string
/// </summary>
/// <param name="contentType">e.g: multipart/signed; protocol="application/pkcs7-signature"; micalg="sha1"; boundary="_956100ef6a82431fb98f65ee70c00cb9_"</param>
/// <returns>e.g: _956100ef6a82431fb98f65ee70c00cb9_</returns>
public static string GetBoundaryFromContentType(string contentType)
{
    return Trim(contentType, "boundary=\"", "\"");
}

/// <summary>
/// Trims the string from the end of startString until endString
/// </summary>
private static string Trim(string str, string start, string end)
{
    int startIndex = str.IndexOf(start) + start.Length;
    int endIndex = str.IndexOf(end, startIndex);
    int length = endIndex - startIndex;

    return str.Substring(startIndex, length);
}

Oh, OK, erm the <summary> of ExtractPayload says it all. All I do is truncate the message signature off the message, by looking for the message part boundaries. It should of course really check that the message signature is valid.

Now let’s handle encrypted messages with AS2Encryption.Decrypt:

internal static byte[] Decrypt(byte[] encodedEncryptedMessage)
{
    EnvelopedCms envelopedCms = new EnvelopedCms();
    envelopedCms.Decode(encodedEncryptedMessage);
    envelopedCms.Decrypt();
    return envelopedCms.Encode();
}

And that, Dear reader is a simple example of how to receive either: unsigned & unencrypted;  signed; or encrypted & signed AS2 messages. What is missing is the necessary checking that the signature is valid, and handling of encrypted but unsigned messages.

Advertisement

19 thoughts on “Receiving AS2 messages with .NET

  1. Hi Matt,

    Great posts about how to implement AS2!

    I have like Anitha a problem – the signing is not removed from the received file. At the end of the file I end up with something like:

    –_8D3A96B8-A5F2-4736-8D07-529B0C3568C7_
    Content-type: application/pkcs7-signature; name=”smime.p7s”
    Content-Transfer-Encoding: base64

    Is there a “standard” way of removing this part, so I end up with the plain message?
    I thought the ExtractPayload method will do the job, but it only removes the Content-type and boundary in the beginning of the file.

    Thank!

    /Mikkel

    • I ended up doing this:


      message = message.Substring(blankLineAfterBoundary, payloadLength);

      int certificateBoundaryIndex = message.IndexOf(boundary);
      if (certificateBoundaryIndex > 0)
      message = message.Substring(0, certificateBoundaryIndex);

      return message;

      Can you see any problems with this?

      Thanks!

      /Mikkel

      • Hi Mikkel,
        i have been working on same kind of project that you did. i have byte[] with mime Header and Signature on it. how i can remove Header and Signature from it. please help me and provide me with some code.
        thanks,
        farooq

  2. Hi Jiri,
    did you get a chance to remove the Signature part from this. as anita mentioned in her port, i am having same problem.

  3. Hi I got any errors:

    Fehler 5 Der Typ- oder Namespacename “EnvelopedCms” konnte nicht gefunden werden. (Fehlt eine Using-Direktive oder ein Assemblyverweis?) D:\VB\net4\Installer\WebApplication3\WebApplication3\AS2Encryption.Decrypt.cs 12 45 WebApplication3

    Fehler 2 Der Name “HttpStatusCode” ist im aktuellen Kontext nicht vorhanden. D:\VB\net4\Installer\WebApplication3\WebApplication3\AS2Receive.cs 24 40 WebApplication3

    Fehler 2 Der Name “ParseFilename” ist im aktuellen Kontext nicht vorhanden. D:\VB\net4\Installer\WebApplication3\WebApplication3\AS2Receive.cs 35 31 WebApplication3

    can you send the project to my eMail-Adress?

  4. First off Matt – thank you. This has been really helpful.

    Ralf – regarding your errors – Go to Project References -> Add Reference -> System.Web

    Then add the following references to AS2Receive:

    using System.Web;
    using System.Web.Configuration;
    using System.Net;

    ——

    The code snippets do not contain a definition of ParseFileName – I just removed that and use:

    string filename = request.Headers[“Subject”];

    It compiled fine – and the filename is used only to save the deciphered output in the last line of the Process() function.

    ——

    Matt if you could post your code for generating the MDN that will really be helpful.

    Also, in your AS2Send class – I was not able to retrieve the MDN.

    I got an HTTPStatusCode = OK, and the message appears to have succeeded – but the Response stream had a 0 byte payload.

    • var incoming_date = request.Headers[“Date”];
      var dateTimeSent = “Unknown date”;
      if (incoming_date != null) dateTimeSent = incoming_date;

      var responseContent1 = “The Message from ‘” + request.Headers[“AS2-From”] + “‘ to ‘” +
      request.Headers[“AS2-To”] + “‘ ” + Environment.NewLine +
      “with MessageID ‘” + request.Headers[“Message-Id”] + “‘” + Environment.NewLine + “sent ” + dateTimeSent +
      ” has been accepted for processing. ” + Environment.NewLine +
      “This does not guarantee that the message has been read or understood.” + Environment.NewLine;

      var responseContent2 = “Reporting-UA: AS2 Adapter” + Environment.NewLine +
      “Final-Recipient: rfc822;” + request.Headers[“AS2-From”] + Environment.NewLine +
      “Original-Message-ID: ” + request.Headers[“Message-ID”] + Environment.NewLine +
      “Disposition: automatic-action/MDN-Sent-automatically; processed”;

      var finalBodyContent = Encoding.ASCII.GetBytes(responseContent1);
      var finalBodyContent2 = Encoding.ASCII.GetBytes(responseContent2);

      //Wrap the file data with a mime header
      finalBodyContent2 = Utilities.CreateMessage(“message/disposition-notification”, “7bit”, “”, finalBodyContent2);

      var PublicAndPrivateKeyPath = “some path”;
      var SigningPassword = “config entry”
      string contentType;
      finalBodyContent2 = Utilities.Sign(finalBodyContent2, PublicAndPrivateKeyPath, SigningPassword, out contentType);
      response.Headers.Add(“EDIINT-Features”, “AS2-Reliability”);

      byte[] signedContentTypeHeader = System.Text.Encoding.ASCII.GetBytes(“Content-Type: ” + “text/plain” + Environment.NewLine);
      byte[] contentWithContentTypeHeaderAdded = Utilities.ConcatBytes(signedContentTypeHeader, finalBodyContent2);

      finalBodyContent2 = Encryption.Encrypt(contentWithContentTypeHeaderAdded, clientCertificatePath,
      EncryptionAlgorithm.DES3);

      byte[] finalResponse = finalBodyContent.Concat(finalBodyContent2).ToArray();

      response.BinaryWrite(finalResponse);

  5. response.Clear();
    response.Headers.Add(“Date”, DateTime.UtcNow.ToString(“ddd, dd MMM yyy HH:mm:ss”) + ” GMT”);

    var incoming_subject = request.Headers[“Subject”];
    var response_subject = “Re:”;
    if (incoming_subject != null) response_subject = response_subject + incoming_subject;
    response.Headers.Add(“Subject”, response_subject);

    response.Headers.Add(“Mime-Version”, “1.0”);
    response.Headers.Add(“AS2-Version”, “1.2”);
    response.Headers.Add(“From”, “XXXXXXXXXX”);
    response.Headers.Add(“AS2-To”, request.Headers[“AS2-From”]);
    response.Headers.Add(“AS2-From”, request.Headers[“AS2-To”]);
    response.Headers.Add(“Connection”, “Close”);
    response.ContentType = “multipart/report; report-type=disposition-notification;”;
    response.StatusCode = 202; // Accepted
    response.StatusDescription = “Accepted”;

    var incoming_date = request.Headers[“Date”];
    var dateTimeSent = “Unknown date”;
    if (incoming_date != null) dateTimeSent = incoming_date;

    var responseContent1 = “The Message from ‘” + request.Headers[“AS2-From”] + “‘ to ‘” +
    request.Headers[“AS2-To”] + “‘ ” + Environment.NewLine +
    “with MessageID ‘” + request.Headers[“Message-Id”] + “‘” + Environment.NewLine + “sent ” + dateTimeSent +
    ” has been accepted for processing. ” + Environment.NewLine +
    “This does not guarantee that the message has been read or understood.” + Environment.NewLine;

    var responseContent2 = “Reporting-UA: AS2 Adapter” + Environment.NewLine +
    “Final-Recipient: rfc822;” + request.Headers[“AS2-From”] + Environment.NewLine +
    “Original-Message-ID: ” + request.Headers[“Message-ID”] + Environment.NewLine +
    “Disposition: automatic-action/MDN-Sent-automatically; processed”;

    var finalBodyContent = Encoding.ASCII.GetBytes(responseContent1);
    var finalBodyContent2 = Encoding.ASCII.GetBytes(responseContent2);

    //Wrap the file data with a mime header
    finalBodyContent2 = Utilities.CreateMessage(“message/disposition-notification”, “7bit”, “”, finalBodyContent2);

    var PublicAndPrivateKeyPath = “some path”
    var SigningPassword = “take it from app config”;
    string contentType;
    finalBodyContent2 = Utilities.Sign(finalBodyContent2, PublicAndPrivateKeyPath, SigningPassword, out contentType);
    response.Headers.Add(“EDIINT-Features”, “AS2-Reliability”);

    byte[] signedContentTypeHeader = System.Text.Encoding.ASCII.GetBytes(“Content-Type: ” + “text/plain” + Environment.NewLine);
    byte[] contentWithContentTypeHeaderAdded = Utilities.ConcatBytes(signedContentTypeHeader, finalBodyContent2);

    finalBodyContent2 = Encryption.Encrypt(contentWithContentTypeHeaderAdded, clientCertificatePath,
    EncryptionAlgorithm.DES3);

    byte[] finalResponse = finalBodyContent.Concat(finalBodyContent2).ToArray();

    response.BinaryWrite(finalResponse);

  6. Anyone have code to generate the Received-Content-MIC? I’ve tried many combinations of received message content but none produce the correct result.

    • If you are using Mimekit (http://www.mimekit.net/) which I will recommend, you can do it in different ways.

      The simple way:

      MimeEntity baseMime; // Your mimepart holding the actual payload of your AS2 message

      using (var stream = new MemoryStream())
      {
      baseMime.WriteTo(stream);
      stream.Seek(0, SeekOrigin.Begin);
      HashAlgorithm hashAlgorithm = SHA1.Create();
      byte[] micHash = hashAlgorithm.ComputeHash(stream);
      var micValue = Convert.ToBase64String(micHash);
      }

      The above method only works, if the payload will always be using \r\n (Windows style linebreak) and not \n (unix style linebreak). This is because the WriteTo method changes the \n to \r\n and therefore the MIC calculation will be wrong.

      To avoid this you can do something like this and concatinate the headers and the body yourself:

      var headerData = new byte[0]; // This should contain your headers (including a newline at the end)
      var payloadData = new byte[0]; // Your AS2 payload

      using (var fullStream = new MemoryStream())
      {
      fullStream.Write(headerData, 0, headerData.Length);
      fullStream.Write(payloadData, headerData.Length, payloadData.Length);
      fullStream.Seek(0, SeekOrigin.Begin);

      HashAlgorithm hashAlgorithm = SHA1.Create();
      byte[] micHash = hashAlgorithm.ComputeHash(fullStream);
      var micValue = Convert.ToBase64String(micHash);
      }

      If you need anything else but the SHA1 algorithm, you can just change it.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s