Add unit tests

- Unit tests for RFC-7508 type signatures
- Unit tests for evaluation of trusted headers
pull/134/head
Richard Körber 2023-02-10 19:54:50 +01:00
parent 2118fb8593
commit aae98d7ce8
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
11 changed files with 782 additions and 44 deletions

View File

@ -482,6 +482,8 @@ public final class EmailProcessor {
/**
* Uses the given truststore for signature verification.
* <p>
* 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.
* <p>
* This is for self-signed certificates. No revocation checks will take place.
*
* @param certificate
* {@link X509Certificate} of the CA

View File

@ -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;

View File

@ -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,6 +41,8 @@ import org.shredzone.acme4j.exception.AcmeException;
public class AcmeInvalidMessageException extends AcmeException {
private static final long serialVersionUID = 5607857024718309330L;
private final List<ErrorBundle> errors;
/**
* Creates a new {@link AcmeInvalidMessageException}.
*
@ -40,6 +51,21 @@ public class AcmeInvalidMessageException extends AcmeException {
*/
public AcmeInvalidMessageException(String msg) {
super(msg);
this.errors = Collections.emptyList();
}
/**
* Creates a new {@link AcmeInvalidMessageException}.
*
* @param msg
* Reason of the exception
* @param errors
* List of {@link ErrorBundle} with further details
* @since 2.16
*/
public AcmeInvalidMessageException(String msg, List<ErrorBundle> errors) {
super(msg);
this.errors = unmodifiableList(errors);
}
/**
@ -52,6 +78,23 @@ public class AcmeInvalidMessageException extends AcmeException {
*/
public AcmeInvalidMessageException(String msg, Throwable cause) {
super(msg, cause);
List<ErrorBundle> errors = new ArrayList<>(1);
Optional.ofNullable(cause)
.filter(LocalizedException.class::isInstance)
.map(LocalizedException.class::cast)
.map(LocalizedException::getErrorMessage)
.ifPresent(errors::add);
this.errors = unmodifiableList(errors);
}
/**
* Returns a list with further error details, if available. The list may be empty, but
* is never {@code null}.
*
* @since 2.16
*/
public List<ErrorBundle> getErrors() {
return errors;
}
}

View File

@ -17,7 +17,6 @@ import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
@ -290,7 +289,7 @@ public class SignedMail implements Mail {
* @throws AcmeInvalidMessageException
* if a header with the same value was not found
*/
private void checkDuplicatedField(String header, String value, boolean relaxed) throws AcmeInvalidMessageException {
protected void checkDuplicatedField(String header, String value, boolean relaxed) throws AcmeInvalidMessageException {
long count = headers.stream()
.filter(mh -> mh.nameEquals(header, relaxed) && mh.valueEquals(value, relaxed))
.peek(MailHeader::setTrusted)
@ -315,7 +314,7 @@ public class SignedMail implements Mail {
* @throws AcmeInvalidMessageException
* if a header with the same value was not found
*/
private void deleteField(String header, String value, boolean relaxed) throws AcmeInvalidMessageException {
protected void deleteField(String header, String value, boolean relaxed) throws AcmeInvalidMessageException {
if (!headers.removeIf(mh -> mh.nameEquals(header, relaxed) && mh.valueEquals(value, relaxed))) {
throw new AcmeInvalidMessageException("Secured header '" + header
+ "' was not found in envelope header for deletion");
@ -335,7 +334,7 @@ public class SignedMail implements Mail {
* @throws AcmeInvalidMessageException
* if the header was not found
*/
private void modifyField(String header, String value, boolean relaxed) throws AcmeInvalidMessageException {
protected void modifyField(String header, String value, boolean relaxed) throws AcmeInvalidMessageException {
if (!headers.removeIf(mh -> mh.nameEquals(header, relaxed))) {
throw new AcmeInvalidMessageException("Secured header '" + header
+ "' was not found in envelope header for modification");

View File

@ -27,7 +27,6 @@ import java.security.cert.CertificateException;
import java.security.cert.PKIXParameters;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
@ -75,20 +74,10 @@ public class SignedMailBuilder {
return this;
}
/**
* Uses the given truststore for certificate validation.
*
* @param trustStore {@link KeyStore} to use.
* @return itself
*/
public SignedMailBuilder withTrustStore(KeyStore trustStore)
throws KeyStoreException, InvalidAlgorithmParameterException {
requireNonNull(trustStore, "trustStore");
return withPKIXParameters(new PKIXParameters(trustStore));
}
/**
* Uses the given {@link X509Certificate} for certificate validation.
* <p>
* 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.
* <p>
* 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");

View File

@ -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)

View File

@ -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);
}
}

View File

@ -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<Header> withHeaders(String... kv) {
List<Header> headers = new ArrayList<>();
for (int ix = 0; ix < kv.length; ix += 2) {
headers.add(new Header(kv[ix], kv[ix+1]));
}
return Collections.enumeration(headers);
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,148 @@
Return-Path: <acme-challenge@dc-bsd.my.corp>
X-Original-To: gitlab@dc-bsd.my.corp
Delivered-To: gitlab@dc-bsd.my.corp
Received: from [127.0.0.1] (acme-ca-02.dc-bsd.my.corp [10.70.15.231])
by mail.dc-bsd.my.corp (Postfix) with ESMTP id 0119D9CC22
for <gitlab@dc-bsd.my.corp>; Sat, 26 Nov 2022 21:09:39 +0000 (UTC)
Content-Type: multipart/signed; protocol="application/pkcs7-signature";
micalg=sha256; boundary="--_NmP-1d902f4d1e8a735a-Part_1"
Auto-Submitted: auto-generated; type=acme
From: acme-challenge@dc-bsd.my.corp
To: gitlab@dc-bsd.my.corp
Subject: ACME: ABxfL5s4bjvmyVRvl6y-Y_GhdzTdWpKqlmrKAIVe
Message-ID: <ee3f3a6d-12c4-7cc0-aff2-b5a9b19a9f7e@dc-bsd.my.corp>
Date: Sat, 26 Nov 2022 21:09:38 +0000
MIME-Version: 1.0
----_NmP-1d902f4d1e8a735a-Part_1
Content-Type: multipart/alternative;
boundary="--_NmP-1d902f4d1e8a735a-Part_2"
----_NmP-1d902f4d1e8a735a-Part_2
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<html><p>This is an automatically generated ACME challenge for email =
address <em>gitlab@dc-bsd.my.corp</em>. 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.</p> <p>Please reply to this mail and fill out the =
following template: <pre>-----BEGIN ACME RESPONSE-----
&lt;fill in challengeResponse here&gt;
-----END ACME RESPONSE-----
</pre>Use the value of the following calculation inside the ACME response:
<pre> token =3D (decodeBase64url(token-part1) || decodeBase64url(token-par=
t2))
keyAuthorization =3D base64url(token) || '.' || base64url(Thumbprint(acco=
untKey))
challengeResponse =3D base64url(SHA256(keyAuthorization))
</pre>Where can I find all the ingredients for this?<ul><li>token-part1 is =
in the subject of this email after 'ACME: ',</li><li>token-part2 can be =
found in your challenge request (over https),</li><li>accountKey has been =
generated in your ACME client.</li></ul></p></html>
----_NmP-1d902f4d1e8a735a-Part_2
Content-Type: application/json; charset=utf8
Content-Encoding: utf8
{ "token-part1": "001c5f2f9b386e3be6c9546f97acbe63f1a17734dd5a92aa966aca00855e" }
----_NmP-1d902f4d1e8a735a-Part_2--
----_NmP-1d902f4d1e8a735a-Part_1
Content-Type: application/pkcs7-signature; name=smime.p7s
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename=smime.p7s
MIIUXQYJKoZIhvcNAQcCoIIUTjCCFEoCAQExDzANBglghkgBZQMEAgEFADCCBkkGCSqGSIb3DQEH
AaCCBjoEggY2Q29udGVudC1UeXBlOiBtdWx0aXBhcnQvYWx0ZXJuYXRpdmU7DQogYm91bmRhcnk9
Ii0tX05tUC0xZDkwMmY0ZDFlOGE3MzVhLVBhcnRfMiINCg0KLS0tLV9ObVAtMWQ5MDJmNGQxZThh
NzM1YS1QYXJ0XzINCkNvbnRlbnQtVHlwZTogdGV4dC9odG1sOyBjaGFyc2V0PXV0Zi04DQpDb250
ZW50LVRyYW5zZmVyLUVuY29kaW5nOiBxdW90ZWQtcHJpbnRhYmxlDQoNCjxodG1sPjxwPlRoaXMg
aXMgYW4gYXV0b21hdGljYWxseSBnZW5lcmF0ZWQgQUNNRSBjaGFsbGVuZ2UgZm9yIGVtYWlsID0N
CmFkZHJlc3MgPGVtPmdpdGxhYkBkYy1ic2QubXkuY29ycDwvZW0+LiBJZiB5b3UgaGF2ZW4ndCBy
ZXF1ZXN0ZWQgYW4gUy9NSU1FID0NCmNlcnRpZmljYXRlIGdlbmVyYXRpb24gZm9yIHRoaXMgZW1h
aWwgYWRkcmVzcywgYmUgdmVyeSBhZnJhaWQuIElmIHlvdSBkaWQgPQ0KcmVxdWVzdCBpdCwgeW91
ciBlbWFpbCBjbGllbnQgbWlnaHQgYmUgYWJsZSB0byBwcm9jZXNzIHRoaXMgcmVxdWVzdCA9DQph
dXRvbWF0aWNhbGx5LCBvciB5b3UgbWlnaHQgaGF2ZSB0byBwYXN0ZSB0aGUgZmlyc3QgdG9rZW4g
cGFydCBpbnRvIGFuID0NCmV4dGVybmFsIHByb2dyYW0uPC9wPiA8cD5QbGVhc2UgcmVwbHkgdG8g
dGhpcyBtYWlsIGFuZCBmaWxsIG91dCB0aGUgPQ0KZm9sbG93aW5nIHRlbXBsYXRlOiA8cHJlPi0t
LS0tQkVHSU4gQUNNRSBSRVNQT05TRS0tLS0tDQombHQ7ZmlsbCBpbiBjaGFsbGVuZ2VSZXNwb25z
ZSBoZXJlJmd0Ow0KLS0tLS1FTkQgQUNNRSBSRVNQT05TRS0tLS0tDQo8L3ByZT5Vc2UgdGhlIHZh
bHVlIG9mIHRoZSBmb2xsb3dpbmcgY2FsY3VsYXRpb24gaW5zaWRlIHRoZSBBQ01FIHJlc3BvbnNl
Og0KPHByZT4gIHRva2VuID0zRCAoZGVjb2RlQmFzZTY0dXJsKHRva2VuLXBhcnQxKSB8fCBkZWNv
ZGVCYXNlNjR1cmwodG9rZW4tcGFyPQ0KdDIpKQ0KICBrZXlBdXRob3JpemF0aW9uID0zRCBiYXNl
NjR1cmwodG9rZW4pIHx8ICcuJyB8fCBiYXNlNjR1cmwoVGh1bWJwcmludChhY2NvPQ0KdW50S2V5
KSkNCiAgY2hhbGxlbmdlUmVzcG9uc2UgPTNEIGJhc2U2NHVybChTSEEyNTYoa2V5QXV0aG9yaXph
dGlvbikpDQo8L3ByZT5XaGVyZSBjYW4gSSBmaW5kIGFsbCB0aGUgaW5ncmVkaWVudHMgZm9yIHRo
aXM/PHVsPjxsaT50b2tlbi1wYXJ0MSBpcyA9DQppbiB0aGUgc3ViamVjdCBvZiB0aGlzIGVtYWls
IGFmdGVyICdBQ01FOiAnLDwvbGk+PGxpPnRva2VuLXBhcnQyIGNhbiBiZSA9DQpmb3VuZCBpbiB5
b3VyIGNoYWxsZW5nZSByZXF1ZXN0IChvdmVyIGh0dHBzKSw8L2xpPjxsaT5hY2NvdW50S2V5IGhh
cyBiZWVuID0NCmdlbmVyYXRlZCBpbiB5b3VyIEFDTUUgY2xpZW50LjwvbGk+PC91bD48L3A+PC9o
dG1sPg0KLS0tLV9ObVAtMWQ5MDJmNGQxZThhNzM1YS1QYXJ0XzINCkNvbnRlbnQtVHlwZTogYXBw
bGljYXRpb24vanNvbjsgY2hhcnNldD11dGY4DQpDb250ZW50LUVuY29kaW5nOiB1dGY4DQoNCnsg
InRva2VuLXBhcnQxIjogIjAwMWM1ZjJmOWIzODZlM2JlNmM5NTQ2Zjk3YWNiZTYzZjFhMTc3MzRk
ZDVhOTJhYTk2NmFjYTAwODU1ZSIgfQ0KLS0tLV9ObVAtMWQ5MDJmNGQxZThhNzM1YS1QYXJ0XzIt
LQ0KoIIK5DCCBI8wggQUoAMCAQICFGFq2BiR4ebW84K3/HrjMpkODKUtMAoGCCqGSM49BAMDMEkx
CzAJBgNVBAYTAkpQMQ4wDAYDVQQHDAVUb2tpbzEQMA4GA1UECgwHTXkuQ29ycDEYMBYGA1UEAwwP
Um9vdCBDQSAyMDIyIEcxMB4XDTIyMTEyNDIxNDAxN1oXDTI1MDUyODIxNDAxNlowSDELMAkGA1UE
BhMCSlAxDjAMBgNVBAcMBVRva2lvMRAwDgYDVQQKDAdNeS5Db3JwMRcwFQYDVQQDDA5TdWIgQ0Eg
MjAyMiBHMTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAIILhCsJsrzhrAELwxAbrKaH
ALeP7liQqUiPDbN+JfRvqyEBpBG8JBkGC76uM5TIQ+f+28IBIs0YwdjIiCNPeew/nAPZ1aNZEYco
vQ7G3CQ7mUAhFkeGKOVaS+2LOqvmdry5p1SKlC0ziw/Yf3dD1KSQ4sBPBa7gOp8pDpe9YKX/j/fX
k83iSTFvrp1LTe4BILMh9YzVAukKR5A3WgVkrR156gLoV6Rew6uSwHAOtl4JAYBclRohmvbjU/FP
k2kX8h6NK+V21HXD0inhyI/NnyVPxEO+n2isnrrtz/R6t2pUPn28xhNjbnFEBk8KZ4fQH23/BRtA
cIibh3dzetVRJdoVZEjoYmZdveMB6TqsJJtlSSHTVIKcWNUFKAiqS5wSHVaX2XnjdVgqkQU3j5kD
JBCwPG47KpoNdTTa4JWQwI9G57kGAvPomj+6ljYSavzfCeQ8nxwkoRhG0eXjalpOnjUPAxowqhmE
mRYTg7hzIFPXYInhFIvHwd4zFkPh8HOa7wIDAQABo4IBjjCCAYowEgYDVR0TAQH/BAgwBgEB/wIB
ADAOBgNVHQ8BAf8EBAMCAYYwHgYDVR0gBBcwFTAIBgZngQwBAgEwCQYHZ4EMAQUBAjB+BggrBgEF
BQcBAQRyMHAwbgYIKwYBBQUHMAKGYmh0dHA6Ly9hY21lLWNhLTAxLmRjLWJzZC5teS5jb3JwL2Rv
d25sb2FkL1Jvb3QvNTQ1MWYxOWZkZDg5MmNjYmU5YmU3ODM1ODc2NjcwZmQxYjY5OTllOC9jYS5j
cnQuY2VyMGwGA1UdHwRlMGMwYaBfoF2GW2h0dHA6Ly9hY21lLWNhLTAxLmRjLWJzZC5teS5jb3Jw
L2Rvd25sb2FkL1Jvb3QvNTQ1MWYxOWZkZDg5MmNjYmU5YmU3ODM1ODc2NjcwZmQxYjY5OTllOC5j
cmwwKwYDVR0jBCQwIoAgxGqTGFlSJtP48rVLeyMc58TIb5WWC/mBW4AKc0OmIm0wKQYDVR0OBCIE
INUBWh3giyFeu6hqGeExhD+S2sWYkeQcsGY6tX6AUgULMAoGCCqGSM49BAMDA2kAMGYCMQDW0NKq
k5JJl+DQvDBAVWZ99LFzKP2y9H8RHNynK2g+VlF6h2141+SWO+ev2GAFj5kCMQC8HuhmB1g+aYuP
wcuBtpOSZGrdTHXFmRubI/rb4K8rZTgP/FQXzbK81Mctv1V+AxgwggZNMIIEtaADAgECAhRTWC34
33N0r2EkwdKjLxrvITOOgDANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJKUDEOMAwGA1UEBwwF
VG9raW8xEDAOBgNVBAoMB015LkNvcnAxFzAVBgNVBAMMDlN1YiBDQSAyMDIyIEcxMB4XDTIyMTEy
NDIxNDExNVoXDTIzMDIyMjIxNDExNFowVzELMAkGA1UEBhMCSlAxDjAMBgNVBAcMBVRva2lvMRAw
DgYDVQQKDAdNeS5Db3JwMSYwJAYDVQQDFh1hY21lLWNoYWxsZW5nZUBkYy1ic2QubXkuY29ycDCC
AaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAInbq8HuIIHbaSgUP5jMSZuIt82r34Cbd4b6
C1OgzJWW2CY++t1wkwmAA4NRHZEesm3c6AsqfYbbvifdAlll4bDuQXAtMDphyICgllLqyvdhI7ya
FjhMzTyGHKXKztbjNyLwz9gFP/g6v/44EsPdagaFaVjFGeaBGr/JSih2cdBo8oICaEFhRymtARQP
95mu+A0bV16zE2JnSqvN6ivIrN2uHQjlo0uymWRXJsK3Jv3Xm71vnKiESclkYQHNXi7PyA1GuOEO
oxSln7FQhzXYYurGenjzg9WAemtPPVw3jcMg9rM1k+Lkns/VLj3VTXhDZg1Ye6CsmTpUEmVo3seu
vM2Y8GyTndmNt/tVAwiD77Ok5BRbSCBzQkopwMuzajg5PYXd0VGjhMUOxfgZavZGGFpjwPkEqY7s
hhSwp5Wmq5OBehpPmh47ILHgUrctjALcgWgc4gaLwTVeDOOvHA0IbQBAFqD1mxNb1i4gSEJc9dUa
lxc4LGa6ubhVLsNg9n7mdQIDAQABo4ICHjCCAhowCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBaAw
FgYDVR0lAQH/BAwwCgYIKwYBBQUHAwQwKAYDVR0RBCEwH4EdYWNtZS1jaGFsbGVuZ2VAZGMtYnNk
Lm15LmNvcnAwEwYDVR0gBAwwCjAIBgZngQwBAgIwgeAGCCsGAQUFBwEBBIHTMIHQMF8GCCsGAQUF
BzABhlNodHRwOi8vYWNtZS1jYS0wMS5kYy1ic2QubXkuY29ycC9vY3NwL1N1Yi82MTZhZDgxODkx
ZTFlNmQ2ZjM4MmI3ZmM3YWUzMzI5OTBlMGNhNTJkLzBtBggrBgEFBQcwAoZhaHR0cDovL2FjbWUt
Y2EtMDEuZGMtYnNkLm15LmNvcnAvZG93bmxvYWQvU3ViLzYxNmFkODE4OTFlMWU2ZDZmMzgyYjdm
YzdhZTMzMjk5MGUwY2E1MmQvY2EuY3J0LmNlcjBrBgNVHR8EZDBiMGCgXqBchlpodHRwOi8vYWNt
ZS1jYS0wMS5kYy1ic2QubXkuY29ycC9kb3dubG9hZC9TdWIvNjE2YWQ4MTg5MWUxZTZkNmYzODJi
N2ZjN2FlMzMyOTkwZTBjYTUyZC5jcmwwKwYDVR0jBCQwIoAg1QFaHeCLIV67qGoZ4TGEP5LaxZiR
5BywZjq1foBSBQswKQYDVR0OBCIEIFWVbzWpC2IKkBu6DBLEtmO/1GjEXKJaYigKZkkPYBsgMA0G
CSqGSIb3DQEBCwUAA4IBgQAFvMhQ3dpIYwXch5Op0v3qhnRpZFpZh/3DCjLAQt2d+O71nVGGt6Bj
jsiewHU6Dtl5zxDZfz9cQTaRhEWfXka2byFWKfYw3xq64cmr5a7+9RynvtrSBjx9a+IOVNAylyjK
xnNdWVZLa0Qna1Qqngz32i7aXxENuES2kZcBTnLDEaA3WBuGdlqBsFn1T0A6pP20/np8TS4Q17hR
GEAE9jrhcmRcbP7ABqDz7+Jcq0qnpXE8zJwm9DsR4HJ1Cudsm3iIz3BSDOchUMPPcOOG+JltYOus
eaheeNFDdyxKIx4TZkRwI+avl27DJ4O2n/OKqDQZTYcg/HxH2xWwNfq2/Z08YeH/I4xJnlPxZSDG
DjvVNcQMsAwOdG2O55Lq7q4wmp7ZH2JVA97vekBAJwltjyy3APDOeqi4CnzlaZcOn0GRg7MasQmF
AYbOlo8Ti8oe4U65w8Y2q4ip1EbiwfNMrAkcaAVGIJm2pTapp9nOraed68uMogF7xPhucJLA7Zer
WMMxggL9MIIC+QIBATBgMEgxCzAJBgNVBAYTAkpQMQ4wDAYDVQQHDAVUb2tpbzEQMA4GA1UECgwH
TXkuQ29ycDEXMBUGA1UEAwwOU3ViIENBIDIwMjIgRzECFFNYLfjfc3SvYSTB0qMvGu8hM46AMA0G
CWCGSAFlAwQCAQUAoIHvMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwLwYJKoZIhvcNAQkEMSIE
IIbuwptDlJ0DVnN9vhoyQl6l6BeOjhuVjyRkfKXrS2bSMIGhBgsqhkiG9w0BCRACNzGBkTGBjgoB
ATCBiDA8GgdTdWJqZWN0DC5BQ01FOiBBQnhmTDVzNGJqdm15VlJ2bDZ5LVlfR2hkelRkV3BLcWxt
cktBSVZlAgEAMB4aAlRvDBVnaXRsYWJAZGMtYnNkLm15LmNvcnACAQAwKBoERnJvbQwdYWNtZS1j
aGFsbGVuZ2VAZGMtYnNkLm15LmNvcnACAQAwDQYJKoZIhvcNAQELBQAEggGAGUyk3DnAEFJeB3xS
qbgx6NdbkP3G1ah9el01wEez4jQC1pvRe07dMApkJImlSxnOB5Lp/06a14YvAu/rGdmTIAZjeuXV
VPxo+urSTUj4dZB26HNKHyUwXvKWgIhfwlrc/kK+sXyYXk8cNNtAuOQlTJyU1B1NG5nFKTi4UOZI
9B1Lc0MktnrAntB70bOVcT0Lz5Qgslc8jwIUePJLyGKohKTe/744QPqZAz7nQaXS9E30yd1oTB7I
FH4Fg9GjWnD8rLvdCR/S54zEqsdIX/YCmky5DzyjLthY1Al4AJwaodd5gPQn2xhtkxssHLg48/X+
3Se+JTyad6MXmMixnlJFL8DoL8r/BcfZbT4b8oLWMrbURooMPVnxYbTx67IOGxuIYFolM9d9F39P
GBcFkTGIbcVUOIyV2vGH0fXOiVx4ktm4j3Ds843KZ8hj52SZr7RBAsgileucs713UOjfhrQqkbZh
nF5r9fOKs8ky8tSePQ8izk/jXC6PY5D7xyELoPrh
----_NmP-1d902f4d1e8a735a-Part_1--