mirror of https://github.com/shred/acme4j
Add unit tests
- Unit tests for RFC-7508 type signatures - Unit tests for evaluation of trusted headerspull/134/head
parent
2118fb8593
commit
aae98d7ce8
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
@ -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-----
|
||||
<fill in challengeResponse here>
|
||||
-----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--
|
Loading…
Reference in New Issue