org.slf4j
slf4j-api
diff --git a/acme4j-smime/src/main/java/module-info.java b/acme4j-smime/src/main/java/module-info.java
index 81f30867..901135b2 100644
--- a/acme4j-smime/src/main/java/module-info.java
+++ b/acme4j-smime/src/main/java/module-info.java
@@ -18,13 +18,17 @@ module org.shredzone.acme4j.smime {
requires transitive jakarta.mail;
requires static com.github.spotbugs.annotations;
+ requires org.bouncycastle.util;
+ requires org.bouncycastle.mail;
requires org.bouncycastle.pkix;
requires org.bouncycastle.provider;
+ requires org.slf4j;
exports org.shredzone.acme4j.smime;
exports org.shredzone.acme4j.smime.challenge;
exports org.shredzone.acme4j.smime.csr;
exports org.shredzone.acme4j.smime.email;
+ exports org.shredzone.acme4j.smime.exception;
provides org.shredzone.acme4j.provider.ChallengeProvider
with org.shredzone.acme4j.smime.challenge.EmailReply00ChallengeProvider;
diff --git a/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/email/EmailProcessor.java b/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/email/EmailProcessor.java
index df483666..52867927 100644
--- a/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/email/EmailProcessor.java
+++ b/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/email/EmailProcessor.java
@@ -16,38 +16,56 @@ package org.shredzone.acme4j.smime.email;
import static java.util.Objects.requireNonNull;
import static jakarta.mail.Message.RecipientType.TO;
+import java.io.IOException;
import java.net.URL;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
+import edu.umd.cs.findbugs.annotations.CheckForNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
import jakarta.mail.Address;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
+import jakarta.mail.Session;
+import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
+import jakarta.mail.internet.MimeMessage;
+import jakarta.mail.internet.MimeMultipart;
+import org.bouncycastle.cms.CMSException;
+import org.bouncycastle.cms.SignerInformation;
+import org.bouncycastle.cms.SignerInformationVerifier;
+import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
+import org.bouncycastle.mail.smime.SMIMESigned;
+import org.bouncycastle.operator.OperatorCreationException;
import org.shredzone.acme4j.Identifier;
import org.shredzone.acme4j.Login;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.smime.challenge.EmailReply00Challenge;
+import org.shredzone.acme4j.smime.exception.AcmeInvalidMessageException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* A processor for incoming "Challenge" emails.
- *
- * Note that according to RFC-8823, the incoming mail must be DKIM or S/MIME signed, and
- * the signature must be validated. This is not done by this processor, because
- * it is usually checked by the inbound MTA.
*
* @see RFC 8823
* @since 2.12
*/
public final class EmailProcessor {
+ private static final Logger LOG = LoggerFactory.getLogger(EmailProcessor.class);
private static final Pattern SUBJECT_PATTERN = Pattern.compile("ACME:\\s+([0-9A-Za-z_\\s-]+=?)\\s*");
+ private static final int RFC822NAME = 1;
private final String token1;
private final Optional messageId;
@@ -56,42 +74,186 @@ public final class EmailProcessor {
private final Collection replyTo;
private final AtomicReference challengeRef = new AtomicReference<>();
+ /**
+ * Processes the given e-mail message.
+ *
+ * Note that according to RFC-8823, the challenge message must be signed using either
+ * DKIM or S/MIME. This method does not do any DKIM or S/MIME validation, and assumes
+ * that this has already been done by the inbound MTA.
+ *
+ * @param message
+ * E-mail that was received from the CA. The inbound MTA has already taken
+ * care of DKIM and/or S/MIME validation.
+ * @return EmailProcessor for this e-mail
+ * @throws AcmeInvalidMessageException
+ * if a validation failed, and the message must be rejected.
+ * @since 2.15
+ */
+ public static EmailProcessor plainMessage(Message message)
+ throws AcmeInvalidMessageException {
+ return new EmailProcessor(message, null, false, null);
+ }
+
+ /**
+ * Performs an S/MIME validation and processes the given e-mail message.
+ *
+ * The owner of the given certificate must be the sender of that email.
+ *
+ * @param message
+ * E-mail that was received from the CA.
+ * @param mailSession
+ * A {@link Session} that can be used for processing inner e-mails.
+ * @param signCert
+ * The signing certificate of the sender.
+ * @param strict
+ * If {@code true}, the S/MIME protected headers "From", "To", and "Subject"
+ * must match the headers of the received message. If {@code false},
+ * only the S/MIME protected headers are used, and the headers of the received
+ * message are ignored.
+ * @return EmailProcessor for this e-mail
+ * @throws AcmeInvalidMessageException
+ * if a validation failed, and the message must be rejected.
+ * @since 2.15
+ */
+ public static EmailProcessor smimeMessage(Message message, Session mailSession,
+ X509Certificate signCert, boolean strict)
+ throws AcmeInvalidMessageException {
+ try {
+ if (!(message instanceof MimeMessage)) {
+ throw new AcmeInvalidMessageException("Not a S/MIME message");
+ }
+ MimeMessage mimeMessage = (MimeMessage) message;
+
+ if (!(mimeMessage.getContent() instanceof MimeMultipart)) {
+ throw new AcmeProtocolException("S/MIME signed email must contain MimeMultipart");
+ }
+ MimeMultipart mp = (MimeMultipart) message.getContent();
+
+ SMIMESigned signed = new SMIMESigned(mp);
+
+ SignerInformationVerifier verifier = new JcaSimpleSignerInfoVerifierBuilder().build(signCert);
+ boolean hasMatch = false;
+ for (SignerInformation signer : signed.getSignerInfos().getSigners()) {
+ hasMatch |= signer.verify(verifier);
+ }
+ if (!hasMatch) {
+ throw new AcmeInvalidMessageException("The S/MIME signature is invalid");
+ }
+
+ MimeMessage content = signed.getContentAsMimeMessage(mailSession);
+ if (!content.getContentType().equalsIgnoreCase("message/rfc822; forwarded=no")) {
+ throw new AcmeInvalidMessageException("Message does not contain protected headers");
+ }
+
+ MimeMessage body = new MimeMessage(mailSession, content.getInputStream());
+
+ List
validFromAddresses = Optional.ofNullable(signCert.getSubjectAlternativeNames())
+ .orElseGet(Collections::emptyList)
+ .stream()
+ .filter(l -> ((Number) l.get(0)).intValue() == RFC822NAME)
+ .map(l -> l.get(1).toString())
+ .map(l -> {
+ try {
+ return new InternetAddress(l);
+ } catch (AddressException ex) {
+ // Ignore invalid email addresses
+ LOG.debug("Certificate contains invalid e-mail address {}", l, ex);
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+
+ if (validFromAddresses.isEmpty()) {
+ throw new AcmeInvalidMessageException("Signing certificate does not provide a rfc822Name subjectAltName");
+ }
+
+ return new EmailProcessor(message, body, strict, validFromAddresses);
+ } catch (IOException | MessagingException | CMSException | OperatorCreationException |
+ CertificateParsingException ex) {
+ throw new AcmeInvalidMessageException("Invalid S/MIME mail", ex);
+ }
+ }
+
/**
* Creates a new {@link EmailProcessor} for the incoming "Challenge" message.
*
- * The incoming message is validated against the requirements of RFC-8823. An {@link
- * AcmeProtocolException} is thrown if the validation fails. DKIM or S/MIME signature
- * is not checked by the processor, and must be checked elsewhere (usually by
- * the inbound MTA).
+ * The incoming message is validated against the requirements of RFC-8823.
*
* @param message
* "Challenge" message as it was sent by the CA.
- * @throws AcmeProtocolException
- * if the incoming message is not a valid "challenge" message according to
- * RFC-8823.
+ * @param signedMessage
+ * The signed part of the challenge message if present, or {@code null}. The
+ * signature is assumed to be valid, and must be validated in a previous
+ * step.
+ * @param strict
+ * If {@code true}, the S/MIME protected headers "From", "To", and "Subject"
+ * must match the headers of the received message. If {@code false},
+ * only the S/MIME protected headers are used, and the headers of the received
+ * message are ignored.
+ * @param validFromAddresses
+ * A {@link List} of {@link Address} that were found in the certificate's
+ * rfc822Name subjectAltName extension. The mail's From address must
+ * be found in this list, otherwise the signed message will be rejected.
+ * {@code null} to disable this validation step.
+ * @throws AcmeInvalidMessageException
+ * if a validation failed, and the message must be rejected.
*/
- public EmailProcessor(Message message) {
+ private EmailProcessor(Message message, @Nullable MimeMessage signedMessage,
+ boolean strict, @Nullable List
validFromAddresses)
+ throws AcmeInvalidMessageException {
requireNonNull(message, "message");
// Validate challenge and extract token 1
try {
- if (!isAutoGenerated(message)) {
- throw new AcmeProtocolException("Message is not auto-generated");
+ if (!isAutoGenerated(getOptional(m -> m.getHeader("Auto-Submitted"), message, signedMessage))) {
+ throw new AcmeInvalidMessageException("Message is not auto-generated");
}
- Address[] from = message.getFrom();
+ Address[] from = getMandatory(Message::getFrom, message, signedMessage, "From");
+ if (from == null) {
+ throw new AcmeInvalidMessageException("Message has no 'From' header");
+ }
if (from.length != 1) {
- throw new AcmeProtocolException("Message must have exactly one sender, but has " + from.length);
+ throw new AcmeInvalidMessageException("Message must have exactly one sender, but has " + from.length);
+ }
+ if (validFromAddresses != null && !validFromAddresses.contains(from[0])) {
+ throw new AcmeInvalidMessageException("Sender '" + from[0] + "' was not found in signing certificate");
+ }
+ if (strict && signedMessage != null) {
+ Address[] outerFrom = message.getFrom();
+ if ((outerFrom.length > 1) || (outerFrom.length == 1 && outerFrom[0] != null
+ && !outerFrom[0].equals(from[0]))) {
+ throw new AcmeInvalidMessageException("Protected 'From' header does not match envelope header");
+ }
}
sender = new InternetAddress(from[0].toString());
- Address[] to = message.getRecipients(TO);
+ Address[] to = getMandatory(m -> m.getRecipients(TO), message, signedMessage, "To");
+ if (to == null) {
+ throw new AcmeInvalidMessageException("Message has no 'To' header");
+ }
if (to.length != 1) {
throw new AcmeProtocolException("Message must have exactly one recipient, but has " + to.length);
}
+ if (strict && signedMessage != null) {
+ Address[] outerTo = message.getRecipients(TO);
+ if ((outerTo.length > 1) || (outerTo.length == 1 && outerTo[0] != null
+ && !outerTo[0].equals(to[0]))) {
+ throw new AcmeInvalidMessageException("Protected 'To' header does not match envelope header");
+ }
+ }
recipient = new InternetAddress(to[0].toString());
- String subject = message.getSubject();
+ String subject = getMandatory(Message::getSubject, message, signedMessage, "Subject");
+ if (subject == null) {
+ throw new AcmeInvalidMessageException("Message has no 'Subject' header");
+ }
+ if (strict && signedMessage != null
+ && message.getSubject() != null
+ && !message.getSubject().equals(signedMessage.getSubject())) {
+ throw new AcmeInvalidMessageException("Protected 'Subject' header does not match envelope header");
+ }
Matcher m = SUBJECT_PATTERN.matcher(subject);
if (!m.matches()) {
throw new AcmeProtocolException("Invalid subject: " + subject);
@@ -99,7 +261,7 @@ public final class EmailProcessor {
// white spaces within the token part must be ignored
this.token1 = m.group(1).replaceAll("\\s+", "");
- Address[] rto = message.getReplyTo();
+ Address[] rto = getOptional(Message::getReplyTo, message, signedMessage);
if (rto != null) {
replyTo = Collections.unmodifiableList(Arrays.stream(rto)
.filter(InternetAddress.class::isInstance)
@@ -109,7 +271,7 @@ public final class EmailProcessor {
replyTo = Collections.emptyList();
}
- String[] mid = message.getHeader("Message-ID");
+ String[] mid = getOptional(n -> n.getHeader("Message-ID"), message, signedMessage);
if (mid != null && mid.length > 0) {
messageId = Optional.of(mid[0]);
} else {
@@ -297,16 +459,78 @@ public final class EmailProcessor {
return new ResponseGenerator(this);
}
+ /**
+ * Get an optional property from the message.
+ *
+ * Optional property means: If there is a signed message, try to fetch the property
+ * from there. If the property is not present, fetch it from the unsigned message
+ * instead. If it's also not there, return {@code null}.
+ *
+ * @param getter
+ * The getter method of {@link Message} to be invoked
+ * @param message
+ * The outer (unsigned) {@link Message} that serves as fallback
+ * @param signedMessage
+ * The signed (inner) {@link Message} where the property is looked up first
+ * @param
+ * The expected result type
+ * @return The mail property, or {@code null} if not found
+ */
+ @CheckForNull
+ private T getOptional(MessageFunction getter, Message message, @Nullable Message signedMessage)
+ throws MessagingException {
+ if (signedMessage != null) {
+ T result = getter.apply(signedMessage);
+ if (result != null) {
+ return result;
+ }
+ }
+ return getter.apply(message);
+ }
+
+ /**
+ * Get a mandatory property from the message.
+ *
+ * Mandatory means: If there is a signed message, the property must be
+ * present there. The unsigned message is only queried as fallback if there is no
+ * signed message at all.
+ *
+ * @param getter
+ * The getter method of {@link Message} to be invoked
+ * @param message
+ * The outer (unsigned) {@link Message} that serves as fallback if there is
+ * no signed message.
+ * @param signedMessage
+ * The signed (inner) {@link Message} where the property is expected, or
+ * {@code null} if there is no signed message.
+ * @param header
+ * Name of the expected header
+ * @param
+ * The expected result type
+ * @return The mail property, or {@code null} if not found
+ */
+ @CheckForNull
+ private T getMandatory(MessageFunction getter, Message message, @Nullable Message signedMessage, String header)
+ throws MessagingException, AcmeInvalidMessageException {
+ if (signedMessage != null) {
+ T value = getter.apply(signedMessage);
+ if (value == null) {
+ throw new AcmeInvalidMessageException("Protected header '" + header + "' expected, but missing.");
+ }
+ return value;
+ }
+ return getter.apply(message);
+ }
+
/**
* Checks if this message is "auto-generated".
*
- * @param message
- * Message to check.
+ * @param autoSubmitted
+ * Auto-Submitted header content
* @return {@code true} if the mail was auto-generated.
*/
- private boolean isAutoGenerated(Message message) throws MessagingException {
- String[] autoSubmitted = message.getHeader("Auto-Submitted");
- if (autoSubmitted == null) {
+ private boolean isAutoGenerated(@Nullable String[] autoSubmitted) throws MessagingException {
+ if (autoSubmitted == null || autoSubmitted.length == 0) {
return false;
}
return Arrays.stream(autoSubmitted)
@@ -323,4 +547,10 @@ public final class EmailProcessor {
}
}
+ @FunctionalInterface
+ private interface MessageFunction {
+ @CheckForNull
+ R apply(M message) throws MessagingException;
+ }
+
}
diff --git a/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/email/ResponseGenerator.java b/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/email/ResponseGenerator.java
index 6a939d5e..33529bd3 100644
--- a/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/email/ResponseGenerator.java
+++ b/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/email/ResponseGenerator.java
@@ -28,9 +28,8 @@ import jakarta.mail.internet.MimeMessage;
/**
* A helper for creating an email response to the "challenge" email.
*
- * According to RFC-8823, the response email must have a DKIM or S/MIME signature. This is
- * not done by the response generator, because it is usually performed by the
- * outbound MTA.
+ * According to RFC-8823, the response email must be DKIM signed. This is
+ * not done by the response generator, but must be done by the outbound MTA.
*
* @see RFC 8823
* @since 2.12
diff --git a/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/exception/AcmeInvalidMessageException.java b/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/exception/AcmeInvalidMessageException.java
new file mode 100644
index 00000000..145c8aa4
--- /dev/null
+++ b/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/exception/AcmeInvalidMessageException.java
@@ -0,0 +1,57 @@
+/*
+ * acme4j - Java ACME client
+ *
+ * Copyright (C) 2022 Richard "Shred" Körber
+ * http://acme4j.shredzone.org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ */
+package org.shredzone.acme4j.smime.exception;
+
+import org.shredzone.acme4j.exception.AcmeException;
+
+/**
+ * This exception is thrown when the challenge message is invalid.
+ *
+ * If this exception is thrown, the challenge message does not match the actual challenge,
+ * and must be rejected.
+ *
+ * Reasons may be:
+ *
+ * - Unexpected sender address
+ * - Bad S/MIME signature
+ *
+ *
+ * @since 2.15
+ */
+public class AcmeInvalidMessageException extends AcmeException {
+ private static final long serialVersionUID = 5607857024718309330L;
+
+ /**
+ * Creates a new {@link AcmeInvalidMessageException}.
+ *
+ * @param msg
+ * Reason of the exception
+ */
+ public AcmeInvalidMessageException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Creates a new {@link AcmeInvalidMessageException}.
+ *
+ * @param msg
+ * Reason of the exception
+ * @param cause
+ * Cause
+ */
+ public AcmeInvalidMessageException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
+}
diff --git a/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/exception/package-info.java b/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/exception/package-info.java
new file mode 100644
index 00000000..79dc0c40
--- /dev/null
+++ b/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/exception/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * acme4j - Java ACME client
+ *
+ * Copyright (C) 2022 Richard "Shred" Körber
+ * http://acme4j.shredzone.org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+@ReturnValuesAreNonnullByDefault
+@DefaultAnnotationForParameters(NonNull.class)
+@DefaultAnnotationForFields(NonNull.class)
+package org.shredzone.acme4j.smime.exception;
+
+import edu.umd.cs.findbugs.annotations.DefaultAnnotationForFields;
+import edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;
diff --git a/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/SMIMETests.java b/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/SMIMETests.java
index 72a92605..3fc25443 100644
--- a/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/SMIMETests.java
+++ b/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/SMIMETests.java
@@ -23,6 +23,9 @@ import java.io.Reader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
import java.util.Properties;
import jakarta.mail.Message;
@@ -125,4 +128,20 @@ public abstract class SMIMETests {
}
}
+ /**
+ * Reads a certificate from the given resource.
+ *
+ * @param name
+ * Resource name of the certificate
+ * @return X509Certificate that was read
+ */
+ protected X509Certificate readCertificate(String name) throws IOException {
+ try (InputStream in = SMIMETests.class.getResourceAsStream("/" + name + ".pem")) {
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ return (X509Certificate) cf.generateCertificate(in);
+ } catch (CertificateException ex) {
+ throw new IOException(ex);
+ }
+ }
+
}
diff --git a/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/email/EmailProcessorTest.java b/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/email/EmailProcessorTest.java
index 5d3f5ff9..fd183aa6 100644
--- a/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/email/EmailProcessorTest.java
+++ b/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/email/EmailProcessorTest.java
@@ -14,21 +14,27 @@
package org.shredzone.acme4j.smime.email;
import static jakarta.mail.Message.RecipientType.TO;
-import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
+import java.security.Security;
+import java.security.cert.X509Certificate;
import java.util.Optional;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.InternetAddress;
+import jakarta.mail.internet.MimeMessage;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.shredzone.acme4j.Identifier;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.smime.EmailIdentifier;
import org.shredzone.acme4j.smime.SMIMETests;
import org.shredzone.acme4j.smime.challenge.EmailReply00Challenge;
+import org.shredzone.acme4j.smime.exception.AcmeInvalidMessageException;
/**
* Unit tests for {@link EmailProcessor} and {@link ResponseGenerator}.
@@ -40,9 +46,14 @@ public class EmailProcessorTest extends SMIMETests {
private final InternetAddress expectedReplyTo = email("acme-validator@example.org");
private final Message message = mockMessage("challenge");
+ @BeforeAll
+ public static void setup() {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
@Test
- public void testEmailParser() throws MessagingException {
- EmailProcessor processor = new EmailProcessor(message);
+ public void testEmailParser() throws AcmeInvalidMessageException {
+ EmailProcessor processor = EmailProcessor.plainMessage(message);
processor.expectedFrom(expectedFrom);
processor.expectedTo(expectedTo);
processor.expectedIdentifier(EmailIdentifier.email(expectedTo));
@@ -55,10 +66,93 @@ public class EmailProcessorTest extends SMIMETests {
assertThat(processor.getReplyTo()).contains(email("acme-validator@example.org"));
}
+ @Test
+ public void testValidSignature() throws AcmeInvalidMessageException, IOException {
+ MimeMessage message = (MimeMessage) mockMessage("valid-mail");
+ X509Certificate certificate = readCertificate("valid-signer");
+ EmailProcessor processor = EmailProcessor.smimeMessage(message, mailSession, certificate, true);
+ }
+
+ @Test
+ public void testInvalidSignature() {
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(() -> {
+ MimeMessage message = (MimeMessage) mockMessage("invalid-signed-mail");
+ X509Certificate certificate = readCertificate("valid-signer");
+ EmailProcessor processor = EmailProcessor.smimeMessage(message, mailSession, certificate, true);
+ })
+ .withMessage("The S/MIME signature is invalid");
+ }
+
+ @Test
+ public void testValidSignatureButNoSAN() {
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(() -> {
+ MimeMessage message = (MimeMessage) mockMessage("invalid-nosan");
+ X509Certificate certificate = readCertificate("valid-signer-nosan");
+ EmailProcessor processor = EmailProcessor.smimeMessage(message, mailSession, certificate, true);
+ })
+ .withMessage("Signing certificate does not provide a rfc822Name subjectAltName");
+ }
+
+ @Test
+ public void testSANDoesNotMatchFrom() {
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(() -> {
+ MimeMessage message = (MimeMessage) mockMessage("invalid-cert-mismatch");
+ X509Certificate certificate = readCertificate("valid-signer");
+ EmailProcessor processor = EmailProcessor.smimeMessage(message, mailSession, certificate, true);
+ })
+ .withMessage("Sender 'different-ca@example.com' was not found in signing certificate");
+ }
+
+ @Test
+ public void testInvalidProtectedFromHeader() {
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(() -> {
+ MimeMessage message = (MimeMessage) mockMessage("invalid-protected-mail-from");
+ X509Certificate certificate = readCertificate("valid-signer");
+ EmailProcessor processor = EmailProcessor.smimeMessage(message, mailSession, certificate, true);
+ })
+ .withMessage("Protected 'From' header does not match envelope header");
+ }
+
+ @Test
+ public void testInvalidProtectedToHeader() {
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(() -> {
+ MimeMessage message = (MimeMessage) mockMessage("invalid-protected-mail-to");
+ X509Certificate certificate = readCertificate("valid-signer");
+ EmailProcessor processor = EmailProcessor.smimeMessage(message, mailSession, certificate, true);
+ })
+ .withMessage("Protected 'To' header does not match envelope header");
+ }
+
+ @Test
+ public void testInvalidProtectedSubjectHeader() {
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(() -> {
+ MimeMessage message = (MimeMessage) mockMessage("invalid-protected-mail-subject");
+ X509Certificate certificate = readCertificate("valid-signer");
+ EmailProcessor processor = EmailProcessor.smimeMessage(message, mailSession, certificate, true);
+ })
+ .withMessage("Protected 'Subject' header does not match envelope header");
+ }
+
+ @Test
+ public void testNonStrictInvalidProtectedSubjectHeader() {
+ assertThatNoException()
+ .isThrownBy(() -> {
+ MimeMessage message = (MimeMessage) mockMessage("invalid-protected-mail-subject");
+ X509Certificate certificate = readCertificate("valid-signer");
+ EmailProcessor processor = EmailProcessor.smimeMessage(message, mailSession, certificate, false);
+ });
+ }
+
@Test
public void textExpectedFromFails() {
assertThrows(AcmeProtocolException.class, () -> {
- EmailProcessor processor = new EmailProcessor(message);
+ EmailProcessor processor = EmailProcessor.plainMessage(message);
processor.expectedFrom(expectedTo);
});
}
@@ -66,7 +160,7 @@ public class EmailProcessorTest extends SMIMETests {
@Test
public void textExpectedToFails() {
assertThrows(AcmeProtocolException.class, () -> {
- EmailProcessor processor = new EmailProcessor(message);
+ EmailProcessor processor = EmailProcessor.plainMessage(message);
processor.expectedTo(expectedFrom);
});
}
@@ -74,7 +168,7 @@ public class EmailProcessorTest extends SMIMETests {
@Test
public void textExpectedIdentifierFails1() {
assertThrows(AcmeProtocolException.class, () -> {
- EmailProcessor processor = new EmailProcessor(message);
+ EmailProcessor processor = EmailProcessor.plainMessage(message);
processor.expectedIdentifier(EmailIdentifier.email(expectedFrom));
});
}
@@ -82,7 +176,7 @@ public class EmailProcessorTest extends SMIMETests {
@Test
public void textExpectedIdentifierFails2() {
assertThrows(AcmeProtocolException.class, () -> {
- EmailProcessor processor = new EmailProcessor(message);
+ EmailProcessor processor = EmailProcessor.plainMessage(message);
processor.expectedIdentifier(Identifier.ip("192.168.0.1"));
});
}
@@ -90,7 +184,7 @@ public class EmailProcessorTest extends SMIMETests {
@Test
public void textNoChallengeFails1() {
assertThrows(IllegalStateException.class, () -> {
- EmailProcessor processor = new EmailProcessor(message);
+ EmailProcessor processor = EmailProcessor.plainMessage(message);
processor.getToken();
});
}
@@ -98,7 +192,7 @@ public class EmailProcessorTest extends SMIMETests {
@Test
public void textNoChallengeFails2() {
assertThrows(IllegalStateException.class, () -> {
- EmailProcessor processor = new EmailProcessor(message);
+ EmailProcessor processor = EmailProcessor.plainMessage(message);
processor.getAuthorization();
});
}
@@ -106,16 +200,16 @@ public class EmailProcessorTest extends SMIMETests {
@Test
public void textNoChallengeFails3() {
assertThrows(IllegalStateException.class, () -> {
- EmailProcessor processor = new EmailProcessor(message);
+ EmailProcessor processor = EmailProcessor.plainMessage(message);
processor.respond();
});
}
@Test
- public void testChallenge() {
+ public void testChallenge() throws AcmeInvalidMessageException {
EmailReply00Challenge challenge = mockChallenge("emailReplyChallenge");
- EmailProcessor processor = new EmailProcessor(message);
+ EmailProcessor processor = EmailProcessor.plainMessage(message);
processor.withChallenge(challenge);
assertThat(processor.getToken()).isEqualTo(TOKEN);
assertThat(processor.getAuthorization()).isEqualTo(KEY_AUTHORIZATION);
@@ -126,16 +220,16 @@ public class EmailProcessorTest extends SMIMETests {
public void testChallengeMismatch() {
assertThrows(AcmeProtocolException.class, () -> {
EmailReply00Challenge challenge = mockChallenge("emailReplyChallengeMismatch");
- EmailProcessor processor = new EmailProcessor(message);
+ EmailProcessor processor = EmailProcessor.plainMessage(message);
processor.withChallenge(challenge);
});
}
@Test
- public void testResponse() throws IOException, MessagingException {
+ public void testResponse() throws IOException, MessagingException, AcmeInvalidMessageException {
EmailReply00Challenge challenge = mockChallenge("emailReplyChallenge");
- Message response = new EmailProcessor(message)
+ Message response = EmailProcessor.plainMessage(message)
.withChallenge(challenge)
.respond()
.generateResponse(mailSession);
@@ -144,10 +238,10 @@ public class EmailProcessorTest extends SMIMETests {
}
@Test
- public void testResponseWithHeaderFooter() throws IOException, MessagingException {
+ public void testResponseWithHeaderFooter() throws IOException, MessagingException, AcmeInvalidMessageException {
EmailReply00Challenge challenge = mockChallenge("emailReplyChallenge");
- Message response = new EmailProcessor(message)
+ Message response = EmailProcessor.plainMessage(message)
.withChallenge(challenge)
.respond()
.withHeader("This is an introduction.")
@@ -161,10 +255,10 @@ public class EmailProcessorTest extends SMIMETests {
}
@Test
- public void testResponseWithCallback() throws IOException, MessagingException {
+ public void testResponseWithCallback() throws IOException, MessagingException, AcmeInvalidMessageException {
EmailReply00Challenge challenge = mockChallenge("emailReplyChallenge");
- Message response = new EmailProcessor(message)
+ Message response = EmailProcessor.plainMessage(message)
.withChallenge(challenge)
.respond()
.withGenerator((msg, body) -> msg.setContent("Head\r\n" + body + "Foot", "text/plain"))
diff --git a/acme4j-smime/src/test/resources/email/invalid-cert-mismatch.eml b/acme4j-smime/src/test/resources/email/invalid-cert-mismatch.eml
new file mode 100644
index 00000000..80ec19e7
--- /dev/null
+++ b/acme4j-smime/src/test/resources/email/invalid-cert-mismatch.eml
@@ -0,0 +1,67 @@
+From: different-ca@example.org
+To: recipient@example.org
+Subject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=
+Auto-Submitted: auto-generated; type=acme
+Message-ID:
+MIME-Version: 1.0
+Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha1"; boundary="----6B6A5C5DBC60D7D16B6C08BF092D4185"
+
+This is an S/MIME signed message
+
+------6B6A5C5DBC60D7D16B6C08BF092D4185
+Content-Type: message/RFC822; forwarded=no
+
+From: different-ca@example.com
+To: recipient@example.org
+Subject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=
+Message-ID:
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+
+This is an automatically generated ACME challenge.
+
+------6B6A5C5DBC60D7D16B6C08BF092D4185
+Content-Type: application/x-pkcs7-signature; name="smime.p7s"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename="smime.p7s"
+
+MIIGzQYJKoZIhvcNAQcCoIIGvjCCBroCAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3
+DQEHAaCCA/4wggP6MIIC4qADAgECAhQoC/xUcLhcK13sGSiYxuUPf758tDANBgkq
+hkiG9w0BAQsFADB8MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBDaXR5MR4w
+HAYDVQQKDBVBY21lIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4YW1wbGUu
+Y29tMSMwIQYJKoZIhvcNAQkBFhR2YWxpZC1jYUBleGFtcGxlLmNvbTAeFw0yMjEx
+MDQxMjU1MzlaFw0zMjExMDExMjU1MzlaMHwxCzAJBgNVBAYTAlhYMRIwEAYDVQQH
+DAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNhdGVzIEx0ZDEUMBIG
+A1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZhbGlkLWNhQGV4YW1w
+bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzTyW1VRm+mQH
+YU5hZWgzgrPRkRzMLlLoCamlRs5DjGf3zIpo9a/17m60YfIXBJruImUBXxa5lp0Z
+qaayty+nZOpS0wBZSqTLuslZ0WuyyyW2DEBNO7jLW58cMn8MfAwMYjDcxtubNb7M
+AG9iZRj6wn6tKCsXtYUgAIpNhyPPtDuEZ5df1ecOnvlW2vO+MwytM8DLLtwolET/
+tPzOXPHDiyKjij02jyJ1DlZxptKudiKaBeY1WY/W/PpS6fGskTJQc/bZPgE3OP/9
+Y9Y1uzZTulkO3R6MlLNdLam32/ehpJvSWyfSbToyC2ejvaXoRChPFcAmTpqA0HPg
+h8sudiS14QIDAQABo3QwcjAdBgNVHQ4EFgQUR0gRYoOCPJYqFiPGwB141VTCXuIw
+HwYDVR0jBBgwFoAUR0gRYoOCPJYqFiPGwB141VTCXuIwDwYDVR0TAQH/BAUwAwEB
+/zAfBgNVHREEGDAWgRR2YWxpZC1jYUBleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsF
+AAOCAQEAJDQsNTWxVLNHBrVl6p6IetzeBt64GoWp6OP5C8/HY3dlznfbrn6ZvWBr
+Vsnc7VZ49r8r/QvRKRrljkIQuNwaW/LmRxJ1AVGGiorspmrdz0Lf2WOXnLH/+4lR
+q5dar5YmGqi9Mo2j5ALg/2MiO1PonuKs1eX9tLZCCeanXFexU0qaeFZelunJ6UUm
+BXpaGO1QfECcNnvarudosbe1ve6YGABn8MpAY+8zdYnYHPB8Pojhzvk7/8PMHoPu
+njDb3lZT+b8BDmNz+GISCUSHYkdK2rWh+8wqD3T5rOtzTqAPuSNeDNocUF6wzxem
+HWqvP7yM3VoXYvn7FA4NCa9mE3k8MzGCApcwggKTAgEBMIGUMHwxCzAJBgNVBAYT
+AlhYMRIwEAYDVQQHDAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNh
+dGVzIEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZh
+bGlkLWNhQGV4YW1wbGUuY29tAhQoC/xUcLhcK13sGSiYxuUPf758tDAJBgUrDgMC
+GgUAoIHYMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X
+DTIyMTEwNDEzMzE1NVowIwYJKoZIhvcNAQkEMRYEFNE5fZJFFgePdLTkBjs3GMcD
+M5UyMHkGCSqGSIb3DQEJDzFsMGowCwYJYIZIAWUDBAEqMAsGCWCGSAFlAwQBFjAL
+BglghkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3
+DQMCAgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIIB
+AEN+8Cgvfw/72HPSSgJROYqScFrYTCRF7HTAk22zJCVzuh21rbEiPGLJ9Sy4ak/i
+BXvXtkX8YJBGuVYrx6QxoF8vmcwlIPVw9Qoc2FevyfRQD19hP7rd7miZ8LWu0B8v
+d54mr7aD5zQADLvQGlxjKvCSM3F8HF1KQrRZrfJbEL9NRrgYD7c8ZEAvisaoEfPO
+vRjsj9IzKg/RWhOAmh5n591ZNKVb8k0G+5lyCSuP8m/9k0sE705sVrq4sbgIgtFB
+HNVLwYvxb88F//rFosFW/njsnlgFx5hjpLbwKu6Du2Sd7L1oye/xUGlKDY8yuy2m
+pwKapV2IpD8TjsL7NZ7rJ5U=
+
+------6B6A5C5DBC60D7D16B6C08BF092D4185--
+
diff --git a/acme4j-smime/src/test/resources/email/invalid-nosan.eml b/acme4j-smime/src/test/resources/email/invalid-nosan.eml
new file mode 100644
index 00000000..ed879c66
--- /dev/null
+++ b/acme4j-smime/src/test/resources/email/invalid-nosan.eml
@@ -0,0 +1,66 @@
+From: valid-ca@example.com
+To: recipient@example.org
+Subject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=
+Auto-Submitted: auto-generated; type=acme
+Message-ID:
+MIME-Version: 1.0
+Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha1"; boundary="----47B1F5074B8F7A13042C44F61463F58F"
+
+This is an S/MIME signed message
+
+------47B1F5074B8F7A13042C44F61463F58F
+Content-Type: message/RFC822; forwarded=no
+
+From: valid-ca@example.com
+To: recipient@example.org
+Subject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=
+Message-ID:
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+
+This is an automatically generated ACME challenge.
+
+------47B1F5074B8F7A13042C44F61463F58F
+Content-Type: application/x-pkcs7-signature; name="smime.p7s"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename="smime.p7s"
+
+MIIGrAYJKoZIhvcNAQcCoIIGnTCCBpkCAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3
+DQEHAaCCA90wggPZMIICwaADAgECAhRPShCzW2lh0D89RfDJ5mos/gMAJjANBgkq
+hkiG9w0BAQsFADB8MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBDaXR5MR4w
+HAYDVQQKDBVBY21lIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4YW1wbGUu
+Y29tMSMwIQYJKoZIhvcNAQkBFhR2YWxpZC1jYUBleGFtcGxlLmNvbTAeFw0yMjEx
+MDQxMjU1MzlaFw0zMjExMDExMjU1MzlaMHwxCzAJBgNVBAYTAlhYMRIwEAYDVQQH
+DAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNhdGVzIEx0ZDEUMBIG
+A1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZhbGlkLWNhQGV4YW1w
+bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2BDJcOmFx6Tu
+W58rpO4FXbvF7ZoSpIOi4YWfSmD7LSHUq+xGx/vodijFT1Y6a74o4d+XeYTH/X4j
+FO9QPdzZDuG8t3ziFACctptaFywUjgYYJQGyPmfg3hn1Cz72q4tAqegOEwL78NA2
+YcEd6sx0udAF80C1QHi/kKBMDgj9AOyNyIZ/rBN8CZSkfkpPYWI99Fl/DOuYnr7k
+MN1TUWS1906mPqBslh1YVyp6fdGaL6DdlIY+ZE5c9BhST/t+7eLq7fnB5KB+tDvH
+D1qnL858K+5Hjfc9MUYTyffDiJaG9zHkEKi3zd0EGcaf1r+lskRqEIOqROjLSDim
+T5Q4CuHCYQIDAQABo1MwUTAdBgNVHQ4EFgQURoDGdRoZP6EOfLXTlNxWPDTRmp8w
+HwYDVR0jBBgwFoAURoDGdRoZP6EOfLXTlNxWPDTRmp8wDwYDVR0TAQH/BAUwAwEB
+/zANBgkqhkiG9w0BAQsFAAOCAQEAvVI9Yj8lL0cvdNY3RD/GQ/tQCGBAzoFTODJU
+wn4zE/LfiXfu8SxJhzcpjzKc2j+mxuwGh0OqraIO2FkpO23+X1gCdCt+ClE/6nMs
+8UMo4H1wMYYGhjkoLvsH9Ne5N+91PvLQG97LoLsoy+Y95ws23WyqUJ2g7A7Isk3v
+7MJZVH2d93hjbtWQ6+3/PP5zJwubEwiDAYvycODfvAig9+0QBIy+uE7XxnEhKxHJ
+pvN3p8NmLya7XH3v92N7M6CioyBqw8HL7I5lt5HBqa/U9USVMMmi9v+tFLZYyd7r
+7acw6hB7MDcLmtEu08Cgo89K23oTm1JBJZrjZUFbYcYP+fiuizGCApcwggKTAgEB
+MIGUMHwxCzAJBgNVBAYTAlhYMRIwEAYDVQQHDAlBY21lIENpdHkxHjAcBgNVBAoM
+FUFjbWUgQ2VydGlmaWNhdGVzIEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xIzAh
+BgkqhkiG9w0BCQEWFHZhbGlkLWNhQGV4YW1wbGUuY29tAhRPShCzW2lh0D89RfDJ
+5mos/gMAJjAJBgUrDgMCGgUAoIHYMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEw
+HAYJKoZIhvcNAQkFMQ8XDTIyMTEwNDEzMzE1NVowIwYJKoZIhvcNAQkEMRYEFK+s
+B97I9ZGpDGUMWLsE3dCU/pbtMHkGCSqGSIb3DQEJDzFsMGowCwYJYIZIAWUDBAEq
+MAsGCWCGSAFlAwQBFjALBglghkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYIKoZIhvcN
+AwICAgCAMA0GCCqGSIb3DQMCAgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMA0G
+CSqGSIb3DQEBAQUABIIBAEvS2tccMXu/OG8fzgOWsf5MzjBdDD/6doTLuIJS7Ktv
+1alpT8ZqSBn4jOgrlM7efTFK8y1vlOdoFZIsfIe+92lclvgHc/2Dw4XB5SswZ59y
+fZH+AVtVpzi5oFYiunhBn19vRP9Bri4ma8gCRe7pUwN15Gap8gl3+UQtUY17wcME
+H7ALcuG0ETPTxz9p2ueN6FmrthmrDSaZVqW5nyTizgr0zSxicEcwfFz9JZGZFKyp
+lPSVrgCwZ1/yaWXlnBXPTdO/DmAvNUAjUk0HZFu+mnelzPPs3c5s4LY3pBNjDPE2
+i2hxgRjca2QMsveYdwZn8I/m1P7yatJ3EozHpO3T4Gc=
+
+------47B1F5074B8F7A13042C44F61463F58F--
+
diff --git a/acme4j-smime/src/test/resources/email/invalid-protected-mail-from.eml b/acme4j-smime/src/test/resources/email/invalid-protected-mail-from.eml
new file mode 100644
index 00000000..3b0ace9f
--- /dev/null
+++ b/acme4j-smime/src/test/resources/email/invalid-protected-mail-from.eml
@@ -0,0 +1,67 @@
+From: tampered-ca@example.org
+To: recipient@example.org
+Subject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=
+Auto-Submitted: auto-generated; type=acme
+Message-ID:
+MIME-Version: 1.0
+Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha1"; boundary="----1FD9CF28CC0AD72EF1FF6D0511838F0E"
+
+This is an S/MIME signed message
+
+------1FD9CF28CC0AD72EF1FF6D0511838F0E
+Content-Type: message/RFC822; forwarded=no
+
+From: valid-ca@example.com
+To: recipient@example.org
+Subject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=
+Message-ID:
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+
+This is an automatically generated ACME challenge.
+
+------1FD9CF28CC0AD72EF1FF6D0511838F0E
+Content-Type: application/x-pkcs7-signature; name="smime.p7s"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename="smime.p7s"
+
+MIIGzQYJKoZIhvcNAQcCoIIGvjCCBroCAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3
+DQEHAaCCA/4wggP6MIIC4qADAgECAhQoC/xUcLhcK13sGSiYxuUPf758tDANBgkq
+hkiG9w0BAQsFADB8MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBDaXR5MR4w
+HAYDVQQKDBVBY21lIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4YW1wbGUu
+Y29tMSMwIQYJKoZIhvcNAQkBFhR2YWxpZC1jYUBleGFtcGxlLmNvbTAeFw0yMjEx
+MDQxMjU1MzlaFw0zMjExMDExMjU1MzlaMHwxCzAJBgNVBAYTAlhYMRIwEAYDVQQH
+DAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNhdGVzIEx0ZDEUMBIG
+A1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZhbGlkLWNhQGV4YW1w
+bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzTyW1VRm+mQH
+YU5hZWgzgrPRkRzMLlLoCamlRs5DjGf3zIpo9a/17m60YfIXBJruImUBXxa5lp0Z
+qaayty+nZOpS0wBZSqTLuslZ0WuyyyW2DEBNO7jLW58cMn8MfAwMYjDcxtubNb7M
+AG9iZRj6wn6tKCsXtYUgAIpNhyPPtDuEZ5df1ecOnvlW2vO+MwytM8DLLtwolET/
+tPzOXPHDiyKjij02jyJ1DlZxptKudiKaBeY1WY/W/PpS6fGskTJQc/bZPgE3OP/9
+Y9Y1uzZTulkO3R6MlLNdLam32/ehpJvSWyfSbToyC2ejvaXoRChPFcAmTpqA0HPg
+h8sudiS14QIDAQABo3QwcjAdBgNVHQ4EFgQUR0gRYoOCPJYqFiPGwB141VTCXuIw
+HwYDVR0jBBgwFoAUR0gRYoOCPJYqFiPGwB141VTCXuIwDwYDVR0TAQH/BAUwAwEB
+/zAfBgNVHREEGDAWgRR2YWxpZC1jYUBleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsF
+AAOCAQEAJDQsNTWxVLNHBrVl6p6IetzeBt64GoWp6OP5C8/HY3dlznfbrn6ZvWBr
+Vsnc7VZ49r8r/QvRKRrljkIQuNwaW/LmRxJ1AVGGiorspmrdz0Lf2WOXnLH/+4lR
+q5dar5YmGqi9Mo2j5ALg/2MiO1PonuKs1eX9tLZCCeanXFexU0qaeFZelunJ6UUm
+BXpaGO1QfECcNnvarudosbe1ve6YGABn8MpAY+8zdYnYHPB8Pojhzvk7/8PMHoPu
+njDb3lZT+b8BDmNz+GISCUSHYkdK2rWh+8wqD3T5rOtzTqAPuSNeDNocUF6wzxem
+HWqvP7yM3VoXYvn7FA4NCa9mE3k8MzGCApcwggKTAgEBMIGUMHwxCzAJBgNVBAYT
+AlhYMRIwEAYDVQQHDAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNh
+dGVzIEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZh
+bGlkLWNhQGV4YW1wbGUuY29tAhQoC/xUcLhcK13sGSiYxuUPf758tDAJBgUrDgMC
+GgUAoIHYMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X
+DTIyMTEwNDEzMzE1NVowIwYJKoZIhvcNAQkEMRYEFK+sB97I9ZGpDGUMWLsE3dCU
+/pbtMHkGCSqGSIb3DQEJDzFsMGowCwYJYIZIAWUDBAEqMAsGCWCGSAFlAwQBFjAL
+BglghkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3
+DQMCAgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIIB
+AFli04jnSd0d0lnE9GO23qzxI98QIbCz1NU1qk+zDyAhwOWQJdMUfuekk3g4Gn2q
+OFzdvlIMpgG2W7AVZlLUQMQjWIoDWkTeqxM8n6StoXcDvlArBHQDbourufFUu7OE
+3TVsI6l/jZG3Xub9Uar0S3lF6rQ/A3vl28poRL/EIQye6ypg6UbS/EvkvfbsKUJD
+6SpExlwh0R7lk1g/xn3tFVSEAH7VSJYr/8C/Bak06NPOtZZSSeU9ryRzeK/gN3SJ
+nrEp+NkTezzApjSnZasrPSbzmRL4+18x3kmAmnwR3aRi7F7KhCs78qPUEVP5XbhX
+2hO9RjMy6Uki+R/AG4aempk=
+
+------1FD9CF28CC0AD72EF1FF6D0511838F0E--
+
diff --git a/acme4j-smime/src/test/resources/email/invalid-protected-mail-subject.eml b/acme4j-smime/src/test/resources/email/invalid-protected-mail-subject.eml
new file mode 100644
index 00000000..f95bbcb1
--- /dev/null
+++ b/acme4j-smime/src/test/resources/email/invalid-protected-mail-subject.eml
@@ -0,0 +1,67 @@
+From: valid-ca@example.com
+To: recipient@example.org
+Subject: ACME: aDiFfErEnTtOkEn
+Auto-Submitted: auto-generated; type=acme
+Message-ID:
+MIME-Version: 1.0
+Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha1"; boundary="----6E9953AAECB0BDB6F65BCD88900D3E15"
+
+This is an S/MIME signed message
+
+------6E9953AAECB0BDB6F65BCD88900D3E15
+Content-Type: message/RFC822; forwarded=no
+
+From: valid-ca@example.com
+To: recipient@example.org
+Subject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=
+Message-ID:
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+
+This is an automatically generated ACME challenge.
+
+------6E9953AAECB0BDB6F65BCD88900D3E15
+Content-Type: application/x-pkcs7-signature; name="smime.p7s"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename="smime.p7s"
+
+MIIGzQYJKoZIhvcNAQcCoIIGvjCCBroCAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3
+DQEHAaCCA/4wggP6MIIC4qADAgECAhQoC/xUcLhcK13sGSiYxuUPf758tDANBgkq
+hkiG9w0BAQsFADB8MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBDaXR5MR4w
+HAYDVQQKDBVBY21lIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4YW1wbGUu
+Y29tMSMwIQYJKoZIhvcNAQkBFhR2YWxpZC1jYUBleGFtcGxlLmNvbTAeFw0yMjEx
+MDQxMjU1MzlaFw0zMjExMDExMjU1MzlaMHwxCzAJBgNVBAYTAlhYMRIwEAYDVQQH
+DAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNhdGVzIEx0ZDEUMBIG
+A1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZhbGlkLWNhQGV4YW1w
+bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzTyW1VRm+mQH
+YU5hZWgzgrPRkRzMLlLoCamlRs5DjGf3zIpo9a/17m60YfIXBJruImUBXxa5lp0Z
+qaayty+nZOpS0wBZSqTLuslZ0WuyyyW2DEBNO7jLW58cMn8MfAwMYjDcxtubNb7M
+AG9iZRj6wn6tKCsXtYUgAIpNhyPPtDuEZ5df1ecOnvlW2vO+MwytM8DLLtwolET/
+tPzOXPHDiyKjij02jyJ1DlZxptKudiKaBeY1WY/W/PpS6fGskTJQc/bZPgE3OP/9
+Y9Y1uzZTulkO3R6MlLNdLam32/ehpJvSWyfSbToyC2ejvaXoRChPFcAmTpqA0HPg
+h8sudiS14QIDAQABo3QwcjAdBgNVHQ4EFgQUR0gRYoOCPJYqFiPGwB141VTCXuIw
+HwYDVR0jBBgwFoAUR0gRYoOCPJYqFiPGwB141VTCXuIwDwYDVR0TAQH/BAUwAwEB
+/zAfBgNVHREEGDAWgRR2YWxpZC1jYUBleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsF
+AAOCAQEAJDQsNTWxVLNHBrVl6p6IetzeBt64GoWp6OP5C8/HY3dlznfbrn6ZvWBr
+Vsnc7VZ49r8r/QvRKRrljkIQuNwaW/LmRxJ1AVGGiorspmrdz0Lf2WOXnLH/+4lR
+q5dar5YmGqi9Mo2j5ALg/2MiO1PonuKs1eX9tLZCCeanXFexU0qaeFZelunJ6UUm
+BXpaGO1QfECcNnvarudosbe1ve6YGABn8MpAY+8zdYnYHPB8Pojhzvk7/8PMHoPu
+njDb3lZT+b8BDmNz+GISCUSHYkdK2rWh+8wqD3T5rOtzTqAPuSNeDNocUF6wzxem
+HWqvP7yM3VoXYvn7FA4NCa9mE3k8MzGCApcwggKTAgEBMIGUMHwxCzAJBgNVBAYT
+AlhYMRIwEAYDVQQHDAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNh
+dGVzIEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZh
+bGlkLWNhQGV4YW1wbGUuY29tAhQoC/xUcLhcK13sGSiYxuUPf758tDAJBgUrDgMC
+GgUAoIHYMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X
+DTIyMTEwNDEzMzE1NVowIwYJKoZIhvcNAQkEMRYEFK+sB97I9ZGpDGUMWLsE3dCU
+/pbtMHkGCSqGSIb3DQEJDzFsMGowCwYJYIZIAWUDBAEqMAsGCWCGSAFlAwQBFjAL
+BglghkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3
+DQMCAgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIIB
+AFli04jnSd0d0lnE9GO23qzxI98QIbCz1NU1qk+zDyAhwOWQJdMUfuekk3g4Gn2q
+OFzdvlIMpgG2W7AVZlLUQMQjWIoDWkTeqxM8n6StoXcDvlArBHQDbourufFUu7OE
+3TVsI6l/jZG3Xub9Uar0S3lF6rQ/A3vl28poRL/EIQye6ypg6UbS/EvkvfbsKUJD
+6SpExlwh0R7lk1g/xn3tFVSEAH7VSJYr/8C/Bak06NPOtZZSSeU9ryRzeK/gN3SJ
+nrEp+NkTezzApjSnZasrPSbzmRL4+18x3kmAmnwR3aRi7F7KhCs78qPUEVP5XbhX
+2hO9RjMy6Uki+R/AG4aempk=
+
+------6E9953AAECB0BDB6F65BCD88900D3E15--
+
diff --git a/acme4j-smime/src/test/resources/email/invalid-protected-mail-to.eml b/acme4j-smime/src/test/resources/email/invalid-protected-mail-to.eml
new file mode 100644
index 00000000..46a474cb
--- /dev/null
+++ b/acme4j-smime/src/test/resources/email/invalid-protected-mail-to.eml
@@ -0,0 +1,67 @@
+From: valid-ca@example.com
+To: tampered-recipient@example.com
+Subject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=
+Auto-Submitted: auto-generated; type=acme
+Message-ID:
+MIME-Version: 1.0
+Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha1"; boundary="----2D5F3855936C8172B69EB7BC1C12A23A"
+
+This is an S/MIME signed message
+
+------2D5F3855936C8172B69EB7BC1C12A23A
+Content-Type: message/RFC822; forwarded=no
+
+From: valid-ca@example.com
+To: recipient@example.org
+Subject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=
+Message-ID:
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+
+This is an automatically generated ACME challenge.
+
+------2D5F3855936C8172B69EB7BC1C12A23A
+Content-Type: application/x-pkcs7-signature; name="smime.p7s"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename="smime.p7s"
+
+MIIGzQYJKoZIhvcNAQcCoIIGvjCCBroCAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3
+DQEHAaCCA/4wggP6MIIC4qADAgECAhQoC/xUcLhcK13sGSiYxuUPf758tDANBgkq
+hkiG9w0BAQsFADB8MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBDaXR5MR4w
+HAYDVQQKDBVBY21lIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4YW1wbGUu
+Y29tMSMwIQYJKoZIhvcNAQkBFhR2YWxpZC1jYUBleGFtcGxlLmNvbTAeFw0yMjEx
+MDQxMjU1MzlaFw0zMjExMDExMjU1MzlaMHwxCzAJBgNVBAYTAlhYMRIwEAYDVQQH
+DAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNhdGVzIEx0ZDEUMBIG
+A1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZhbGlkLWNhQGV4YW1w
+bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzTyW1VRm+mQH
+YU5hZWgzgrPRkRzMLlLoCamlRs5DjGf3zIpo9a/17m60YfIXBJruImUBXxa5lp0Z
+qaayty+nZOpS0wBZSqTLuslZ0WuyyyW2DEBNO7jLW58cMn8MfAwMYjDcxtubNb7M
+AG9iZRj6wn6tKCsXtYUgAIpNhyPPtDuEZ5df1ecOnvlW2vO+MwytM8DLLtwolET/
+tPzOXPHDiyKjij02jyJ1DlZxptKudiKaBeY1WY/W/PpS6fGskTJQc/bZPgE3OP/9
+Y9Y1uzZTulkO3R6MlLNdLam32/ehpJvSWyfSbToyC2ejvaXoRChPFcAmTpqA0HPg
+h8sudiS14QIDAQABo3QwcjAdBgNVHQ4EFgQUR0gRYoOCPJYqFiPGwB141VTCXuIw
+HwYDVR0jBBgwFoAUR0gRYoOCPJYqFiPGwB141VTCXuIwDwYDVR0TAQH/BAUwAwEB
+/zAfBgNVHREEGDAWgRR2YWxpZC1jYUBleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsF
+AAOCAQEAJDQsNTWxVLNHBrVl6p6IetzeBt64GoWp6OP5C8/HY3dlznfbrn6ZvWBr
+Vsnc7VZ49r8r/QvRKRrljkIQuNwaW/LmRxJ1AVGGiorspmrdz0Lf2WOXnLH/+4lR
+q5dar5YmGqi9Mo2j5ALg/2MiO1PonuKs1eX9tLZCCeanXFexU0qaeFZelunJ6UUm
+BXpaGO1QfECcNnvarudosbe1ve6YGABn8MpAY+8zdYnYHPB8Pojhzvk7/8PMHoPu
+njDb3lZT+b8BDmNz+GISCUSHYkdK2rWh+8wqD3T5rOtzTqAPuSNeDNocUF6wzxem
+HWqvP7yM3VoXYvn7FA4NCa9mE3k8MzGCApcwggKTAgEBMIGUMHwxCzAJBgNVBAYT
+AlhYMRIwEAYDVQQHDAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNh
+dGVzIEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZh
+bGlkLWNhQGV4YW1wbGUuY29tAhQoC/xUcLhcK13sGSiYxuUPf758tDAJBgUrDgMC
+GgUAoIHYMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X
+DTIyMTEwNDEzMzE1NVowIwYJKoZIhvcNAQkEMRYEFK+sB97I9ZGpDGUMWLsE3dCU
+/pbtMHkGCSqGSIb3DQEJDzFsMGowCwYJYIZIAWUDBAEqMAsGCWCGSAFlAwQBFjAL
+BglghkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3
+DQMCAgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIIB
+AFli04jnSd0d0lnE9GO23qzxI98QIbCz1NU1qk+zDyAhwOWQJdMUfuekk3g4Gn2q
+OFzdvlIMpgG2W7AVZlLUQMQjWIoDWkTeqxM8n6StoXcDvlArBHQDbourufFUu7OE
+3TVsI6l/jZG3Xub9Uar0S3lF6rQ/A3vl28poRL/EIQye6ypg6UbS/EvkvfbsKUJD
+6SpExlwh0R7lk1g/xn3tFVSEAH7VSJYr/8C/Bak06NPOtZZSSeU9ryRzeK/gN3SJ
+nrEp+NkTezzApjSnZasrPSbzmRL4+18x3kmAmnwR3aRi7F7KhCs78qPUEVP5XbhX
+2hO9RjMy6Uki+R/AG4aempk=
+
+------2D5F3855936C8172B69EB7BC1C12A23A--
+
diff --git a/acme4j-smime/src/test/resources/email/invalid-signed-mail.eml b/acme4j-smime/src/test/resources/email/invalid-signed-mail.eml
new file mode 100644
index 00000000..f4211d99
--- /dev/null
+++ b/acme4j-smime/src/test/resources/email/invalid-signed-mail.eml
@@ -0,0 +1,67 @@
+From: valid-ca@example.com
+To: recipient@example.org
+Subject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=
+Auto-Submitted: auto-generated; type=acme
+Message-ID:
+MIME-Version: 1.0
+Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha1"; boundary="----2D188458DC295B22904B7A1FB62F57BF"
+
+This is an S/MIME signed message
+
+------2D188458DC295B22904B7A1FB62F57BF
+Content-Type: message/RFC822; forwarded=no
+
+From: valid-ca@example.com
+To: recipient@example.org
+Subject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=
+Message-ID:
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+
+This is an automatically generated ACME challenge.
+
+------2D188458DC295B22904B7A1FB62F57BF
+Content-Type: application/x-pkcs7-signature; name="smime.p7s"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename="smime.p7s"
+
+MIIG1QYJKoZIhvcNAQcCoIIGxjCCBsICAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3
+DQEHAaCCBAQwggQAMIIC6KADAgECAhRuqrQwjQAEKPLiumz639inbqPeJzANBgkq
+hkiG9w0BAQsFADB+MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBDaXR5MR4w
+HAYDVQQKDBVFbWNhIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4YW1wbGUu
+Y29tMSUwIwYJKoZIhvcNAQkBFhZpbnZhbGlkLWNhQGV4YW1wbGUuY29tMB4XDTIy
+MTEwNDEyNTUzOVoXDTMyMTEwMTEyNTUzOVowfjELMAkGA1UEBhMCWFgxEjAQBgNV
+BAcMCUFjbWUgQ2l0eTEeMBwGA1UECgwVRW1jYSBDZXJ0aWZpY2F0ZXMgTHRkMRQw
+EgYDVQQDDAtleGFtcGxlLmNvbTElMCMGCSqGSIb3DQEJARYWaW52YWxpZC1jYUBl
+eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMxVUSNR
+pIreCKvtK148IWN7MWfK8fymmPOF8oQcFieCC0mfw8efc/4MEVA5qN3avHOXd1RG
+VgaR+tM30zRiTLllc6YnPePUPZNSQmJcnXgMlRhmOeCfo2hNglWFBnP/CV29xarP
+Cf94DqXGrLZ8L8uGtk/JsNOreced34V4RZ9WvN53HlyiNtEJJLggM17wzJZcV+rQ
+7LtsBHZfDOdTScCpEDqLZDmLMVLEBUtrwo5+5mYw4M0PDEP2D4qPux7NAHuaG66F
+Zt7mq6DcceG/AneuUN7xOyMQ9x/D3NfiSgXbZeJM+BbE0cT7EY9WdZBtsS6HjJA0
+yt98FgAIDoSzRQcCAwEAAaN2MHQwHQYDVR0OBBYEFB0Xb1ErzzEjzPrJC8PvkAJ3
+qnoGMB8GA1UdIwQYMBaAFB0Xb1ErzzEjzPrJC8PvkAJ3qnoGMA8GA1UdEwEB/wQF
+MAMBAf8wIQYDVR0RBBowGIEWaW52YWxpZC1jYUBleGFtcGxlLmNvbTANBgkqhkiG
+9w0BAQsFAAOCAQEAnwFh8H7lwMWrHSceM6MVt+5M0yVX/r6K5YWGI/AaFG2Q5jkz
+6yIeESgiXukza4oKY1I1clZwWus9fnrwn+AWbtvKbGLklFWCUB60fx82dZwoO14Q
+Tm3GX6wwC0Y5eFYiXwEJ4gnazBWEWscp4E94AKqr1EYuOI9sR22l/rNtANrEiVsT
+P4+kUgLEr9Y5infYglXQMjDVfNXRSETBnx4a5Fljd7pSD4e7H19eMiByd78q98ze
+t0g2anl2cJbdM6cgu5iyAgS3BgMrFMnd8m7KkZwum+tslNWA1tbDGK0AWH7ztjh3
+orifkxq2Hw6tdypZoWoLrwSEDEvNIy8+sQjUOTGCApkwggKVAgEBMIGWMH4xCzAJ
+BgNVBAYTAlhYMRIwEAYDVQQHDAlBY21lIENpdHkxHjAcBgNVBAoMFUVtY2EgQ2Vy
+dGlmaWNhdGVzIEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xJTAjBgkqhkiG9w0B
+CQEWFmludmFsaWQtY2FAZXhhbXBsZS5jb20CFG6qtDCNAAQo8uK6bPrf2Kduo94n
+MAkGBSsOAwIaBQCggdgwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG
+9w0BCQUxDxcNMjIxMTA0MTMzMTU1WjAjBgkqhkiG9w0BCQQxFgQUr6wH3sj1kakM
+ZQxYuwTd0JT+lu0weQYJKoZIhvcNAQkPMWwwajALBglghkgBZQMEASowCwYJYIZI
+AWUDBAEWMAsGCWCGSAFlAwQBAjAKBggqhkiG9w0DBzAOBggqhkiG9w0DAgICAIAw
+DQYIKoZIhvcNAwICAUAwBwYFKw4DAgcwDQYIKoZIhvcNAwICASgwDQYJKoZIhvcN
+AQEBBQAEggEAdZ4XoWViOrqOd4kWZVD+12GXJ7NcCWHOiTdXZ0S/3TKwwTPwQ0f0
+XZmk8iJwcfXqAv48ITK+yh4jk8urtrjS3xohHGxVonifbgxoLm/yHqA13D4F0M9q
+r0jfmXAWfH1HDk6AtZA5c1IJVJNMcVcLHkib232FgpicgPEZNWvRr8zHCNN3dymF
+Qme9h2BqHxy2+nX96BBEiRMImG9Z9G4+sOqpwiDTNgzr7nFtldKtjiV/GarBeteZ
+nx4QcHXY307ydLDrh2JzLK/+LZEhTMBQ2rWGfAo/owDGi/Pal//SgqQWFy3luJp2
+8WG34Z4CnG514YmRzN2Z1V3fvreRInllkQ==
+
+------2D188458DC295B22904B7A1FB62F57BF--
+
diff --git a/acme4j-smime/src/test/resources/email/valid-mail.eml b/acme4j-smime/src/test/resources/email/valid-mail.eml
new file mode 100644
index 00000000..80e60279
--- /dev/null
+++ b/acme4j-smime/src/test/resources/email/valid-mail.eml
@@ -0,0 +1,67 @@
+From: valid-ca@example.com
+To: recipient@example.org
+Subject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=
+Auto-Submitted: auto-generated; type=acme
+Message-ID:
+MIME-Version: 1.0
+Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha1"; boundary="----163CF1BA3ECF9F288779BFBE9EF3E10C"
+
+This is an S/MIME signed message
+
+------163CF1BA3ECF9F288779BFBE9EF3E10C
+Content-Type: message/RFC822; forwarded=no
+
+From: valid-ca@example.com
+To: recipient@example.org
+Subject: ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=
+Message-ID:
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+
+This is an automatically generated ACME challenge.
+
+------163CF1BA3ECF9F288779BFBE9EF3E10C
+Content-Type: application/x-pkcs7-signature; name="smime.p7s"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename="smime.p7s"
+
+MIIGzQYJKoZIhvcNAQcCoIIGvjCCBroCAQExCzAJBgUrDgMCGgUAMAsGCSqGSIb3
+DQEHAaCCA/4wggP6MIIC4qADAgECAhQoC/xUcLhcK13sGSiYxuUPf758tDANBgkq
+hkiG9w0BAQsFADB8MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBDaXR5MR4w
+HAYDVQQKDBVBY21lIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4YW1wbGUu
+Y29tMSMwIQYJKoZIhvcNAQkBFhR2YWxpZC1jYUBleGFtcGxlLmNvbTAeFw0yMjEx
+MDQxMjU1MzlaFw0zMjExMDExMjU1MzlaMHwxCzAJBgNVBAYTAlhYMRIwEAYDVQQH
+DAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNhdGVzIEx0ZDEUMBIG
+A1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZhbGlkLWNhQGV4YW1w
+bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzTyW1VRm+mQH
+YU5hZWgzgrPRkRzMLlLoCamlRs5DjGf3zIpo9a/17m60YfIXBJruImUBXxa5lp0Z
+qaayty+nZOpS0wBZSqTLuslZ0WuyyyW2DEBNO7jLW58cMn8MfAwMYjDcxtubNb7M
+AG9iZRj6wn6tKCsXtYUgAIpNhyPPtDuEZ5df1ecOnvlW2vO+MwytM8DLLtwolET/
+tPzOXPHDiyKjij02jyJ1DlZxptKudiKaBeY1WY/W/PpS6fGskTJQc/bZPgE3OP/9
+Y9Y1uzZTulkO3R6MlLNdLam32/ehpJvSWyfSbToyC2ejvaXoRChPFcAmTpqA0HPg
+h8sudiS14QIDAQABo3QwcjAdBgNVHQ4EFgQUR0gRYoOCPJYqFiPGwB141VTCXuIw
+HwYDVR0jBBgwFoAUR0gRYoOCPJYqFiPGwB141VTCXuIwDwYDVR0TAQH/BAUwAwEB
+/zAfBgNVHREEGDAWgRR2YWxpZC1jYUBleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsF
+AAOCAQEAJDQsNTWxVLNHBrVl6p6IetzeBt64GoWp6OP5C8/HY3dlznfbrn6ZvWBr
+Vsnc7VZ49r8r/QvRKRrljkIQuNwaW/LmRxJ1AVGGiorspmrdz0Lf2WOXnLH/+4lR
+q5dar5YmGqi9Mo2j5ALg/2MiO1PonuKs1eX9tLZCCeanXFexU0qaeFZelunJ6UUm
+BXpaGO1QfECcNnvarudosbe1ve6YGABn8MpAY+8zdYnYHPB8Pojhzvk7/8PMHoPu
+njDb3lZT+b8BDmNz+GISCUSHYkdK2rWh+8wqD3T5rOtzTqAPuSNeDNocUF6wzxem
+HWqvP7yM3VoXYvn7FA4NCa9mE3k8MzGCApcwggKTAgEBMIGUMHwxCzAJBgNVBAYT
+AlhYMRIwEAYDVQQHDAlBY21lIENpdHkxHjAcBgNVBAoMFUFjbWUgQ2VydGlmaWNh
+dGVzIEx0ZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xIzAhBgkqhkiG9w0BCQEWFHZh
+bGlkLWNhQGV4YW1wbGUuY29tAhQoC/xUcLhcK13sGSiYxuUPf758tDAJBgUrDgMC
+GgUAoIHYMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X
+DTIyMTEwNDEzMzE1NVowIwYJKoZIhvcNAQkEMRYEFK+sB97I9ZGpDGUMWLsE3dCU
+/pbtMHkGCSqGSIb3DQEJDzFsMGowCwYJYIZIAWUDBAEqMAsGCWCGSAFlAwQBFjAL
+BglghkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3
+DQMCAgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIIB
+AFli04jnSd0d0lnE9GO23qzxI98QIbCz1NU1qk+zDyAhwOWQJdMUfuekk3g4Gn2q
+OFzdvlIMpgG2W7AVZlLUQMQjWIoDWkTeqxM8n6StoXcDvlArBHQDbourufFUu7OE
+3TVsI6l/jZG3Xub9Uar0S3lF6rQ/A3vl28poRL/EIQye6ypg6UbS/EvkvfbsKUJD
+6SpExlwh0R7lk1g/xn3tFVSEAH7VSJYr/8C/Bak06NPOtZZSSeU9ryRzeK/gN3SJ
+nrEp+NkTezzApjSnZasrPSbzmRL4+18x3kmAmnwR3aRi7F7KhCs78qPUEVP5XbhX
+2hO9RjMy6Uki+R/AG4aempk=
+
+------163CF1BA3ECF9F288779BFBE9EF3E10C--
+
diff --git a/acme4j-smime/src/test/resources/invalid-signer-privkey.pem b/acme4j-smime/src/test/resources/invalid-signer-privkey.pem
new file mode 100644
index 00000000..feb50348
--- /dev/null
+++ b/acme4j-smime/src/test/resources/invalid-signer-privkey.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDMVVEjUaSK3gir
+7StePCFjezFnyvH8ppjzhfKEHBYnggtJn8PHn3P+DBFQOajd2rxzl3dURlYGkfrT
+N9M0Yky5ZXOmJz3j1D2TUkJiXJ14DJUYZjngn6NoTYJVhQZz/wldvcWqzwn/eA6l
+xqy2fC/LhrZPybDTq3nHnd+FeEWfVrzedx5cojbRCSS4IDNe8MyWXFfq0Oy7bAR2
+XwznU0nAqRA6i2Q5izFSxAVLa8KOfuZmMODNDwxD9g+Kj7sezQB7mhuuhWbe5qug
+3HHhvwJ3rlDe8TsjEPcfw9zX4koF22XiTPgWxNHE+xGPVnWQbbEuh4yQNMrffBYA
+CA6Es0UHAgMBAAECggEAByliW6OL6dYYZbY9U+M1pF/3/lRNoPZR3A8wzdKSMDZN
+oPn5ibCcByZzIOW0dnopKr//TbPdZgON0ANf4rEjUUguAn/Tmn2g3t3+N6ZZWpDO
+VPmYQ7g0qP42eDreXAhvUprJJ9Bz4EFb+hF5kjfOEQsarrc5/GFBNm7hG7N4dTos
+ANLHDVt9p++40mBC9UnFTI9fghYqqsBipI2hMcVtLlfsTyhfdYk2atC0YwWH2g7H
+nyFsfrgwNlza5iAKuQn1vGhM6nigGBiDfHifNoTHZPhIcgHRwPvZWCpxcAt8wPw1
+4blqZPRqmbOYQHisXVSbKJbgN8zZiZB2j/SSDv9IyQKBgQDqpiSaiBZ+bUfy5M07
+mSClnHR8/zulR7NuSWZ5C92w2EFYI/OkSwKeshKdmafCz9VeVkeCUjjRkAGOF2pr
+I1icdY0XDt/Pd+jQzDqTtiUwwMOLRyvV437nCsMer++LbKRblEL+uSLCPhV2dnyx
+GUXj9JJhnGeZ+xKH20cE1dr2NQKBgQDe7QG6UGrXUzfN2BttvJfOAKETtVfl1Mm0
+uEC2skPlY1KVFYOCMuOFXLHwlc0KYNzVh5qBH+3dPyX1zjTurXmnaOcW0LxgJ4JN
+vKiXDLrt8HYJqANjYCh7vaZmuZ8RduR4iS+E+JG6JLAdfGIzPvzzUgbo5hXuFnQE
+dN8gsJcFywKBgAF24fmY6dMGKZHJfcJmdT6zWELDcQLaDLOef6Y3vb1xzA6ZwtZ+
+pViKMfWL1PExTNqW3UFh8/rS1D+nw8FBajcnwKapMBpiXDCZZbAwTdEdEttWqV5f
+WhZlCcyyOmN7XRc5OKXQT/g4XPftS1/rkXUXvKYhTMA4QehZJPtRvlkVAoGBAMS+
+h/fXYXQIjget4wdGmvPEumSad6jv09UbiIG1cxbQQeIxyo7uOr9IwAKFMyElu8D4
+nPO5KkVJpkb6Ztz/XY7SlqEcOCTkuavCBUjKg2/b+VEsZ1EdXJ1ZE7M1v526QInh
+CX9hobuXBZgAXuq7fKOCkXabGl+2kU4dl49SSvdhAoGAH06W9bZUiqkLIzv+LizW
+rVP9fN4A8EGNtdWVrVD0Ql0hh/PBqGiiVwg47LAVuvpIc9kDDQM/5A6tF7aOGRDR
+k5ZDIrnWmrGQcEMTcisc5OwPnNjLVzV9r0swmeWcqZDrfgxeKpG7vdRaQdR++0FU
+1zaGc8HFGtTeJw6f6ZYttg8=
+-----END PRIVATE KEY-----
diff --git a/acme4j-smime/src/test/resources/invalid-signer.pem b/acme4j-smime/src/test/resources/invalid-signer.pem
new file mode 100644
index 00000000..cb0131ed
--- /dev/null
+++ b/acme4j-smime/src/test/resources/invalid-signer.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEADCCAuigAwIBAgIUbqq0MI0ABCjy4rps+t/Yp26j3icwDQYJKoZIhvcNAQEL
+BQAwfjELMAkGA1UEBhMCWFgxEjAQBgNVBAcMCUFjbWUgQ2l0eTEeMBwGA1UECgwV
+RW1jYSBDZXJ0aWZpY2F0ZXMgTHRkMRQwEgYDVQQDDAtleGFtcGxlLmNvbTElMCMG
+CSqGSIb3DQEJARYWaW52YWxpZC1jYUBleGFtcGxlLmNvbTAeFw0yMjExMDQxMjU1
+MzlaFw0zMjExMDExMjU1MzlaMH4xCzAJBgNVBAYTAlhYMRIwEAYDVQQHDAlBY21l
+IENpdHkxHjAcBgNVBAoMFUVtY2EgQ2VydGlmaWNhdGVzIEx0ZDEUMBIGA1UEAwwL
+ZXhhbXBsZS5jb20xJTAjBgkqhkiG9w0BCQEWFmludmFsaWQtY2FAZXhhbXBsZS5j
+b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDMVVEjUaSK3gir7Ste
+PCFjezFnyvH8ppjzhfKEHBYnggtJn8PHn3P+DBFQOajd2rxzl3dURlYGkfrTN9M0
+Yky5ZXOmJz3j1D2TUkJiXJ14DJUYZjngn6NoTYJVhQZz/wldvcWqzwn/eA6lxqy2
+fC/LhrZPybDTq3nHnd+FeEWfVrzedx5cojbRCSS4IDNe8MyWXFfq0Oy7bAR2Xwzn
+U0nAqRA6i2Q5izFSxAVLa8KOfuZmMODNDwxD9g+Kj7sezQB7mhuuhWbe5qug3HHh
+vwJ3rlDe8TsjEPcfw9zX4koF22XiTPgWxNHE+xGPVnWQbbEuh4yQNMrffBYACA6E
+s0UHAgMBAAGjdjB0MB0GA1UdDgQWBBQdF29RK88xI8z6yQvD75ACd6p6BjAfBgNV
+HSMEGDAWgBQdF29RK88xI8z6yQvD75ACd6p6BjAPBgNVHRMBAf8EBTADAQH/MCEG
+A1UdEQQaMBiBFmludmFsaWQtY2FAZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQAD
+ggEBAJ8BYfB+5cDFqx0nHjOjFbfuTNMlV/6+iuWFhiPwGhRtkOY5M+siHhEoIl7p
+M2uKCmNSNXJWcFrrPX568J/gFm7bymxi5JRVglAetH8fNnWcKDteEE5txl+sMAtG
+OXhWIl8BCeIJ2swVhFrHKeBPeACqq9RGLjiPbEdtpf6zbQDaxIlbEz+PpFICxK/W
+OYp32IJV0DIw1XzV0UhEwZ8eGuRZY3e6Ug+Hux9fXjIgcne/KvfM3rdINmp5dnCW
+3TOnILuYsgIEtwYDKxTJ3fJuypGcLpvrbJTVgNbWwxitAFh+87Y4d6K4n5Math8O
+rXcqWaFqC68EhAxLzSMvPrEI1Dk=
+-----END CERTIFICATE-----
diff --git a/acme4j-smime/src/test/resources/valid-signer-nosan-privkey.pem b/acme4j-smime/src/test/resources/valid-signer-nosan-privkey.pem
new file mode 100644
index 00000000..561dbbc3
--- /dev/null
+++ b/acme4j-smime/src/test/resources/valid-signer-nosan-privkey.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDYEMlw6YXHpO5b
+nyuk7gVdu8XtmhKkg6LhhZ9KYPstIdSr7EbH++h2KMVPVjprvijh35d5hMf9fiMU
+71A93NkO4by3fOIUAJy2m1oXLBSOBhglAbI+Z+DeGfULPvari0Cp6A4TAvvw0DZh
+wR3qzHS50AXzQLVAeL+QoEwOCP0A7I3Ihn+sE3wJlKR+Sk9hYj30WX8M65ievuQw
+3VNRZLX3TqY+oGyWHVhXKnp90ZovoN2Uhj5kTlz0GFJP+37t4urt+cHkoH60O8cP
+Wqcvznwr7keN9z0xRhPJ98OIlob3MeQQqLfN3QQZxp/Wv6WyRGoQg6pE6MtIOKZP
+lDgK4cJhAgMBAAECggEAEZOKA4KnpJY84pyn5P6M1rNt9jZkolfoAd8IFnmdrS31
+mje6CU4reqM1686IqZeaRUeWT6cWyr7+VRdjqGilCqIf4zBIRtbG6M7p7P0jveru
+f2IsMQnrv72OUsggMlO9YqTzMiY5vvz9E4YtbBqOO0haF4/xvqkj8jyr+y9Nf4vY
+Yag2EUddUX2Giqln97aE8G2O8+LJzXTIk1K15DfyBcq1kkye8OSkjsnLybP4wo+v
+uRSDbReF//j+MDepP6IZNPBVyEDyjaXFrNmVb+USyHtzKXwl/GwoYjN7gtOtqc1V
+8vVj1H6+RJK1E67ORLctzzMUZfG6vDB2UZ+vv0lqnQKBgQD7sYWfq1rMzUNhVX4D
+ef0AOoQlBpnOiwoc+7JEXoZoKBw5xrTAxsKRtJFSgIYDYbfi2y3XLu5IO69nkao6
+EqArzaGn+Uc5JcCw4GVVoe6q6W4SwTGC690d/D7R2h24CYjqrIXkFwtvvM6f+5gl
+Vu8da1eXbH73JAEigkKtrvQqVQKBgQDbwzTL6E6hY6DXAxsWzSp1d1n/SK6NdByX
+2yfCQRAQVX6KPVo8Gw1ZGH3IJ/KNVL/TXzDV3NfY6cYBbydcu0/EcVBS11+3KhT7
+uB+Y6T3RThwzRDRBlaeJtUmN63JWmCOBiMIRPmJU2uxS3JzNMqC5QekkjRb/7S2J
+300hAdlb3QKBgGkt3zRBTFmHcZ/sNRPI15RP38cFQiMQ8XH5MJ7njW1bTahLRF/G
+76op9gyvDtG89TZE95wTzZm772ntcmCARhToAqUKQ9w6zZJcw5wMZotfrxMBTupy
+HF4aejoB1yeAPIos/Gq7wpi4IvSyE/uOn7AAmoL54PjwP9Um8Cxaj0hdAoGBALQf
+p6KJ4gj989LHxOhHeUmWbbmEBS4DwXvmMQxS76uzp2f/KXqiYappHI91zqRwllnV
+Z92iiXhNA/Ig/Q5QqOzGQ6Piy50BbPl0zNE0O2rWrt6GRJ6M3ylL4eHk3W6EfHWr
+dgVUMJyEY7b3A75chMfTchh3XCaga/bZhApNza4xAoGAZF1mFIowaUog23TWiann
+ZuCrZObtd2bk4LdtUYSAs2oUFSEWOR4d/av+eb68yf+n69gk1MrP4BA3hyQOX1M9
+Cz/9x/1Tewytywi36noxlgvTfAe3U93LxGjob4z2bsGQ0Xt8q4ngIrEvDeBQa0cq
+bUOxUekM/49+AAToLiTrZhM=
+-----END PRIVATE KEY-----
diff --git a/acme4j-smime/src/test/resources/valid-signer-nosan.pem b/acme4j-smime/src/test/resources/valid-signer-nosan.pem
new file mode 100644
index 00000000..16743a19
--- /dev/null
+++ b/acme4j-smime/src/test/resources/valid-signer-nosan.pem
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID2TCCAsGgAwIBAgIUT0oQs1tpYdA/PUXwyeZqLP4DACYwDQYJKoZIhvcNAQEL
+BQAwfDELMAkGA1UEBhMCWFgxEjAQBgNVBAcMCUFjbWUgQ2l0eTEeMBwGA1UECgwV
+QWNtZSBDZXJ0aWZpY2F0ZXMgTHRkMRQwEgYDVQQDDAtleGFtcGxlLmNvbTEjMCEG
+CSqGSIb3DQEJARYUdmFsaWQtY2FAZXhhbXBsZS5jb20wHhcNMjIxMTA0MTI1NTM5
+WhcNMzIxMTAxMTI1NTM5WjB8MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBD
+aXR5MR4wHAYDVQQKDBVBY21lIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4
+YW1wbGUuY29tMSMwIQYJKoZIhvcNAQkBFhR2YWxpZC1jYUBleGFtcGxlLmNvbTCC
+ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANgQyXDphcek7lufK6TuBV27
+xe2aEqSDouGFn0pg+y0h1KvsRsf76HYoxU9WOmu+KOHfl3mEx/1+IxTvUD3c2Q7h
+vLd84hQAnLabWhcsFI4GGCUBsj5n4N4Z9Qs+9quLQKnoDhMC+/DQNmHBHerMdLnQ
+BfNAtUB4v5CgTA4I/QDsjciGf6wTfAmUpH5KT2FiPfRZfwzrmJ6+5DDdU1FktfdO
+pj6gbJYdWFcqen3Rmi+g3ZSGPmROXPQYUk/7fu3i6u35weSgfrQ7xw9apy/OfCvu
+R433PTFGE8n3w4iWhvcx5BCot83dBBnGn9a/pbJEahCDqkToy0g4pk+UOArhwmEC
+AwEAAaNTMFEwHQYDVR0OBBYEFEaAxnUaGT+hDny105TcVjw00ZqfMB8GA1UdIwQY
+MBaAFEaAxnUaGT+hDny105TcVjw00ZqfMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
+hvcNAQELBQADggEBAL1SPWI/JS9HL3TWN0Q/xkP7UAhgQM6BUzgyVMJ+MxPy34l3
+7vEsSYc3KY8ynNo/psbsBodDqq2iDthZKTtt/l9YAnQrfgpRP+pzLPFDKOB9cDGG
+BoY5KC77B/TXuTfvdT7y0Bvey6C7KMvmPecLNt1sqlCdoOwOyLJN7+zCWVR9nfd4
+Y27VkOvt/zz+cycLmxMIgwGL8nDg37wIoPftEASMvrhO18ZxISsRyabzd6fDZi8m
+u1x97/djezOgoqMgasPBy+yOZbeRwamv1PVElTDJovb/rRS2WMne6+2nMOoQezA3
+C5rRLtPAoKPPStt6E5tSQSWa42VBW2HGD/n4ros=
+-----END CERTIFICATE-----
diff --git a/acme4j-smime/src/test/resources/valid-signer-privkey.pem b/acme4j-smime/src/test/resources/valid-signer-privkey.pem
new file mode 100644
index 00000000..1fc1782c
--- /dev/null
+++ b/acme4j-smime/src/test/resources/valid-signer-privkey.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDNPJbVVGb6ZAdh
+TmFlaDOCs9GRHMwuUugJqaVGzkOMZ/fMimj1r/XubrRh8hcEmu4iZQFfFrmWnRmp
+prK3L6dk6lLTAFlKpMu6yVnRa7LLJbYMQE07uMtbnxwyfwx8DAxiMNzG25s1vswA
+b2JlGPrCfq0oKxe1hSAAik2HI8+0O4Rnl1/V5w6e+Vba874zDK0zwMsu3CiURP+0
+/M5c8cOLIqOKPTaPInUOVnGm0q52IpoF5jVZj9b8+lLp8ayRMlBz9tk+ATc4//1j
+1jW7NlO6WQ7dHoyUs10tqbfb96Gkm9JbJ9JtOjILZ6O9pehEKE8VwCZOmoDQc+CH
+yy52JLXhAgMBAAECggEAT/IIjSvV+zYou8o03TQEUKbr/Ls7e9X2pgDrrROes1wy
+Zf4KWZ3Dzi9YW4jaV4RkO4idyqUHAPjMLM4O8pWA/qnaPm/12EIuS+Gv94gcus5D
+Ri1sCFX4/QUTDkZ4Hf/xePQwo9Oad4qNW6QHr3rV/xoqKCn1D9O9/gfhoEEeYMVV
+nPdHlRR1QPcbptsBGkMYrZ8LwjQb8rMvNTK8HRhc4cKrkobEFvwXcWNO9aIlLveP
+Q58TlQUijeu4eZPM9c3vjl8rM7Oic/ftuU6jmt9IayAVoYblw7yzeEk3lnBlPzME
+e01hr+QD6ZJiquO1stdm6a7TJv15lAghhTBysy7oawKBgQDcY+PBZByHOZ5GzDBb
+fwcScm/RM5zMfDJKrYLNehawJ5egyUOeRm+Ym8d7PpCOWrHLURLpxak2Nrsmivf8
+g1OYAFnWWQm6j3GmlnEspp65n/UVPHETq7tvqulOHmvcOVROsSgHWu8JMgk8mlK1
+kEKlw3y5yzIB7SdyUhCjY5UMGwKBgQDuZeWfg44qvi9o3bb0zbjAHgh0hCQjHy/G
+NVcaVXx06j0a1tAFyESmJqtCEbScrInZnrmpXmTvX5UQczWHi4RYMI8NNFmf3+i0
+XHxWICFrQhdkxppvrEJeXbdfws+UJT8A9K8kokYRO3WyDzrngddfvCbYgHQjMly+
+3Ke8oS2tswKBgQC0sf2ZoSA2ysn3mBCJ5AODX2pIZv3HNojxa4OUPuZ9NWj/fiS/
+j1aOFCMg7DIPVVLytR1BqDtNZOBbAJPEaFRQivEdalEssdFn2W8fQdlfrkN+TtkT
+XLlIHCQ/VXfvzt1Ny7hbF3Zm3qxuEMWBca8DQ91uY6gzpiKye5CCtfINQwKBgQDh
+EPIoFlsxnzvDFQ6VL2MsfS4eUmKLhfXkepcxFWPaPQpTPFpIGzo0Ym1sgqqw/3Nl
+MKS3cZZ5JxPj4+C1htH7MFzdan7yoMFhBa+c39itGkhbq+RBaa9+x5tHnPO8OS2y
+CU8QluLvgeMrp5VE2yAqEcfavernD7TfvBHf04r8YQKBgCHRqnBwHOGHiWyy/Wdu
+ySxqN1hfJWBdKRbtxTj5uCKp/orNtiRVB4GC1ZqhCJ38rZwMSSbp00o9nyDXlRfP
+jTo2zHrxtwem5hB31cZalPt4twJ5SofmNLDngz7UxwpTs8jWaAf8yAtRgdxWbYRg
+bRbZ+LceWQUl8T/RC7UQpy0Y
+-----END PRIVATE KEY-----
diff --git a/acme4j-smime/src/test/resources/valid-signer.pem b/acme4j-smime/src/test/resources/valid-signer.pem
new file mode 100644
index 00000000..3bfbf6ac
--- /dev/null
+++ b/acme4j-smime/src/test/resources/valid-signer.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIID+jCCAuKgAwIBAgIUKAv8VHC4XCtd7BkomMblD3++fLQwDQYJKoZIhvcNAQEL
+BQAwfDELMAkGA1UEBhMCWFgxEjAQBgNVBAcMCUFjbWUgQ2l0eTEeMBwGA1UECgwV
+QWNtZSBDZXJ0aWZpY2F0ZXMgTHRkMRQwEgYDVQQDDAtleGFtcGxlLmNvbTEjMCEG
+CSqGSIb3DQEJARYUdmFsaWQtY2FAZXhhbXBsZS5jb20wHhcNMjIxMTA0MTI1NTM5
+WhcNMzIxMTAxMTI1NTM5WjB8MQswCQYDVQQGEwJYWDESMBAGA1UEBwwJQWNtZSBD
+aXR5MR4wHAYDVQQKDBVBY21lIENlcnRpZmljYXRlcyBMdGQxFDASBgNVBAMMC2V4
+YW1wbGUuY29tMSMwIQYJKoZIhvcNAQkBFhR2YWxpZC1jYUBleGFtcGxlLmNvbTCC
+ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM08ltVUZvpkB2FOYWVoM4Kz
+0ZEczC5S6AmppUbOQ4xn98yKaPWv9e5utGHyFwSa7iJlAV8WuZadGammsrcvp2Tq
+UtMAWUqky7rJWdFrsssltgxATTu4y1ufHDJ/DHwMDGIw3MbbmzW+zABvYmUY+sJ+
+rSgrF7WFIACKTYcjz7Q7hGeXX9XnDp75VtrzvjMMrTPAyy7cKJRE/7T8zlzxw4si
+o4o9No8idQ5WcabSrnYimgXmNVmP1vz6UunxrJEyUHP22T4BNzj//WPWNbs2U7pZ
+Dt0ejJSzXS2pt9v3oaSb0lsn0m06Mgtno72l6EQoTxXAJk6agNBz4IfLLnYkteEC
+AwEAAaN0MHIwHQYDVR0OBBYEFEdIEWKDgjyWKhYjxsAdeNVUwl7iMB8GA1UdIwQY
+MBaAFEdIEWKDgjyWKhYjxsAdeNVUwl7iMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0R
+BBgwFoEUdmFsaWQtY2FAZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBACQ0
+LDU1sVSzRwa1ZeqeiHrc3gbeuBqFqejj+QvPx2N3Zc53265+mb1ga1bJ3O1WePa/
+K/0L0Ska5Y5CELjcGlvy5kcSdQFRhoqK7KZq3c9C39ljl5yx//uJUauXWq+WJhqo
+vTKNo+QC4P9jIjtT6J7irNXl/bS2Qgnmp1xXsVNKmnhWXpbpyelFJgV6WhjtUHxA
+nDZ72q7naLG3tb3umBgAZ/DKQGPvM3WJ2BzwfD6I4c75O//DzB6D7p4w295WU/m/
+AQ5jc/hiEglEh2JHStq1ofvMKg90+azrc06gD7kjXgzaHFBesM8Xph1qrz+8jN1a
+F2L5+xQODQmvZhN5PDM=
+-----END CERTIFICATE-----
diff --git a/acme4j-smime/tool/smime-generator.py b/acme4j-smime/tool/smime-generator.py
new file mode 100755
index 00000000..3bfb0b96
--- /dev/null
+++ b/acme4j-smime/tool/smime-generator.py
@@ -0,0 +1,118 @@
+#!/bin/env python3
+#
+# acme4j - Java ACME client
+#
+# Copyright (C) 2022 Richard "Shred" Körber
+# http://acme4j.shredzone.org
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+#
+
+#
+# This tool creates ACME test e-mails and signs them. It can be
+# used to generate S/MIME mails for unit tests.
+#
+# Requires: M2Crypto
+#
+# WARNING: DO NOT USE THIS CODE TO GENERATE REAL S/MIME MAILS!
+# This generator is only meant to create test mails for unit test
+# purposes, and may lack security relevant features that are
+# needed for real S/MIME mails.
+#
+
+from M2Crypto import BIO, Rand, SMIME
+
+def makebuf(text):
+ return BIO.MemoryBuffer(bytes(text, 'UTF-8'))
+
+def signmail(text, sender, recipient, subject, privkey, pubkey,
+ envelopeFrom=None, envelopeTo=None, envelopeSubject=None):
+ body = 'Content-Type: message/RFC822; forwarded=no\r\n\r\n'
+ body += 'From: {}\r\n'.format(sender)
+ body += 'To: {}\r\n'.format(recipient)
+ body += 'Subject: {}\r\n'.format(subject)
+ body += 'Message-ID: \r\n'
+ body += 'MIME-Version: 1.0\r\n'
+ body += 'Content-Type: text/plain; charset=utf-8\r\n'
+ body += '\r\n'
+ body += text
+ body += '\r\n'
+
+ s = SMIME.SMIME()
+ s.load_key(privkey, pubkey)
+ p7 = s.sign(makebuf(body), SMIME.PKCS7_DETACHED)
+
+ out = BIO.MemoryBuffer()
+ out.write('From: {}\r\n'.format(envelopeFrom if envelopeFrom is not None else sender))
+ out.write('To: {}\r\n'.format(envelopeTo if envelopeTo is not None else recipient))
+ out.write('Subject: {}\r\n'.format(envelopeSubject if envelopeSubject is not None else subject))
+ out.write('Auto-Submitted: auto-generated; type=acme\r\n')
+ out.write('Message-ID: \r\n')
+ s.write(out, p7, makebuf(body))
+
+ return out.read()
+
+with open('src/test/resources/email/valid-mail.eml', 'wb') as w:
+ w.write(signmail('This is an automatically generated ACME challenge.',
+ 'valid-ca@example.com',
+ 'recipient@example.org',
+ 'ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=',
+ 'src/test/resources/valid-signer-privkey.pem',
+ 'src/test/resources/valid-signer.pem'))
+
+with open('src/test/resources/email/invalid-cert-mismatch.eml', 'wb') as w:
+ w.write(signmail('This is an automatically generated ACME challenge.',
+ 'different-ca@example.com',
+ 'recipient@example.org',
+ 'ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=',
+ 'src/test/resources/valid-signer-privkey.pem',
+ 'src/test/resources/valid-signer.pem',
+ envelopeFrom="different-ca@example.org"))
+
+with open('src/test/resources/email/invalid-nosan.eml', 'wb') as w:
+ w.write(signmail('This is an automatically generated ACME challenge.',
+ 'valid-ca@example.com',
+ 'recipient@example.org',
+ 'ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=',
+ 'src/test/resources/valid-signer-nosan-privkey.pem',
+ 'src/test/resources/valid-signer-nosan.pem'))
+
+with open('src/test/resources/email/invalid-signed-mail.eml', 'wb') as w:
+ w.write(signmail('This is an automatically generated ACME challenge.',
+ 'valid-ca@example.com',
+ 'recipient@example.org',
+ 'ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=',
+ 'src/test/resources/invalid-signer-privkey.pem',
+ 'src/test/resources/invalid-signer.pem'))
+
+with open('src/test/resources/email/invalid-protected-mail-from.eml', 'wb') as w:
+ w.write(signmail('This is an automatically generated ACME challenge.',
+ 'valid-ca@example.com',
+ 'recipient@example.org',
+ 'ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=',
+ 'src/test/resources/valid-signer-privkey.pem',
+ 'src/test/resources/valid-signer.pem',
+ envelopeFrom="tampered-ca@example.org"))
+
+with open('src/test/resources/email/invalid-protected-mail-to.eml', 'wb') as w:
+ w.write(signmail('This is an automatically generated ACME challenge.',
+ 'valid-ca@example.com',
+ 'recipient@example.org',
+ 'ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=',
+ 'src/test/resources/valid-signer-privkey.pem',
+ 'src/test/resources/valid-signer.pem',
+ envelopeTo="tampered-recipient@example.com"))
+
+with open('src/test/resources/email/invalid-protected-mail-subject.eml', 'wb') as w:
+ w.write(signmail('This is an automatically generated ACME challenge.',
+ 'valid-ca@example.com',
+ 'recipient@example.org',
+ 'ACME: LgYemJLy3F1LDkiJrdIGbEzyFJyOyf6vBdyZ1TG3sME=',
+ 'src/test/resources/valid-signer-privkey.pem',
+ 'src/test/resources/valid-signer.pem',
+ envelopeSubject="ACME: aDiFfErEnTtOkEn"))
diff --git a/acme4j-smime/tool/test-key-generator.sh b/acme4j-smime/tool/test-key-generator.sh
new file mode 100755
index 00000000..dc231b41
--- /dev/null
+++ b/acme4j-smime/tool/test-key-generator.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+#
+# acme4j - Java ACME client
+#
+# Copyright (C) 2022 Richard "Shred" Körber
+# http://acme4j.shredzone.org
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+#
+
+#
+# Generates test keys for S/MIME unit tests.
+#
+# WARNING: DO NOT USE THIS CODE FOR KEY GENERATION IN PRODUCTION
+# ENVIRONMENTS!
+#
+
+TARGET=src/test/resources/
+
+openssl req -x509 -newkey rsa:2048 -sha256 -nodes -days 3650 \
+ -keyout "$TARGET/valid-signer-privkey.pem" -out "$TARGET/valid-signer.pem" \
+ -subj "/C=XX/L=Acme City/O=Acme Certificates Ltd/CN=example.com/emailAddress=valid-ca@example.com" \
+ -addext "subjectAltName=email:valid-ca@example.com"
+
+openssl req -x509 -newkey rsa:2048 -sha256 -nodes -days 3650 \
+ -keyout "$TARGET/valid-signer-nosan-privkey.pem" -out "$TARGET/valid-signer-nosan.pem" \
+ -subj "/C=XX/L=Acme City/O=Acme Certificates Ltd/CN=example.com/emailAddress=valid-ca@example.com"
+
+openssl req -x509 -newkey rsa:2048 -sha256 -nodes -days 3650 \
+ -keyout "$TARGET/invalid-signer-privkey.pem" -out "$TARGET/invalid-signer.pem" \
+ -subj "/C=XX/L=Acme City/O=Emca Certificates Ltd/CN=example.com/emailAddress=invalid-ca@example.com" \
+ -addext "subjectAltName=email:invalid-ca@example.com"
diff --git a/src/doc/docs/challenge/email-reply-00.md b/src/doc/docs/challenge/email-reply-00.md
index 2a6c9182..ff55e7a9 100644
--- a/src/doc/docs/challenge/email-reply-00.md
+++ b/src/doc/docs/challenge/email-reply-00.md
@@ -14,7 +14,7 @@ To use the S/MIME support, you need to:
* add the `acme4j-smime` module to your list of dependencies
* make sure that `BouncyCastleProvider` is added as security provider
-[RFC 8823](https://tools.ietf.org/html/rfc8823) requires that the DKIM or S/MIME signature of incoming mails _must_ be checked. Outgoing mails _must_ have a valid DKIM or S/MIME signature. This is out of the scope of `acme4j-smime`, but is usually performed by a MTA.
+[RFC 8823](https://tools.ietf.org/html/rfc8823) requires that the DKIM or S/MIME signature of incoming mails _must_ be checked. Outgoing mails _must_ have a valid DKIM signature. Starting with v2.15, _acme4j_ is able to validate and sign S/MIME verification mails. DKIM is usually done by the MTA and thus out of the scope of `acme4j-smime`.
## Ordering
@@ -59,7 +59,7 @@ EmailReply00Challenge challenge = // challenge that is requested by the C
EmailIdentifier identifier = // email address to get the S/MIME cert for
javax.mail.Session mailSession = // javax.mail session
-Message response = new EmailProcessor(challengeMessage)
+Message response = EmailProcessor.plainMessage(challengeMessage)
.expectedIdentifier(identifier)
.withChallenge(challenge)
.respond()
@@ -69,4 +69,28 @@ Transport.send(response); // send response to the CA
challenge.trigger(); // trigger the challenge
```
-The `EmailProcessor` and the related `ResponseGenerator` offer more methods for validating and for customizing the response email.
+The `EmailProcessor` and the related `ResponseGenerator` offer more methods for validating and for customizing the response email, see [the autodocs](../acme4j-smime/apidocs/org.shredzone.acme4j.smime/module-summary.html).
+
+## Validating S/MIME Challenge E-Mails
+
+The `EmailProcessor` is able to validate challenge e-mails that were signed by the CA using S/MIME. To do so, invoke the processor like this:
+
+```java
+Message challengeMessage = // incoming challenge message from the CA
+EmailReply00Challenge challenge = // challenge that is requested by the CA
+EmailIdentifier identifier = // email address to get the S/MIME cert for
+javax.mail.Session mailSession = // javax.mail session
+X509Certificate signCert = // CA's signing certificate, for validation
+boolean strict = // strict checks?
+
+Message response = EmailProcessor.smimeMessage(challengeMessage, mailSession, signCert, strict)
+ .expectedIdentifier(identifier)
+ .withChallenge(challenge)
+ .respond()
+ .generateResponse(mailSession);
+
+Transport.send(response); // send response to the CA
+challenge.trigger(); // trigger the challenge
+```
+
+If `strict` is set to `true`, the S/MIME protected headers `From:`, `To:`, and `Subject:` inside the e-mail **must** match these headers of the wrapping `challengeMessage`. It is recommended to do strict checks. However, if the inbound MTA is changing the headers of the wrapping mail, this flag can be set to `false` instead. In this case, the wrapping headers are ignored, and only the protected headers are used for responding to the challenge.
diff --git a/src/doc/docs/index.md b/src/doc/docs/index.md
index 06868f95..d0126437 100644
--- a/src/doc/docs/index.md
+++ b/src/doc/docs/index.md
@@ -31,7 +31,7 @@ Latest version: 
* [slf4j](http://www.slf4j.org/)
* For `acme4j-utils`: [Bouncy Castle](https://www.bouncycastle.org/)
-* For `acme4j-smime`: [Jakarta Mail](https://eclipse-ee4j.github.io/mail/)
+* For `acme4j-smime`: [Jakarta Mail](https://eclipse-ee4j.github.io/mail/), [Bouncy Castle](https://www.bouncycastle.org/)
## Quick Start
diff --git a/src/doc/docs/migration.md b/src/doc/docs/migration.md
index 19c01f61..e833341d 100644
--- a/src/doc/docs/migration.md
+++ b/src/doc/docs/migration.md
@@ -2,6 +2,11 @@
This document will help you migrate your code to the latest _acme4j_ version.
+## Migration to Version 2.15
+
+- `acme4j-smime` requires BouncyCastle now. The `BouncyCastleProvider` must also be added as security provider.
+- In `acme4j-smime`, the `EmailProcessor` constructor is private now. Use `EmailProcessor.plainMessage()` as drop-in replacement.
+
## Migration to Version 2.13
- The `acme4j-smime` module has switched from _JavaMail_ to _Jakarta Mail_. Unfortunately, this is a breaking API change because classes like `javax.mail.internet.InternetAddress` have moved to respective `jakarta.mail` packages.