Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't create Multipart with custom ContentType #230

Closed
xtomas opened this issue Feb 11, 2016 · 10 comments
Closed

Can't create Multipart with custom ContentType #230

xtomas opened this issue Feb 11, 2016 · 10 comments
Labels
enhancement New feature or request

Comments

@xtomas
Copy link

xtomas commented Feb 11, 2016

Hello Jeffrey,

I'm using your great library more than one year for AS2 protocol implementation. After branch 1.20 it stopped working. I guess the problem started after fix #221 (Fixed serialization of mime parts with empty content).

What I need is to create Multipart with given ContentType. But this constructor of MimeEntity is protected and in Multipart is not implemented so there is no way to use it outside MimeKit Assembly.

Here is example of correct AS2 Message.

Mime-Version: 1.0
AS2-Version: 1.1
AS2-From: testAs2To
AS2-To: testAs2From
Message-Id: <AS2-201602110953571421@testAs2From>
Subject: Message Delivery Notification

Content-Type: multipart/report; report-type=disposition-notification;
    boundary="=-rpGcNT6r+c4y7c4OYTO+aQ=="

--=-rpGcNT6r+c4y7c4OYTO+aQ==
Content-Type: text/plain
Content-Transfer-Encoding: 7bit

The AS2 message has been received. Thank you for exchanging AS2 messages.

--=-rpGcNT6r+c4y7c4OYTO+aQ==
Content-Type: message/disposition-notification
Content-Transfer-Encoding: 7bit

Original-Message-ID: 123456789@test
Received-Content-MIC: ETiMu+fgVMHYfRgfF0ALD9F+0DA=, sha1
Reporting-UA: Vzp.B2B.As2; VZP B2B Agent v0.1.0.0
Original-Recipient: rfc822;testAs2To
Final-Recipient: rfc822;testAs2To
Disposition: automatic-action/MDN-sent-automatically; processed

--=-rpGcNT6r+c4y7c4OYTO+aQ==--

Until 1.19 I was able to use workaround to create Multipart.

// create text part (human readable part of mdn message)
var textPart = new MimePart(As2ContentType.TextContentType)
{
   ContentTransferEncoding = ContentEncoding.SevenBit,
    ContentObject = new ContentObject(new MemoryStream(Text.GetBytes()))
};

// create mdn part (message/disposition-notification format)
var mdnPart = new MimeKit.MessageDispositionNotification
{
    ContentTransferEncoding = ContentEncoding.SevenBit,
    ContentObject = new ContentObject(new MemoryStream(this.ToString().GetBytes()))
};

Multipart content;

// create multipart mime message
using (var memory = new MemoryStream())
{
    content = (Multipart)Multipart.Load(As2ContentType.ReportContentType, memory);
}

var content = new Multipart(As2ContentType.ReportContentType);

content.Add(textPart);
content.Add(mdnPart);

After you have fixed working with empty content, there is no boundary at the end. (in debug console there is message "Multipart without a boundary encountered!" and WriteEndBoundary is set to false.)

The easy fix I see is to add following constructor into Multipart class. It's working, I have tested that.

public Multipart (ContentType contentType) : base (contentType)
{
    ContentType.Parameters["boundary"] = GenerateBoundary();
    children = new List<MimeEntity>();
    WriteEndBoundary = true;
}

So my functional code after that is:

/// <summary>
/// Create Mdn content as multipart mime message
/// </summary>
/// <returns>Multipart instance</returns>
public Multipart GetContent()
{
    // create text part (human readable part of mdn message)
    var textPart = new MimePart(As2ContentType.TextContentType)
    {
        ContentTransferEncoding = ContentEncoding.SevenBit,
        ContentObject = new ContentObject(new MemoryStream(Text.GetBytes()))
    };

    // create mdn part (message/disposition-notification format)
    var mdnPart = new MimeKit.MessageDispositionNotification
    {
        ContentTransferEncoding = ContentEncoding.SevenBit,
        ContentObject = new ContentObject(new MemoryStream(this.ToString().GetBytes()))
    };

    var content = new Multipart(As2ContentType.ReportContentType);

    content.Add(textPart);
    content.Add(mdnPart);

    return content;
}

Do you see another solution?

Regards,

Tomas

@jstedfast
Copy link
Owner

The fact that your work-around stopped working in 1.2.20 is a bug (and I just fixed that), but I don't really like adding a Multipart constructor that takes a ContentType.

Instead, I propose a MultipartReport class.

@jstedfast jstedfast added the enhancement New feature or request label Feb 11, 2016
@jstedfast
Copy link
Owner

FWIW, you'll be able to do this:

var content = new MultipartReport ("disposition-notification");

@xtomas
Copy link
Author

xtomas commented Feb 11, 2016

Wow, this is an exceptional support 👍

All my unit tests are successfull again :-) I will use it immediately after new MimeKit nuget version is available.

@jstedfast
Copy link
Owner

I'll probably release a new nuget this weekend (I normally make releases on weekends due to lack of time on weeknights), so by Monday, there should be a new release waiting for you :)

@jstedfast
Copy link
Owner

I've just released MimeKit 1.2.21 with these fixes to nuget.org

@xtomas
Copy link
Author

xtomas commented Feb 15, 2016

New nuget packet version 1.2.21 works without any problem.

Thank you 👍

@dawud-tan
Copy link

@xtomas could you share the code how to construct application/edifact MimeMessage and sign then encrypt it using MimeKit? please

@jstedfast
Copy link
Owner

To construct an application/edifact MIME part, you would just do this:

var edifact = new MimePart ("application", "edifact") {
    ContentObject = new ContentObject (contentStream)
};

To set that as the message body, you can just do:

message.Body = edifact;

I can't help you with encrypting since I know nothing about that...

@xtomas
Copy link
Author

xtomas commented Dec 21, 2017

Hi @dawud-tan . I do not use edifact format for AS2 messages (but xml), but for signing/encrypting I've created class, derived from WindowsSecureMimeContext.

    /// <summary>
    /// Windows Secure Mime Context extended for AS2 purposes
    /// </summary>
    internal class As2SecureMimeContext : WindowsSecureMimeContext
    {
        #region Delegates

        /// <summary>
        /// Mic delegate
        /// </summary>
        /// <param name="sender">sender object instance</param>
        /// <param name="e">MessageIntegrityCheckArg instance</param>
        public delegate void MicDelegate(object sender, MessageIntegrityCheckEventArgs e);

        /// <summary>
        /// Mic calculated event
        /// </summary>
        public event MicDelegate MicCalculated;

        /// <summary>
        /// On Mic calculated event related methods
        /// </summary>
        /// <param name="mic">MessageIntegrityCheck instance</param>
        protected virtual void OnMicCalculated(MessageIntegrityCheck mic)
        {
            MicCalculated?.Invoke(this, new MessageIntegrityCheckEventArgs(mic));
        }

        #endregion Delegates

        #region Overrides

        /// <summary>
        /// Sign signedContent using certificate associated with mailbox address
        /// </summary>
        /// <exception cref="ArgumentException">If signer or content is null.</exception>
        /// <param name="signer">Use SecureMailboxAddress</param>
        /// <param name="digestAlgo">sha1 and md5 supported only</param>
        /// <param name="content">Stream instance</param>
        /// <returns>MimeKit.MimePart instance</returns>
        public override MimeKit.MimePart Sign(MimeKit.MailboxAddress signer, DigestAlgorithm digestAlgo, Stream content)
        {
            if (content == null)
                throw new ArgumentNullException(nameof(content));
            if (signer == null)
                throw new ArgumentNullException(nameof(signer));

            var mic = new MessageIntegrityCheck(content, (MicAlgorithm)digestAlgo);

            OnMicCalculated(mic);

            return base.Sign(signer, digestAlgo, content);
        }

        /// <summary>
        /// Sign
        /// </summary>
        /// <exception cref="ArgumentNullException">If <paramref name="certificate"/> is null or empty or content is null.</exception>
        /// <param name="certificate"><see cref="X509Certificate2"/> instance</param>
        /// <param name="digestAlgo">DigestAlgorithm enum partnerCertificates</param>
        /// <param name="content">Stream instance</param>
        /// <returns>MimePart instance</returns>
        public MimePart Sign(X509Certificate2 certificate, DigestAlgorithm digestAlgo, Stream content)
        {
            if (certificate == null)
                throw new ArgumentNullException(nameof(certificate));

            if (content == null)
                throw new ArgumentNullException(nameof(content));

            var contentInfo = new ContentInfo(ReadAllBytes(content));
            var cmsSigner = GetRealCmsSigner(certificate, digestAlgo);

            var mic = new MessageIntegrityCheck(contentInfo.Content, (MicAlgorithm)digestAlgo);
            OnMicCalculated(mic);

            var signed = new SignedCms(contentInfo, true);

            signed.ComputeSignature(cmsSigner);
            var signedData = signed.Encode();

            return new ApplicationPkcs7Signature(new MemoryStream(signedData, false));
        }

        #endregion Overrides

        #region Private methods

        /// <summary>
        /// Read all bytes using memory block stream
        /// </summary>
        /// <param name="stream">Stream instance</param>
        /// <returns>byte array</returns>
        protected static byte[] ReadAllBytes(Stream stream)
        {
            if (stream is MemoryBlockStream mbs)
                return mbs.ToArray();

            if (stream is MemoryStream ms)
                return ms.ToArray();

            using (var memory = new MemoryBlockStream())
            {
                stream.CopyTo(memory, 4096);
                return memory.ToArray();
            }
        }

        /// <summary>
        /// Get Real <see cref="System.Security.Cryptography.Pkcs.CmsSigner">CmsSigner</see>
        /// </summary>
        /// <exception cref="ArgumentNullException">If <paramref name="certificate"/> is null.</exception>
        /// <param name="certificate"><see cref="X509Certificate2"/> instance</param>
        /// <param name="digestAlgo">DigestAlgorithm enum partnerCertificates</param>
        /// <returns>RealCmsSigner(System.Security.Cryptography.Pkcs.CmsSigner) instance</returns>
        protected virtual RealCmsSigner GetRealCmsSigner(X509Certificate2 certificate, DigestAlgorithm digestAlgo)
        {
            if (certificate == null)
                throw new ArgumentNullException(nameof(certificate));

            var signer = new RealCmsSigner(certificate)
            {
                DigestAlgorithm = new Oid(GetDigestOid(digestAlgo)),
                IncludeOption = X509IncludeOption.EndCertOnly
            };
            signer.SignedAttributes.Add(new Pkcs9SigningTime());

            return signer;
        }

        #endregion Private methods
    }

    /// <summary>
    /// MIC EventArgs
    /// </summary>
    internal class MessageIntegrityCheckEventArgs : EventArgs
    {
        /// <summary>
        /// Message Intergrity Check (MIC)
        /// </summary>
        public MessageIntegrityCheck MessageIntegrityCheck { get; internal set; }

        /// <summary>a
        /// Create Message Integrity Check event arguments
        /// </summary>
        /// <exception cref="ArgumentNullException">If mic is null.</exception>
        /// <param name="mic">MessageIntegrityCheck Instance</param>
        public MessageIntegrityCheckEventArgs(MessageIntegrityCheck mic)
        {
            MessageIntegrityCheck = mic ?? throw new ArgumentNullException(nameof(mic));
        }
    }

To created signed multipart, I use this method.

        private MultipartSigned SignContent(X509Certificate2 certificate)
        {
            if (certificate == null)
                throw new ArgumentNullException(nameof(certificate));

            MultipartSigned signedParts;

            using (var ctx = new As2SecureMimeContext())
            {
                // after mic is ready, set Mic property
                ctx.MicCalculated += (s, mic) => Mic = mic.MessageIntegrityCheck;

                signedParts = ctx.CreateAndVerify(certificate, DigestAlgorithm.Sha1, this.Content);
            }

            return signedParts;
        }

I hope, it will help you ;-)

TJ

@Steffaan
Copy link

@xtomas Your code example looks very helpfull. I'm also facing the task of writing an AS2 handler. Are you willing to share some more of your code using MimeKit? Especcially the MessageIntegrityCheck class would be helpfull.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants