/**
* The FreeBSD Copyright
* Copyright 1994-2008 The FreeBSD Project. All rights reserved.
* Copyright (C) 2013-2017 Philip Helger philip[at]helger[dot]com
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FREEBSD PROJECT OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation
* are those of the authors and should not be interpreted as representing
* official policies, either expressed or implied, of the FreeBSD Project.
*/
package com.helger.as2lib.util;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.mail.internet.ContentType;
import javax.mail.internet.InternetHeaders;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.helger.as2lib.CAS2Info;
import com.helger.as2lib.cert.CertificateNotFoundException;
import com.helger.as2lib.cert.ECertificatePartnershipType;
import com.helger.as2lib.cert.ICertificateFactory;
import com.helger.as2lib.cert.KeyNotFoundException;
import com.helger.as2lib.crypto.BCCryptoHelper;
import com.helger.as2lib.crypto.ECryptoAlgorithmSign;
import com.helger.as2lib.crypto.ICryptoHelper;
import com.helger.as2lib.disposition.DispositionOptions;
import com.helger.as2lib.disposition.DispositionType;
import com.helger.as2lib.message.AS2Message;
import com.helger.as2lib.message.AS2MessageMDN;
import com.helger.as2lib.message.IMessage;
import com.helger.as2lib.message.IMessageMDN;
import com.helger.as2lib.params.MessageParameters;
import com.helger.as2lib.partner.PartnershipNotFoundException;
import com.helger.as2lib.processor.CNetAttribute;
import com.helger.as2lib.session.IAS2Session;
import com.helger.as2lib.util.http.HTTPHelper;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.mime.CMimeType;
import com.helger.commons.state.ETriState;
@Immutable
public final class AS2Helper
{
private static final Logger s_aLogger = LoggerFactory.getLogger (AS2Helper.class);
private static final String HEADER_RECEIVED_CONTENT_MIC = "Received-Content-MIC";
private static final String HEADER_DISPOSITION = "Disposition";
private static final String HEADER_ORIGINAL_MESSAGE_ID = "Original-Message-ID";
private static final String HEADER_FINAL_RECIPIENT = "Final-Recipient";
private static final String HEADER_ORIGINAL_RECIPIENT = "Original-Recipient";
private static final String HEADER_REPORTING_UA = "Reporting-UA";
private static final class SingletonHolder
{
static final BCCryptoHelper s_aInstance = new BCCryptoHelper ();
}
private AS2Helper ()
{}
@Nonnull
public static ICryptoHelper getCryptoHelper ()
{
return SingletonHolder.s_aInstance;
}
/**
* Create and fill the Mdn parameter
*
* @param aSession
* Session to retrieve the certificate factory for signing
* @param aMdn
* The MDN object to be filled
* @param bSignMDN
* <code>true</code> to sign the MDN
* @param bIncludeCertificateInSignedContent
* <code>true</code> if the passed certificate should be part of the
* signed content, <code>false</code> if the certificate should not be
* put in the content. E.g. for PEPPOL this must be <code>true</code>.
* @param eMICAlg
* The MIC algorithm to be used. Must be present if bSignMDN is
* <code>true</code>.
* @param bUseOldRFC3851MicAlgs
* <code>true</code> to use the old RFC 3851 MIC algorithm names (e.g.
* <code>sha1</code>), <code>false</code> to use the new RFC 5751 MIC
* algorithm names (e.g. <code>sha-1</code>).
* @throws Exception
* In case something internally goes wrong
*/
public static void createMDNData (@Nonnull final IAS2Session aSession,
@Nonnull final IMessageMDN aMdn,
final boolean bSignMDN,
final boolean bIncludeCertificateInSignedContent,
@Nullable final ECryptoAlgorithmSign eMICAlg,
final boolean bUseOldRFC3851MicAlgs) throws Exception
{
ValueEnforcer.notNull (aSession, "AS2Session");
ValueEnforcer.notNull (aMdn, "MDN");
if (bSignMDN)
ValueEnforcer.notNull (eMICAlg, "MICAlg");
// Create the report and sub-body parts
final MimeMultipart aReportParts = new MimeMultipart ();
// Create the text part
final MimeBodyPart aTextPart = new MimeBodyPart ();
final String sText = aMdn.getText () + HTTPHelper.EOL;
aTextPart.setContent (sText, CMimeType.TEXT_PLAIN.getAsString ());
aTextPart.setHeader (CAS2Header.HEADER_CONTENT_TYPE, CMimeType.TEXT_PLAIN.getAsString ());
aReportParts.addBodyPart (aTextPart);
// Create the report part
final MimeBodyPart aReportPart = new MimeBodyPart ();
{
final InternetHeaders aReportValues = new InternetHeaders ();
aReportValues.setHeader (HEADER_REPORTING_UA, aMdn.getAttribute (AS2MessageMDN.MDNA_REPORTING_UA));
aReportValues.setHeader (HEADER_ORIGINAL_RECIPIENT, aMdn.getAttribute (AS2MessageMDN.MDNA_ORIG_RECIPIENT));
aReportValues.setHeader (HEADER_FINAL_RECIPIENT, aMdn.getAttribute (AS2MessageMDN.MDNA_FINAL_RECIPIENT));
aReportValues.setHeader (HEADER_ORIGINAL_MESSAGE_ID, aMdn.getAttribute (AS2MessageMDN.MDNA_ORIG_MESSAGEID));
aReportValues.setHeader (HEADER_DISPOSITION, aMdn.getAttribute (AS2MessageMDN.MDNA_DISPOSITION));
aReportValues.setHeader (HEADER_RECEIVED_CONTENT_MIC, aMdn.getAttribute (AS2MessageMDN.MDNA_MIC));
final Enumeration <?> aReportEn = aReportValues.getAllHeaderLines ();
final StringBuilder aReportData = new StringBuilder ();
while (aReportEn.hasMoreElements ())
aReportData.append ((String) aReportEn.nextElement ()).append (HTTPHelper.EOL);
aReportData.append (HTTPHelper.EOL);
aReportPart.setContent (aReportData.toString (), "message/disposition-notification");
}
aReportPart.setHeader (CAS2Header.HEADER_CONTENT_TYPE, "message/disposition-notification");
aReportParts.addBodyPart (aReportPart);
// Convert report parts to MimeBodyPart
final MimeBodyPart aReport = new MimeBodyPart ();
aReportParts.setSubType ("report; report-type=disposition-notification");
aReport.setContent (aReportParts);
aReport.setHeader (CAS2Header.HEADER_CONTENT_TYPE, aReportParts.getContentType ());
// Sign the MDN data if needed
if (bSignMDN)
{
final ICertificateFactory aCertFactory = aSession.getCertificateFactory ();
try
{
final X509Certificate aSenderCert = aCertFactory.getCertificate (aMdn, ECertificatePartnershipType.SENDER);
final PrivateKey aSenderKey = aCertFactory.getPrivateKey (aMdn, aSenderCert);
final MimeBodyPart aSignedReport = getCryptoHelper ().sign (aReport,
aSenderCert,
aSenderKey,
eMICAlg,
bIncludeCertificateInSignedContent,
bUseOldRFC3851MicAlgs);
aMdn.setData (aSignedReport);
}
catch (final CertificateNotFoundException ex)
{
ex.terminate ();
aMdn.setData (aReport);
}
catch (final KeyNotFoundException ex)
{
ex.terminate ();
aMdn.setData (aReport);
}
}
else
{
// No signing needed
aMdn.setData (aReport);
}
// Update the MDN headers with content information
final MimeBodyPart aData = aMdn.getData ();
aMdn.setHeader (CAS2Header.HEADER_CONTENT_TYPE, aData.getContentType ());
// final int size = getSize (aData);
// aMdn.setHeader (CAS2Header.HEADER_CONTENT_LENGTH, Integer.toString
// (size));
}
/**
* Create a new MDN
*
* @param aSession
* AS2 session to be used. May not be <code>null</code>.
* @param aMsg
* The source AS2 message for which the MDN is to be created. May not
* be <code>null</code>.
* @param aDisposition
* The disposition - either success or error. May not be
* <code>null</code>.
* @param sText
* The text to be send. May not be <code>null</code>.
* @return The created MDN object which is already attached to the passed
* source AS2 message.
* @throws Exception
* In case of an error
*/
@Nonnull
public static IMessageMDN createMDN (@Nonnull final IAS2Session aSession,
@Nonnull final AS2Message aMsg,
@Nonnull final DispositionType aDisposition,
@Nonnull final String sText) throws Exception
{
ValueEnforcer.notNull (aSession, "AS2Session");
ValueEnforcer.notNull (aMsg, "AS2Message");
ValueEnforcer.notNull (aDisposition, "Disposition");
ValueEnforcer.notNull (sText, "Text");
final AS2MessageMDN aMDN = new AS2MessageMDN (aMsg);
aMDN.setHeader (CAS2Header.HEADER_AS2_VERSION, CAS2Header.DEFAULT_AS2_VERSION);
aMDN.setHeader (CAS2Header.HEADER_DATE, DateHelper.getFormattedDateNow (CAS2Header.DEFAULT_DATE_FORMAT));
aMDN.setHeader (CAS2Header.HEADER_SERVER, CAS2Info.NAME_VERSION);
aMDN.setHeader (CAS2Header.HEADER_MIME_VERSION, CAS2Header.DEFAULT_MIME_VERSION);
aMDN.setHeader (CAS2Header.HEADER_AS2_FROM, aMsg.getPartnership ().getReceiverAS2ID ());
aMDN.setHeader (CAS2Header.HEADER_AS2_TO, aMsg.getPartnership ().getSenderAS2ID ());
// get the MDN partnership info
aMDN.getPartnership ().setSenderAS2ID (aMDN.getHeader (CAS2Header.HEADER_AS2_FROM));
aMDN.getPartnership ().setReceiverAS2ID (aMDN.getHeader (CAS2Header.HEADER_AS2_TO));
// Set the appropriate keystore aliases
aMDN.getPartnership ().setSenderX509Alias (aMsg.getPartnership ().getReceiverX509Alias ());
aMDN.getPartnership ().setReceiverX509Alias (aMsg.getPartnership ().getSenderX509Alias ());
// Update the partnership
try
{
aSession.getPartnershipFactory ().updatePartnership (aMDN, true);
}
catch (final PartnershipNotFoundException ex)
{
// This would block sending an MDN in case a PartnershipNotFoundException
// was the reason for sending the MDN :)
}
aMDN.setHeader (CAS2Header.HEADER_FROM, aMsg.getPartnership ().getReceiverEmail ());
final String sSubject = aMDN.getPartnership ().getMDNSubject ();
if (sSubject != null)
{
aMDN.setHeader (CAS2Header.HEADER_SUBJECT, new MessageParameters (aMsg).format (sSubject));
}
else
{
aMDN.setHeader (CAS2Header.HEADER_SUBJECT, "Your Requested MDN Response");
}
aMDN.setText (new MessageParameters (aMsg).format (sText));
aMDN.setAttribute (AS2MessageMDN.MDNA_REPORTING_UA, CAS2Info.NAME_VERSION +
"@" +
aMsg.getAttribute (CNetAttribute.MA_DESTINATION_IP) +
":" +
aMsg.getAttribute (CNetAttribute.MA_DESTINATION_PORT));
aMDN.setAttribute (AS2MessageMDN.MDNA_ORIG_RECIPIENT, "rfc822; " + aMsg.getHeader (CAS2Header.HEADER_AS2_TO));
aMDN.setAttribute (AS2MessageMDN.MDNA_FINAL_RECIPIENT, "rfc822; " + aMsg.getPartnership ().getReceiverAS2ID ());
aMDN.setAttribute (AS2MessageMDN.MDNA_ORIG_MESSAGEID, aMsg.getHeader (CAS2Header.HEADER_MESSAGE_ID));
aMDN.setAttribute (AS2MessageMDN.MDNA_DISPOSITION, aDisposition.getAsString ());
final String sDispositionOptions = aMsg.getHeader (CAS2Header.HEADER_DISPOSITION_NOTIFICATION_OPTIONS);
final DispositionOptions aDispositionOptions = DispositionOptions.createFromString (sDispositionOptions);
String sMIC = null;
if (aDispositionOptions.getMICAlgCount () > 0)
{
// If the source message was signed or encrypted, include the headers -
// see message sending for details
final boolean bIncludeHeadersInMIC = aMsg.getPartnership ().getSigningAlgorithm () != null ||
aMsg.getPartnership ().getEncryptAlgorithm () != null ||
aMsg.getPartnership ().getCompressionType () != null;
sMIC = getCryptoHelper ().calculateMIC (aMsg.getData (),
aDispositionOptions.getFirstMICAlg (),
bIncludeHeadersInMIC);
}
aMDN.setAttribute (AS2MessageMDN.MDNA_MIC, sMIC);
boolean bSignMDN = false;
boolean bIncludeCertificateInSignedContent = false;
if (aDispositionOptions.getProtocol () != null)
{
if (aDispositionOptions.isProtocolRequired () || aDispositionOptions.hasMICAlg ())
{
// Sign if required or if optional and a MIC algorithm is present
bSignMDN = true;
// Include certificate in signed content?
final ETriState eIncludeCertificateInSignedContent = aMsg.getPartnership ()
.getIncludeCertificateInSignedContent ();
if (eIncludeCertificateInSignedContent.isDefined ())
{
// Use per partnership
bIncludeCertificateInSignedContent = eIncludeCertificateInSignedContent.getAsBooleanValue ();
}
else
{
// Use global value
bIncludeCertificateInSignedContent = aSession.isCryptoSignIncludeCertificateInBodyPart ();
}
}
}
final boolean bUseOldRFC3851MicAlgs = aMsg.getPartnership ().isRFC3851MICAlgs ();
createMDNData (aSession,
aMDN,
bSignMDN,
bIncludeCertificateInSignedContent,
aDispositionOptions.getFirstMICAlg (),
bUseOldRFC3851MicAlgs);
aMDN.updateMessageID ();
// store MDN into msg in case AsynchMDN is sent fails, needs to be resent by
// send module
aMsg.setMDN (aMDN);
return aMDN;
}
public static void parseMDN (@Nonnull final IMessage aMsg,
@Nonnull final X509Certificate aReceiverCert,
final boolean bUseCertificateInBodyPart) throws Exception
{
s_aLogger.info ("Start parsing MDN of" + aMsg.getLoggingText ());
final IMessageMDN aMdn = aMsg.getMDN ();
MimeBodyPart aMainPart = aMdn.getData ();
final ICryptoHelper aCryptoHelper = getCryptoHelper ();
final boolean bDisableVerify = aMsg.getPartnership ().isDisableVerify ();
final boolean bMsgIsSigned = aCryptoHelper.isSigned (aMainPart);
final boolean bForceVerify = aMsg.getPartnership ().isForceVerify ();
if (bMsgIsSigned && bDisableVerify)
{
s_aLogger.info ("Message claims to be signed but signature validation is disabled" + aMsg.getLoggingText ());
}
else
if (bMsgIsSigned || bForceVerify)
{
if (bForceVerify && !bMsgIsSigned)
s_aLogger.info ("Forced verify MDN signature" + aMsg.getLoggingText ());
else
if (s_aLogger.isDebugEnabled ())
s_aLogger.debug ("Verifying MDN signature" + aMsg.getLoggingText ());
aMainPart = aCryptoHelper.verify (aMainPart, aReceiverCert, bUseCertificateInBodyPart, bForceVerify);
// Remember that message was signed and verified
aMdn.setAttribute (AS2Message.ATTRIBUTE_RECEIVED_SIGNED, Boolean.TRUE.toString ());
s_aLogger.info ("Successfully verified signature of MDN of message" + aMsg.getLoggingText ());
}
final MimeMultipart aReportParts = new MimeMultipart (aMainPart.getDataHandler ().getDataSource ());
final ContentType aReportType = new ContentType (aReportParts.getContentType ());
if (aReportType.getBaseType ().equalsIgnoreCase ("multipart/report"))
{
final int nReportCount = aReportParts.getCount ();
for (int j = 0; j < nReportCount; j++)
{
final MimeBodyPart aReportPart = (MimeBodyPart) aReportParts.getBodyPart (j);
if (aReportPart.isMimeType (CMimeType.TEXT_PLAIN.getAsString ()))
{
// XXX is this "toString" really a correct solution?
aMdn.setText (aReportPart.getContent ().toString ());
}
else
if (aReportPart.isMimeType ("message/disposition-notification"))
{
final InternetHeaders aDisposition = new InternetHeaders (aReportPart.getInputStream ());
aMdn.setAttribute (AS2MessageMDN.MDNA_REPORTING_UA, aDisposition.getHeader (HEADER_REPORTING_UA, ", "));
aMdn.setAttribute (AS2MessageMDN.MDNA_ORIG_RECIPIENT,
aDisposition.getHeader (HEADER_ORIGINAL_RECIPIENT, ", "));
aMdn.setAttribute (AS2MessageMDN.MDNA_FINAL_RECIPIENT,
aDisposition.getHeader (HEADER_FINAL_RECIPIENT, ", "));
aMdn.setAttribute (AS2MessageMDN.MDNA_ORIG_MESSAGEID,
aDisposition.getHeader (HEADER_ORIGINAL_MESSAGE_ID, ", "));
aMdn.setAttribute (AS2MessageMDN.MDNA_DISPOSITION, aDisposition.getHeader (HEADER_DISPOSITION, ", "));
aMdn.setAttribute (AS2MessageMDN.MDNA_MIC, aDisposition.getHeader (HEADER_RECEIVED_CONTENT_MIC, ", "));
}
else
s_aLogger.info ("Got unsupported MDN body part MIME type: " + aReportPart.getContentType ());
}
}
}
}