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

Send an AS2 message with .NET

UPDATE Jan 2011: I’ve written a post about receiving AS2 messages.

A recent project at work required us to generate and send AS2 (Applicability Statement 2) messages. A colleague had written a project years ago using .NET 1.1 and a COM dll for encryption, so I set about porting that to Visual Studio 2008 and .NET 3.5.

I also wanted to use .NET’s System.Security libraries for encryption instead of having to use reflection to call the old COM dll.

It was quite difficult to get the signing and encryption working – in the end I had to resort to reverse engineering, by getting BizTalk to send an AS2 message and intercepting the message content with Fiddler, so that I could see how the AS2 messages and their headers were supposed to look.

Here’s some code which constructs an AS2 message and sends it over HTTP:

using System;
using System.IO;
using System.Net;

namespace WebTestPlugins.AS2Helpers
{
    public struct ProxySettings
    {
        public string Name;
        public string Username;
        public string Password;
        public string Domain;
    }

    public class AS2Send
    {
        public static HttpStatusCode SendFile(Uri uri, string filename, byte[] fileData, string from, string to, ProxySettings proxySettings, int timeoutMs, string signingCertFilename, string signingCertPassword, string recipientCertFilename)
        {
            if (String.IsNullOrEmpty(filename)) throw new ArgumentNullException("filename");

            if (fileData.Length == 0) throw new ArgumentException("filedata");

            byte[] content = fileData;

            //Initialise the request
            HttpWebRequest http = (HttpWebRequest)WebRequest.Create(uri);
           
            if (!String.IsNullOrEmpty(proxySettings.Name))
            {
                 WebProxy proxy = new WebProxy(proxySettings.Name);
               
                NetworkCredential proxyCredential = new NetworkCredential();
                proxyCredential.Domain = proxySettings.Domain;
                proxyCredential.UserName = proxySettings.Username;
                proxyCredential.Password = proxySettings.Password;

                proxy.Credentials = proxyCredential;
               
                http.Proxy = proxy;
            }

            //Define the standard request objects
            http.Method = "POST";

            http.AllowAutoRedirect = true;

            http.KeepAlive = true;

            http.PreAuthenticate = false; //Means there will be two requests sent if Authentication required.
            http.SendChunked = false;

            http.UserAgent = "MY SENDING AGENT";

            //These Headers are common to all transactions
            http.Headers.Add("Mime-Version", "1.0");
            http.Headers.Add("AS2-Version", "1.2"); 

            http.Headers.Add("AS2-From", from);
            http.Headers.Add("AS2-To", to);
            http.Headers.Add("Subject", filename + " transmission.");
            http.Headers.Add("Message-Id", "<AS2_" + DateTime.Now.ToString("hhmmssddd") + ">");
            http.Timeout = timeoutMs;

            string contentType = (Path.GetExtension(filename) == ".xml") ? "application/xml" : "application/EDIFACT";

            bool encrypt = !string.IsNullOrEmpty(recipientCertFilename);
            bool sign = !string.IsNullOrEmpty(signingCertFilename);

            if (!sign && !encrypt)
            {
                http.Headers.Add("Content-Transfer-Encoding", "binary");
                http.Headers.Add("Content-Disposition", "inline; filename=\"" + filename + "\"");
            }
            if (sign)
            {
                // Wrap the file data with a mime header
                content = AS2MIMEUtilities.CreateMessage(contentType, "binary", "", content);

                content = AS2MIMEUtilities.Sign(content, signingCertFilename, signingCertPassword, out contentType);

                http.Headers.Add("EDIINT-Features", "multiple-attachments");

            }
            if (encrypt)
            {
                if (string.IsNullOrEmpty(recipientCertFilename))
                {
                    throw new ArgumentNullException(recipientCertFilename, "if encrytionAlgorithm is specified then recipientCertFilename must be specified");
                }

                byte[] signedContentTypeHeader = System.Text.ASCIIEncoding.ASCII.GetBytes("Content-Type: " + contentType + Environment.NewLine);
                byte[] contentWithContentTypeHeaderAdded = AS2MIMEUtilities.ConcatBytes(signedContentTypeHeader, content);

                content = AS2Encryption.Encrypt(contentWithContentTypeHeaderAdded, recipientCertFilename, EncryptionAlgorithm.DES3);
               

                contentType = "application/pkcs7-mime; smime-type=enveloped-data; name=\"smime.p7m\"";
            }
           
            http.ContentType = contentType;           
            http.ContentLength = content.Length;

            SendWebRequest(http, content);

            return HandleWebResponse(http);
        }

        private static HttpStatusCode HandleWebResponse(HttpWebRequest http)
        {
            HttpWebResponse response = (HttpWebResponse)http.GetResponse();
           
            response.Close();
            return response.StatusCode;
        }

        private static void SendWebRequest(HttpWebRequest http, byte[] fileData)
        {
            Stream oRequestStream = http.GetRequestStream();
            oRequestStream.Write(fileData, 0, fileData.Length);
            oRequestStream.Flush(); 
            oRequestStream.Close();
        }
    }
}

And here’s the code which signs and/or encrypts the message:

using System;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;

namespace WebTestPlugins.AS2Helpers
{
    public static class EncryptionAlgorithm
    {
        public static string DES3 = "3DES";
        public static string RC2 = "RC2";
    }

    public class AS2Encryption
    {
        internal static byte[] Encode(byte[] arMessage, string signerCert, string signerPassword)
        {
            X509Certificate2 cert = new X509Certificate2(signerCert, signerPassword);
            ContentInfo contentInfo = new ContentInfo(arMessage);

            SignedCms signedCms = new SignedCms(contentInfo, true); // <- true detaches the signature
            CmsSigner cmsSigner = new CmsSigner(cert);

            signedCms.ComputeSignature(cmsSigner);
            byte[] signature = signedCms.Encode();

            return signature;
        }

        internal static byte[] Encrypt(byte[] message, string recipientCert, string encryptionAlgorithm)
        {
            if (!string.Equals(encryptionAlgorithm, EncryptionAlgorithm.DES3) && !string.Equals(encryptionAlgorithm, EncryptionAlgorithm.RC2))
                throw new ArgumentException("encryptionAlgorithm argument must be 3DES or RC2 - value specified was:" + encryptionAlgorithm);

            X509Certificate2 cert = new X509Certificate2(recipientCert);

            ContentInfo contentInfo = new ContentInfo(message);

            EnvelopedCms envelopedCms = new EnvelopedCms(contentInfo,
                new AlgorithmIdentifier(new System.Security.Cryptography.Oid(encryptionAlgorithm))); // should be 3DES or RC2

            CmsRecipient recipient = new CmsRecipient(SubjectIdentifierType.IssuerAndSerialNumber, cert);

            envelopedCms.Encrypt(recipient);

            byte[] encoded = envelopedCms.Encode();

            return encoded;
        }

        internal static byte[] Decrypt(byte[] encodedEncryptedMessage, out string encryptionAlgorithmName)
        {
            EnvelopedCms envelopedCms = new EnvelopedCms();

            // NB. the message will have been encrypted with your public key.
            // The corresponding private key must be installed in the Personal Certificates folder of the user
            // this process is running as.
            envelopedCms.Decode(encodedEncryptedMessage);

            envelopedCms.Decrypt();
            encryptionAlgorithmName = envelopedCms.ContentEncryptionAlgorithm.Oid.FriendlyName;

            return envelopedCms.Encode();
        }

    }
}

For completeness you’ll also need the AS2MimeUtilities class. Disclaimer: I didn’t write this class, it’s legacy hungarian-notated goodness.

using System;
using System.Text;

namespace WebTestPlugins.AS2Helpers
{
    /// <summary>
    /// Contains a number of useful static functions for creating MIME messages.
    /// </summary>
    public class AS2MIMEUtilities
    {
        public const string MESSAGE_SEPARATOR = "\r\n\r\n";
        public AS2MIMEUtilities()
        {
        }

        /// <summary>
        /// return a unique MIME style boundary
        /// this needs to be unique enought not to occur within the data
        /// and so is a Guid without - or { } characters.
        /// </summary>
        /// <returns></returns>
        protected static string MIMEBoundary()
        {
            return "_" + Guid.NewGuid().ToString("N") + "_";
        }

        /// <summary>
        /// Creates the a Mime header out of the components listed.
        /// </summary>
        /// <param name="sContentType">Content type</param>
        /// <param name="sEncoding">Encoding method</param>
        /// <param name="sDisposition">Disposition options</param>
        /// <returns>A string containing the three headers.</returns>
        public static string MIMEHeader(string sContentType, string sEncoding, string sDisposition)
        {
            string sOut = "";

            sOut = "Content-Type: " + sContentType + Environment.NewLine;
            if (sEncoding != "" )
                sOut += "Content-Transfer-Encoding: " + sEncoding + Environment.NewLine;

            if (sDisposition != "" )
                sOut += "Content-Disposition: " + sDisposition + Environment.NewLine;

            sOut = sOut + Environment.NewLine;

            return sOut;
        }

        /// <summary>
        /// Return a single array of bytes out of all the supplied byte arrays.
        /// </summary>
        /// <param name="arBytes">Byte arrays to add</param>
        /// <returns>The single byte array.</returns>
        public static byte[] ConcatBytes(params byte[][] arBytes)
        {
            long lLength = 0;
            long lPosition = 0;

            //Get total size required.
            foreach(byte[] ar in arBytes)
                lLength += ar.Length;

            //Create new byte array
            byte[] toReturn = new byte[lLength];
               
            //Fill the new byte array
            foreach(byte[] ar in arBytes)
            {
                ar.CopyTo(toReturn,lPosition);
                lPosition += ar.Length;
            }

            return toReturn;
        }

        /// <summary>
        /// Create a Message out of byte arrays (this makes more sense than the above method)
        /// </summary>
        /// <param name="sContentType">Content type ie multipart/report</param>
        /// <param name="sEncoding">The encoding provided...</param>
        /// <param name="sDisposition">The disposition of the message...</param>
        /// <param name="abMessageParts">The byte arrays that make up the components</param>
        /// <returns>The message as a byte array.</returns>
        public static byte[] CreateMessage(string sContentType, string sEncoding, string sDisposition, params byte[][] abMessageParts)
        {
            int iHeaderLength=0;
            return CreateMessage(sContentType, sEncoding, sDisposition, out iHeaderLength, abMessageParts);
        }
        /// <summary>
        /// Create a Message out of byte arrays (this makes more sense than the above method)
        /// </summary>
        /// <param name="sContentType">Content type ie multipart/report</param>
        /// <param name="sEncoding">The encoding provided...</param>
        /// <param name="sDisposition">The disposition of the message...</param>
        /// <param name="iHeaderLength">The length of the headers.</param>
        /// <param name="abMessageParts">The message parts.</param>
        /// <returns>The message as a byte array.</returns>
        public static byte[] CreateMessage(string sContentType, string sEncoding, string sDisposition, out int iHeaderLength, params byte[][] abMessageParts)
        {
            long lLength = 0;
            long lPosition = 0;

            //Only one part... Add headers only...
            if (abMessageParts.Length==1)
            {
                byte[] bHeader = ASCIIEncoding.ASCII.GetBytes(MIMEHeader(sContentType, sEncoding, sDisposition));
                iHeaderLength = bHeader.Length;
                return ConcatBytes(bHeader, abMessageParts[0]);
            }
            else
            {
                // get boundary and "static" subparts.
                string sBoundary = MIMEBoundary();
                byte[] bPackageHeader = ASCIIEncoding.ASCII.GetBytes(MIMEHeader(sContentType + "; boundary=\"" + sBoundary + "\"", sEncoding, sDisposition));
                byte[] bBoundary = ASCIIEncoding.ASCII.GetBytes(Environment.NewLine + "--" + sBoundary + Environment.NewLine);
                byte[] bFinalFooter = ASCIIEncoding.ASCII.GetBytes(Environment.NewLine + "--" + sBoundary + "--" + Environment.NewLine);

                //Calculate the total size required.
                iHeaderLength = bPackageHeader.Length;

                foreach(byte[] ar in abMessageParts)
                    lLength += ar.Length;
                lLength += iHeaderLength + bBoundary.Length*abMessageParts.Length +
                    bFinalFooter.Length;

                //Create new byte array to that size.
                byte[] toReturn = new byte[lLength];
               
                //Copy the headers in.
                bPackageHeader.CopyTo(toReturn, lPosition);
                lPosition += bPackageHeader.Length;

                //Fill the new byte array in by coping the message parts.
                foreach(byte[] ar in abMessageParts)
                {
                    bBoundary.CopyTo(toReturn, lPosition);
                    lPosition += bBoundary.Length;

                    ar.CopyTo(toReturn,lPosition);
                    lPosition += ar.Length;
                }

                //Finally add the footer boundary.
                bFinalFooter.CopyTo(toReturn, lPosition);

                return toReturn;
            }
        }

        /// <summary>
        /// Signs a message and returns a MIME encoded array of bytes containing the signature.
        /// </summary>
        /// <param name="arMessage"></param>
        /// <param name="bPackageHeader"></param>
        /// <returns></returns>
        public static byte[] Sign(byte[] arMessage, string signerCert, string signerPassword, out string sContentType)
        {
            byte[] bInPKCS7 = new byte[0];

            // get a MIME boundary
            string sBoundary = MIMEBoundary();

            // Get the Headers for the entire message.
            sContentType = "multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=\"sha1\"; boundary=\"" + sBoundary + "\"";
           
            // Define the boundary byte array.
            byte[] bBoundary = ASCIIEncoding.ASCII.GetBytes(Environment.NewLine + "--" + sBoundary + Environment.NewLine);
               
            // Encode the header for the signature portion.
            byte[] bSignatureHeader = ASCIIEncoding.ASCII.GetBytes(MIMEHeader("application/pkcs7-signature; name=\"smime.p7s\"", "base64", "attachment; filename=smime.p7s"));
   
            // Get the signature.
            byte[] bSignature = AS2Encryption.Encode(arMessage, signerCert, signerPassword);
           
            // convert to base64
            string sig = Convert.ToBase64String(bSignature) + MESSAGE_SEPARATOR;
            bSignature = System.Text.ASCIIEncoding.ASCII.GetBytes(sig);

            // Calculate the final footer elements.
            byte[] bFinalFooter = ASCIIEncoding.ASCII.GetBytes("--" + sBoundary + "--" + Environment.NewLine);

            // Concatenate all the above together to form the message.
            bInPKCS7 = ConcatBytes(bBoundary, arMessage, bBoundary,
                bSignatureHeader, bSignature, bFinalFooter);

            return bInPKCS7;
        }
    }
}

Anyway, I hope this helps someone out there because I couldn’t find a sample of how to send an AS2 message in .NET.