/* * Copyright 2010-2017 Norwegian Agency for Public Management and eGovernment (Difi) * * Licensed under the EUPL, Version 1.1 or – as soon they * will be approved by the European Commission - subsequent * versions of the EUPL (the "Licence"); * * You may not use this work except in compliance with the Licence. * * You may obtain a copy of the Licence at: * * https://joinup.ec.europa.eu/community/eupl/og_page/eupl * * Unless required by applicable law or agreed to in * writing, software distributed under the Licence is * distributed on an "AS IS" basis, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. * See the Licence for the specific language governing * permissions and limitations under the Licence. */ package no.difi.oxalis.as2.util; import no.difi.oxalis.commons.bouncycastle.BCHelper; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cms.SignerInformation; import org.bouncycastle.cms.SignerInformationStore; import org.bouncycastle.cms.SignerInformationVerifier; import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.mail.smime.SMIMESigned; import org.bouncycastle.util.Store; import org.testng.annotations.Test; import javax.activation.MimeType; import javax.mail.BodyPart; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.security.Provider; import java.security.Security; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.Iterator; import static org.testng.Assert.*; /** * @author steinar * @author thore */ public class MimeMessageHelperTest { public static final String OPENAS2_MDN_TXT = "openas2-mdn.txt"; public static final String OPENAS2_MDN_NO_HEADERS_TXT = "openas2-mdn-no-headers.txt"; static { BCHelper.registerProvider(); } @Test public void testToString() throws Exception { InputStream resourceAsStream = MimeMessageHelperTest.class.getClassLoader().getResourceAsStream(OPENAS2_MDN_TXT); assertNotNull(resourceAsStream, OPENAS2_MDN_TXT + " not found in classpath"); MimeMessage mimeMessage = MimeMessageHelper.createMimeMessage(resourceAsStream); byte[] bytes = MimeMessageHelper.toBytes(mimeMessage); String s = new String(bytes); assertTrue(s.contains("Content-Type")); s = MimeMessageHelper.toString(mimeMessage); assertTrue(s.contains("Content-Type")); } @Test public void parseLegalMimeMessageWithHeaders() throws Exception { InputStream resourceAsStream = MimeMessageHelperTest.class.getClassLoader().getResourceAsStream(OPENAS2_MDN_TXT); assertNotNull(resourceAsStream, OPENAS2_MDN_TXT + " not found in classpath"); MimeMessage mimeMessage = MimeMessageHelper.createMimeMessage(resourceAsStream); Object content = mimeMessage.getContent(); assertNotNull(content); String contentType = mimeMessage.getContentType(); assertEquals(new MimeType(contentType).getBaseType(), new MimeType("multipart/signed").getBaseType()); MimeMultipart mimeMultipartSigned = (MimeMultipart) content; // Two Bodies in the MimeMultiPart assertEquals(mimeMultipartSigned.getCount(), 2); // First body is a multipart/report BodyPart firstBody = mimeMultipartSigned.getBodyPart(0); assertEquals(new MimeType(firstBody.getContentType()).getBaseType(), new MimeType("multipart/report").getBaseType()); // Second body contains the signature BodyPart secondBody = mimeMultipartSigned.getBodyPart(1); assertEquals(new MimeType(secondBody.getContentType()).getBaseType(), new MimeType("application/pkcs7-signature").getBaseType()); // The inner multipart should contain two bodies: the text/plain and the message/disposition-notification MimeMultipart innerMultiPart = (MimeMultipart) firstBody.getContent(); assertNotNull(innerMultiPart); // Must have two parts assertEquals(innerMultiPart.getCount(), 2); // First part is text/plain assertEquals(new MimeType(innerMultiPart.getBodyPart(0).getContentType()).getBaseType(), new MimeType("text/plain").getBaseType()); // Second part of first part in multipart is message/disposition-notification assertEquals(new MimeType(innerMultiPart.getBodyPart(1).getContentType()).getBaseType(), new MimeType("message/disposition-notification").getBaseType()); } @Test public void verifyingSignatureOfRealMdn() throws Exception { boolean debug = false; // enable this to add certificate debugging // first we validate some real positive MDN's from various systems String[] mdnsToVerify = {"itsligo-mdn.txt", "unit4-mdn.txt", "unimaze-mdn.txt", "difi-negative-mdn.txt"}; for (String resourceName : mdnsToVerify) { boolean verified = verify(resourceName, debug); //System.out.println("Verification of " + resourceName + " returned status=" + verified); assertTrue(verified, "Resource " + resourceName + " signature did not validate"); } // then we validate some real negative MDN's from various systems String[] mdnsNegative = {"unit4-mdn-negative.txt"}; for (String resourceName : mdnsNegative) { boolean verified = verify(resourceName, debug); assertTrue(verified, "Resource " + resourceName + " signature did not validated"); } // then we validate some corrupt MDN's we have manually messed up String[] mdnsToFail = {"unit4-mdn-error.txt"}; for (String resourceName : mdnsToFail) { boolean failed = verify(resourceName, debug); assertFalse(failed, "Resource " + resourceName + " signature should not have validated"); } if (debug) { // dump list of all providers registered for (Provider p : Security.getProviders()) { System.out.println("Provider : " + p.getName()); } } } /** * verify the signature (assuming the cert is contained in the message) */ private boolean verify(String resourceName, boolean debug) { System.out.println("Verifying resource " + resourceName + " (debug=" + debug + ")"); String resourcePath = "real-mdn-examples/" + resourceName; try { // shortcuts lots of steps in the above test (parseLegalMimeMessageWithHeaders) MimeMultipart multipartSigned = (MimeMultipart) MimeMessageHelper.createMimeMessage(MimeMessageHelperTest.class.getClassLoader().getResourceAsStream(resourcePath)).getContent(); assertNotNull(multipartSigned); // verify signature SMIMESigned signedMessage = new SMIMESigned(multipartSigned); Store certs = signedMessage.getCertificates(); SignerInformationStore signers = signedMessage.getSignerInfos(); for (Object signerInformation : signers.getSigners()) { SignerInformation signer = (SignerInformation) signerInformation; Collection certCollection = certs.getMatches(signer.getSID()); Iterator certIterator = certCollection.iterator(); X509Certificate cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate((X509CertificateHolder) certIterator.next()); if (debug) System.out.println("Signing certificate : " + cert); SignerInformationVerifier signerInformationVerifier = new JcaSimpleSignerInfoVerifierBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build(cert); if (signer.verify(signerInformationVerifier)) return true; } } catch (Exception ex) { System.out.println("Verification failed with exception " + ex.getMessage()); } return false; } /** * Verifies that if you don't supply the headers, a plain/text message will be created even though it might appear as being a multipart. * * @throws Exception */ @Test public void parseMimeMessageStreamWithoutContentTypeEmbedded() throws Exception { InputStream resourceAsStream = MimeMessageHelperTest.class.getClassLoader().getResourceAsStream(OPENAS2_MDN_NO_HEADERS_TXT); assertNotNull(resourceAsStream, OPENAS2_MDN_NO_HEADERS_TXT + " not found in classpath"); MimeMessage mimeMessage = MimeMessageHelper.createMimeMessage(resourceAsStream); Object content = mimeMessage.getContent(); assertNotNull(content); assertTrue(content instanceof String); assertEquals(mimeMessage.getContentType(), "text/plain"); } @Test public void parseMimeMessageExperiment() throws IOException, MessagingException { InputStream inputStream = MimeMessageHelperTest.class.getClassLoader().getResourceAsStream("mime-message.txt"); assertNotNull(inputStream, "mime-message.txt not found in class path"); MimeMessage mimeMessage = MimeMessageHelper.parseMultipart(inputStream, "multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha-1; boundary=\"----=_Part_34_426016548.1445612302735\""); Object content = mimeMessage.getContent(); assertTrue(content instanceof MimeMultipart); ByteArrayOutputStream os = new ByteArrayOutputStream(); mimeMessage.writeTo(os); String s = new String(os.toByteArray()); assertFalse(s.contains("--null")); } /** * Verifies that if you supply the correct "Content-Type:" together with an input stream, which does not contain the * required "Content-Type:" header at the start, may be created by simply supplying the header. * <p> * This would mimic how to create a mime message from a Servlet input stream. * * @throws Exception */ @Test public void parseMimeMessageStreamWithSuppliedContentType() throws Exception { InputStream resourceAsStream = MimeMessageHelperTest.class.getClassLoader().getResourceAsStream(OPENAS2_MDN_NO_HEADERS_TXT); assertNotNull(resourceAsStream, OPENAS2_MDN_NO_HEADERS_TXT + " not found in classpath"); MimeType mimeType = new MimeType("multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha1;\n" + "\tboundary=\"----=_Part_2_1193010873.1384331414156\""); assertEquals(mimeType.getBaseType(), "multipart/signed"); assertEquals(mimeType.getSubType(), "signed"); MimeMessage m2 = MimeMessageHelper.parseMultipart(resourceAsStream, mimeType.toString()); m2.writeTo(System.out); Object content2 = m2.getContent(); assertTrue(content2 instanceof MimeMultipart, "Not MimeMultiPart as excpected, but " + content2.getClass().getSimpleName()); assertEquals(new MimeType(m2.getContentType()).getBaseType(), new MimeType("multipart/signed").getBaseType()); } }