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 1c68a1f5..a78cc69b 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 @@ -482,6 +482,8 @@ public final class EmailProcessor { /** * Uses the given truststore for signature verification. + *
+ * This is for self-signed certificates. No revocation checks will take place. * * @param trustStore * {@link KeyStore} of the truststore to be used. @@ -497,6 +499,8 @@ public final class EmailProcessor { /** * Uses the given certificate for signature verification. + *
+ * This is for self-signed certificates. No revocation checks will take place.
*
* @param certificate
* {@link X509Certificate} of the CA
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 6700c260..ad1ddd42 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
@@ -24,7 +24,6 @@ import jakarta.mail.Address;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.Session;
-import jakarta.mail.Transport;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
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
index 145c8aa4..23e6af52 100644
--- 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
@@ -13,6 +13,15 @@
*/
package org.shredzone.acme4j.smime.exception;
+import static java.util.Collections.unmodifiableList;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import org.bouncycastle.i18n.ErrorBundle;
+import org.bouncycastle.i18n.LocalizedException;
import org.shredzone.acme4j.exception.AcmeException;
/**
@@ -32,26 +41,60 @@ import org.shredzone.acme4j.exception.AcmeException;
public class AcmeInvalidMessageException extends AcmeException {
private static final long serialVersionUID = 5607857024718309330L;
+ private final List
+ * This is for self-signed certificates. No revocation checks will take place.
*
* @param signCert {@link X509Certificate} to use.
* @return itself
@@ -106,6 +95,22 @@ public class SignedMailBuilder {
}
}
+ /**
+ * Uses the given truststore for certificate validation.
+ *
+ * This is for self-signed certificates. No revocation checks will take place.
+ *
+ * @param trustStore {@link KeyStore} to use.
+ * @return itself
+ */
+ public SignedMailBuilder withTrustStore(KeyStore trustStore)
+ throws KeyStoreException, InvalidAlgorithmParameterException {
+ requireNonNull(trustStore, "trustStore");
+ PKIXParameters param = new PKIXParameters(trustStore);
+ param.setRevocationEnabled(false);
+ return withPKIXParameters(param);
+ }
+
/**
* Uses the given {@link PKIXParameters} for certificate validation.
*
@@ -227,6 +232,7 @@ public class SignedMailBuilder {
* if the signature is invalid, or if the message was signed with more than
* one signature.
*/
+ @SuppressWarnings("unchecked")
private SignerInformation validateSignature(MimeMessage message, PKIXParameters pkixParameters)
throws AcmeInvalidMessageException {
try {
@@ -236,9 +242,15 @@ public class SignedMailBuilder {
if (store.size() != 1) {
throw new AcmeInvalidMessageException("Expected exactly one signer, but found " + store.size());
}
- return store.getSigners().iterator().next();
+
+ SignerInformation si = store.getSigners().iterator().next();
+ SignedMailValidator.ValidationResult vr = smv.getValidationResult(si);
+ if (!vr.isValidSignature()) {
+ throw new AcmeInvalidMessageException("Invalid signature", vr.getErrors());
+ }
+ return si;
} catch (SignedMailValidatorException ex) {
- throw new AcmeInvalidMessageException("Invalid signature", ex);
+ throw new AcmeInvalidMessageException("Cannot validate signature", ex);
}
}
@@ -289,7 +301,7 @@ public class SignedMailBuilder {
*
* @return CaCerts truststore
*/
- private static KeyStore getCaCertsTrustStore() {
+ protected static KeyStore getCaCertsTrustStore() {
KeyStore caCerts = CACERTS_TRUSTSTORE.get();
if (caCerts == null) {
String javaHome = System.getProperty("java.home");
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 68349a82..2cd83dff 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
@@ -17,6 +17,7 @@ import static jakarta.mail.Message.RecipientType.TO;
import static org.assertj.core.api.Assertions.*;
import java.io.IOException;
+import java.security.KeyStore;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.Optional;
@@ -70,19 +71,25 @@ public class EmailProcessorTest extends SMIMETests {
assertThatNoException().isThrownBy(() -> {
MimeMessage message = mockMessage("valid-mail");
X509Certificate certificate = readCertificate("valid-signer");
- EmailProcessor.smimeMessage(message, mailSession, certificate, true);
+ EmailProcessor.builder().certificate(certificate).strict().build(message);
});
}
@Test
public void testInvalidSignature() {
- assertThatExceptionOfType(AcmeInvalidMessageException.class)
- .isThrownBy(() -> {
+ AcmeInvalidMessageException ex = catchThrowableOfType(() -> {
MimeMessage message = mockMessage("invalid-signed-mail");
X509Certificate certificate = readCertificate("valid-signer");
- EmailProcessor.smimeMessage(message, mailSession, certificate, true);
- })
- .withMessage("Message is not signed by the expected sender");
+ EmailProcessor.builder().certificate(certificate).strict().build(message);
+ }, AcmeInvalidMessageException.class);
+
+ assertThat(ex).isNotNull();
+ assertThat(ex.getMessage()).isEqualTo("Invalid signature");
+ assertThat(ex.getErrors()).hasSize(2);
+ assertThat(ex.getErrors())
+ .first().hasFieldOrPropertyWithValue("id", "SignedMailValidator.emailFromCertMismatch");
+ assertThat(ex.getErrors())
+ .element(1).hasFieldOrPropertyWithValue("id", "SignedMailValidator.certPathInvalid");
}
@Test
@@ -91,31 +98,37 @@ public class EmailProcessorTest extends SMIMETests {
.isThrownBy(() -> {
MimeMessage message = mockMessage("invalid-nosan");
X509Certificate certificate = readCertificate("valid-signer-nosan");
- EmailProcessor.smimeMessage(message, mailSession, certificate, true);
+ EmailProcessor.builder().certificate(certificate).strict().build(message);
})
.withMessage("Certificate does not have a subjectAltName extension");
}
@Test
public void testSANDoesNotMatchFrom() {
- assertThatExceptionOfType(AcmeInvalidMessageException.class)
- .isThrownBy(() -> {
+ AcmeInvalidMessageException ex = catchThrowableOfType(() -> {
MimeMessage message = mockMessage("invalid-cert-mismatch");
X509Certificate certificate = readCertificate("valid-signer");
- EmailProcessor.smimeMessage(message, mailSession, certificate, true);
- })
- .withMessage("Secured header 'From' does not match envelope header");
+ EmailProcessor.builder().certificate(certificate).strict().build(message);
+ }, AcmeInvalidMessageException.class);
+
+ assertThat(ex).isNotNull();
+ assertThat(ex.getMessage()).isEqualTo("Invalid signature");
+ assertThat(ex.getErrors())
+ .singleElement().hasFieldOrPropertyWithValue("id", "SignedMailValidator.emailFromCertMismatch");
}
@Test
public void testInvalidProtectedFromHeader() {
- assertThatExceptionOfType(AcmeInvalidMessageException.class)
- .isThrownBy(() -> {
+ AcmeInvalidMessageException ex = catchThrowableOfType(() -> {
MimeMessage message = mockMessage("invalid-protected-mail-from");
X509Certificate certificate = readCertificate("valid-signer");
- EmailProcessor.smimeMessage(message, mailSession, certificate, true);
- })
- .withMessage("Secured header 'From' does not match envelope header");
+ EmailProcessor.builder().certificate(certificate).strict().build(message);
+ }, AcmeInvalidMessageException.class);
+
+ assertThat(ex).isNotNull();
+ assertThat(ex.getMessage()).isEqualTo("Invalid signature");
+ assertThat(ex.getErrors())
+ .singleElement().hasFieldOrPropertyWithValue("id", "SignedMailValidator.emailFromCertMismatch");
}
@Test
@@ -124,7 +137,7 @@ public class EmailProcessorTest extends SMIMETests {
.isThrownBy(() -> {
MimeMessage message = mockMessage("invalid-protected-mail-to");
X509Certificate certificate = readCertificate("valid-signer");
- EmailProcessor.smimeMessage(message, mailSession, certificate, true);
+ EmailProcessor.builder().certificate(certificate).strict().build(message);
})
.withMessage("Secured header 'To' does not match envelope header");
}
@@ -135,7 +148,7 @@ public class EmailProcessorTest extends SMIMETests {
.isThrownBy(() -> {
MimeMessage message = mockMessage("invalid-protected-mail-subject");
X509Certificate certificate = readCertificate("valid-signer");
- EmailProcessor.smimeMessage(message, mailSession, certificate, true);
+ EmailProcessor.builder().certificate(certificate).strict().build(message);
})
.withMessage("Secured header 'Subject' does not match envelope header");
}
@@ -146,10 +159,34 @@ public class EmailProcessorTest extends SMIMETests {
.isThrownBy(() -> {
MimeMessage message = mockMessage("invalid-protected-mail-subject");
X509Certificate certificate = readCertificate("valid-signer");
- EmailProcessor.smimeMessage(message, mailSession, certificate, false);
+ EmailProcessor.builder().certificate(certificate).relaxed().build(message);
});
}
+ @Test
+ public void testValidSignatureRfc7508() throws Exception {
+ MimeMessage message = mockMessage("valid-mail-7508");
+
+ KeyStore keyStore = KeyStore.getInstance("JKS");
+ keyStore.load(EmailProcessorTest.class.getResourceAsStream("/7508-valid-ca.jks"), "test123".toCharArray());
+
+ EmailProcessor processor = EmailProcessor.builder().trustStore(keyStore).build(message);
+ assertThat(processor.getSender()).isEqualTo(new InternetAddress("acme-challenge@dc-bsd.my.corp"));
+ assertThat(processor.getRecipient()).isEqualTo(new InternetAddress("gitlab@dc-bsd.my.corp"));
+ assertThat(processor.getToken1()).isEqualTo("ABxfL5s4bjvmyVRvl6y-Y_GhdzTdWpKqlmrKAIVe");
+ }
+
+ @Test
+ public void testInvalidSignatureRfc7508() throws Exception {
+ MimeMessage message = mockMessage("valid-mail-7508");
+
+ KeyStore keyStore = KeyStore.getInstance("JKS");
+ keyStore.load(EmailProcessorTest.class.getResourceAsStream("/7508-fake-ca.jks"), "test123".toCharArray());
+
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(() -> EmailProcessor.builder().trustStore(keyStore).build(message));
+ }
+
@Test
public void textExpectedFromFails() {
assertThatExceptionOfType(AcmeProtocolException.class)
diff --git a/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/wrapper/SignedMailBuilderTest.java b/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/wrapper/SignedMailBuilderTest.java
new file mode 100644
index 00000000..6603fc62
--- /dev/null
+++ b/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/wrapper/SignedMailBuilderTest.java
@@ -0,0 +1,39 @@
+/*
+ * acme4j - Java ACME client
+ *
+ * Copyright (C) 2023 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.wrapper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link SignedMailBuilder}.
+ */
+public class SignedMailBuilderTest {
+
+ @Test
+ public void testDefaultTrustStoreIsCreated() throws KeyStoreException {
+ KeyStore keyStore = SignedMailBuilder.getCaCertsTrustStore();
+ assertThat(keyStore).isNotNull();
+ assertThat(keyStore.size()).isGreaterThan(0);
+
+ // Make sure the instance is cached
+ KeyStore keyStore2 = SignedMailBuilder.getCaCertsTrustStore();
+ assertThat(keyStore2).isSameAs(keyStore);
+ }
+
+}
\ No newline at end of file
diff --git a/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/wrapper/SignedMailTest.java b/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/wrapper/SignedMailTest.java
new file mode 100644
index 00000000..363388c8
--- /dev/null
+++ b/acme4j-smime/src/test/java/org/shredzone/acme4j/smime/wrapper/SignedMailTest.java
@@ -0,0 +1,457 @@
+/*
+ * acme4j - Java ACME client
+ *
+ * Copyright (C) 2023 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.wrapper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+
+import jakarta.mail.Header;
+import jakarta.mail.internet.InternetAddress;
+import org.junit.jupiter.api.Test;
+import org.shredzone.acme4j.smime.exception.AcmeInvalidMessageException;
+
+/**
+ * Unit tests for {@link SignedMail}.
+ */
+public class SignedMailTest {
+
+ @Test
+ public void testCheckDuplicatedStrictGood() throws Exception {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "From", "foo@example.com"
+ ));
+
+ // Success: Field is present and identical
+ signedMail.checkDuplicatedField("From", "foo@example.com", false);
+
+ assertThat(signedMail.getFrom()).isEqualTo(new InternetAddress("foo@example.com"));
+ }
+
+ @Test
+ public void testCheckDuplicatedStrictBad() {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "From", " foo@example.com "
+ ));
+
+ // Failure: Field is same, but has extra whitespaces
+ assertThatExceptionOfType(AcmeInvalidMessageException.class).isThrownBy(() ->
+ signedMail.checkDuplicatedField("From", "foo@example.com", false)
+ );
+ }
+
+ @Test
+ public void testCheckDuplicatedRelaxedGood() throws Exception {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "FROM", " foo@example.com "
+ ));
+
+ // Good: Field is there and identical (ignoring case and whitespaces)
+ signedMail.checkDuplicatedField("From", "foo@example.com", true);
+
+ assertThat(signedMail.getFrom()).isEqualTo(new InternetAddress("foo@example.com"));
+ }
+
+ @Test
+ public void testCheckDuplicatedRelaxedBad() {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "From", "bar@example.com"
+ ));
+
+ // Failure: Field is present, but different value
+ assertThatExceptionOfType(AcmeInvalidMessageException.class).isThrownBy(() ->
+ signedMail.checkDuplicatedField("From", "foo@example.com", true)
+ );
+ }
+
+ @Test
+ public void testDeleteFieldStrictGood() throws Exception {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "From", "foo@example.com"
+ ));
+
+ // Good: Field is present and identical
+ signedMail.deleteField("From", "foo@example.com", false);
+
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(signedMail::getFrom)
+ .withMessage("Protected 'FROM' header is required, but missing");
+ }
+
+ @Test
+ public void testDeleteFieldStrictBad() throws Exception {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "From", "bar@example.com"
+ ));
+
+ // Bad: Field is present, but has different value
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(() -> signedMail.deleteField("From", "foo@example.com", false))
+ .withMessage("Secured header 'From' was not found in envelope header for deletion");
+ }
+
+ @Test
+ public void testDeleteFieldRelaxedGood() throws Exception {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "FROM", " foo@example.com "
+ ));
+
+ // Good: Field is present and identical (ignoring case and whitespaces)
+ signedMail.deleteField("From", "foo@example.com", true);
+
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(signedMail::getFrom)
+ .withMessage("Protected 'FROM' header is required, but missing");
+ }
+
+ @Test
+ public void testDeleteFieldRelaxedBad() throws Exception {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "FROM", "bar@example.com"
+ ));
+
+ // Bad: Field is present, but has different value
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(() -> signedMail.deleteField("From", "foo@example.com", true))
+ .withMessage("Secured header 'From' was not found in envelope header for deletion");
+ }
+
+ @Test
+ public void testModifyFieldStrictGood() throws Exception {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "From", "foo@example.com"
+ ));
+
+ // Good: field is present, content is replaced
+ signedMail.modifyField("From", "bar@example.com", false);
+
+ assertThat(signedMail.getFrom()).isEqualTo(new InternetAddress("bar@example.com"));
+ }
+
+ @Test
+ public void testModifyFieldStrictBad() {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "FROM", "foo@example.com"
+ ));
+
+ // Failure: Field is not present because it's all-caps
+ assertThatExceptionOfType(AcmeInvalidMessageException.class).isThrownBy(() ->
+ signedMail.modifyField("From", "bar@example.com", false)
+ );
+ }
+
+ @Test
+ public void testModifyFieldRelaxedGood() throws Exception {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "FROM", "foo@example.com"
+ ));
+
+ // Good: Field is present (ignoring case)
+ signedMail.modifyField("From", "bar@example.com", true);
+
+ assertThat(signedMail.getFrom()).isEqualTo(new InternetAddress("bar@example.com"));
+ }
+
+ @Test
+ public void testModifyFieldRelaxedBad() {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "To", "foo@example.com"
+ ));
+
+ // Failure: Field is not present at all
+ assertThatExceptionOfType(AcmeInvalidMessageException.class).isThrownBy(() ->
+ signedMail.modifyField("From", "foo@example.com", true)
+ );
+ }
+
+ @Test
+ public void testImportUntrusted() {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "From", "foo@example.com",
+ "Message-Id", "123456ABCDEF"
+ ));
+
+ // Success because Message ID does not need to be trusted
+ assertThat(signedMail.getMessageId()).isNotEmpty().contains("123456ABCDEF");
+
+ // Failure because From is required to be trusted
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(signedMail::getFrom);
+ }
+
+ @Test
+ public void testImportTrustedStrict() throws Exception {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "From", "foo@example.com",
+ "Message-Id", "123456ABCDEF"
+ ));
+ signedMail.importTrustedHeaders(withHeaders(
+ "From", "foo@example.com"
+ ));
+
+ // Success because Message ID does not need to be trusted
+ assertThat(signedMail.getMessageId()).isNotEmpty().contains("123456ABCDEF");
+
+ // Success because From is trusted
+ assertThat(signedMail.getFrom()).isEqualTo(new InternetAddress("foo@example.com"));
+ }
+
+ @Test
+ public void testImportTrustedRelaxed() throws Exception {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "Message-Id", "123456ABCDEF"
+ ));
+ signedMail.importTrustedHeadersRelaxed(withHeaders(
+ "From", "foo@example.com"
+ ));
+
+ // Success because Message ID does not need to be trusted
+ assertThat(signedMail.getMessageId()).isNotEmpty().contains("123456ABCDEF");
+
+ // Success because From is trusted
+ assertThat(signedMail.getFrom()).isEqualTo(new InternetAddress("foo@example.com"));
+ }
+
+ @Test
+ public void testImportStrictFails() {
+ SignedMail signedMail = new SignedMail();
+
+ // Fails because there is no matching untrusted header
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(() -> signedMail.importTrustedHeaders(withHeaders(
+ "From", "foo@example.com"
+ )));
+ }
+
+ @Test
+ public void testFromEmpty() {
+ SignedMail signedMail = new SignedMail();
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(signedMail::getFrom)
+ .withMessage("Protected 'FROM' header is required, but missing");
+ }
+
+ @Test
+ public void testFromUntrusted() {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "From", "foo@example.com"
+ ));
+
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(signedMail::getFrom)
+ .withMessage("Protected 'FROM' header is required, but missing");
+ }
+
+ @Test
+ public void testFromTrusted() throws Exception {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importTrustedHeadersRelaxed(withHeaders(
+ "From", "foo@example.com"
+ ));
+
+ assertThat(signedMail.getFrom()).isEqualTo(new InternetAddress("foo@example.com"));
+ }
+
+ @Test
+ public void testToEmpty() {
+ SignedMail signedMail = new SignedMail();
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(signedMail::getTo)
+ .withMessage("Protected 'TO' header is required, but missing");
+ }
+
+ @Test
+ public void testToUntrusted() {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "To", "foo@example.com"
+ ));
+
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(signedMail::getTo)
+ .withMessage("Protected 'TO' header is required, but missing");
+ }
+
+ @Test
+ public void testToTrusted() throws Exception {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importTrustedHeadersRelaxed(withHeaders(
+ "To", "foo@example.com"
+ ));
+
+ assertThat(signedMail.getTo()).isEqualTo(new InternetAddress("foo@example.com"));
+ }
+
+ @Test
+ public void testSubjectEmpty() {
+ SignedMail signedMail = new SignedMail();
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(signedMail::getSubject)
+ .withMessage("Protected 'SUBJECT' header is required, but missing");
+ }
+
+ @Test
+ public void testSubjectUntrusted() {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "Subject", "abc123"
+ ));
+
+ assertThatExceptionOfType(AcmeInvalidMessageException.class)
+ .isThrownBy(signedMail::getSubject)
+ .withMessage("Protected 'SUBJECT' header is required, but missing");
+ }
+
+ @Test
+ public void testSubjectTrusted() throws Exception {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importTrustedHeadersRelaxed(withHeaders(
+ "Subject", "abc123"
+ ));
+
+ assertThat(signedMail.getSubject()).isEqualTo("abc123");
+ }
+
+ @Test
+ public void testMessageIdEmpty() {
+ SignedMail signedMail = new SignedMail();
+ assertThat(signedMail.getMessageId()).isEmpty();
+ }
+
+ @Test
+ public void testMessageId() {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "Message-Id", "12345ABCDE"
+ ));
+
+ assertThat(signedMail.getMessageId()).isNotEmpty().contains("12345ABCDE");
+ }
+
+ @Test
+ public void testReplyToEmpty() throws Exception {
+ SignedMail signedMail = new SignedMail();
+ assertThat(signedMail.getReplyTo()).isEmpty();
+ }
+
+ @Test
+ public void testReplyTo() throws Exception {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "Reply-To", "foo@example.com",
+ "Reply-To", "bar@example.org"
+ ));
+
+ assertThat(signedMail.getReplyTo()).contains(
+ new InternetAddress("foo@example.com"),
+ new InternetAddress("bar@example.org")
+ );
+ }
+
+ @Test
+ public void testIsAutoSubmitted() {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "Auto-Submitted", "auto-generated; type=acme"
+ ));
+
+ assertThat(signedMail.isAutoSubmitted()).isTrue();
+ }
+
+ @Test
+ public void testIsNotAutoSubmitted() {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "Auto-Submitted", "no"
+ ));
+
+ assertThat(signedMail.isAutoSubmitted()).isFalse();
+ }
+
+ @Test
+ public void testIsAutoSubmittedMissing() {
+ SignedMail signedMail = new SignedMail();
+ assertThat(signedMail.isAutoSubmitted()).isFalse();
+ }
+
+ @Test
+ public void testMissingSecuredHeadersEmpty() {
+ SignedMail signedMail = new SignedMail();
+ assertThat(signedMail.getMissingSecuredHeaders()).contains("FROM", "TO", "SUBJECT");
+ }
+
+ @Test
+ public void testMissingSecuredHeadersGood() {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importTrustedHeadersRelaxed(withHeaders(
+ "From", "foo@example.com",
+ "To", "bar@example.org",
+ "Subject", "foo123"
+ ));
+
+ assertThat(signedMail.getMissingSecuredHeaders()).isEmpty();
+ }
+
+ @Test
+ public void testMissingSecuredHeadersTrustedBad() {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importTrustedHeadersRelaxed(withHeaders(
+ "From", "foo@example.com",
+ "To", "bar@example.org"
+ ));
+
+ assertThat(signedMail.getMissingSecuredHeaders()).contains("SUBJECT");
+ }
+
+ @Test
+ public void testMissingSecuredHeadersUntustedBad() {
+ SignedMail signedMail = new SignedMail();
+ signedMail.importUntrustedHeaders(withHeaders(
+ "From", "foo@example.com",
+ "To", "bar@example.org",
+ "Subject", "foo123"
+ ));
+
+ assertThat(signedMail.getMissingSecuredHeaders()).contains("FROM", "TO", "SUBJECT");
+ }
+
+ private Enumeration This is an automatically generated ACME challenge for email =
+address gitlab@dc-bsd.my.corp. If you haven't requested an S/MIME =
+certificate generation for this email address, be very afraid. If you did =
+request it, your email client might be able to process this request =
+automatically, or you might have to paste the first token part into an =
+external program. Please reply to this mail and fill out the =
+following template: -----BEGIN ACME RESPONSE-----
+<fill in challengeResponse here>
+-----END ACME RESPONSE-----
+
Use the value of the following calculation inside the ACME response:
+ token =3D (decodeBase64url(token-part1) || decodeBase64url(token-par=
+t2))
+ keyAuthorization =3D base64url(token) || '.' || base64url(Thumbprint(acco=
+untKey))
+ challengeResponse =3D base64url(SHA256(keyAuthorization))
+
Where can I find all the ingredients for this?