All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.shredzone.acme4j.smime.email.EmailProcessor Maven / Gradle / Ivy

The newest version!
/*
 * acme4j - Java ACME client
 *
 * Copyright (C) 2021 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.email;

import static java.util.Collections.unmodifiableCollection;
import static java.util.Objects.requireNonNull;

import java.net.URL;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.PKIXParameters;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;

import edu.umd.cs.findbugs.annotations.Nullable;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.Session;
import jakarta.mail.internet.InternetAddress;
import org.shredzone.acme4j.Identifier;
import org.shredzone.acme4j.Login;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.smime.challenge.EmailReply00Challenge;
import org.shredzone.acme4j.smime.exception.AcmeInvalidMessageException;
import org.shredzone.acme4j.smime.wrapper.Mail;
import org.shredzone.acme4j.smime.wrapper.SignedMailBuilder;
import org.shredzone.acme4j.smime.wrapper.SimpleMail;

/**
 * A processor for incoming "Challenge" emails.
 *
 * @see RFC 8823
 * @since 2.12
 */
public final class EmailProcessor {
    private static final Pattern SUBJECT_PATTERN = Pattern.compile("ACME:\\s+([0-9A-Za-z_\\s-]+=?)\\s*");

    private final InternetAddress sender;
    private final InternetAddress recipient;
    private final @Nullable String messageId;
    private final Collection replyTo;
    private final String token1;
    private final AtomicReference challengeRef = new AtomicReference<>();

    /**
     * Processes the given plain e-mail message.
     * 

* Note that according to RFC-8823, the challenge message must be signed using either * DKIM or S/MIME. This method does not do any DKIM or S/MIME validation, and assumes * that this has already been done in a previous stage. * * @param message * E-mail that was received from the CA. The inbound MTA has already taken * care of DKIM and/or S/MIME validation. * @return EmailProcessor for this e-mail * @throws AcmeInvalidMessageException * if a validation failed, and the message must be rejected. * @since 2.15 */ public static EmailProcessor plainMessage(Message message) throws AcmeInvalidMessageException { return builder().skipVerification().build(message); } /** * Processes the given signed e-mail message. *

* This method expects an S/MIME signed message. The signature must use a certificate * that can be validated using Java's cacert truststore. Strict validation rules are * applied. *

* Use the {@link #builder()} method if you need to configure the validation process. * * @param message * S/MIME signed e-mail that was received from the CA. * @return EmailProcessor for this e-mail * @throws AcmeInvalidMessageException * if a validation failed, and the message must be rejected. * @since 2.16 */ public static EmailProcessor signedMessage(Message message) throws AcmeInvalidMessageException { return builder().build(message); } /** * Creates a {@link Builder} for building an {@link EmailProcessor} with individual * configuration. * * @since 2.16 */ public static Builder builder() { return new Builder(); } /** * Creates a new {@link EmailProcessor} for the incoming "Challenge" message. *

* The incoming message is validated against the requirements of RFC-8823. * * @param message * "Challenge" message as it was sent by the CA. * @throws AcmeInvalidMessageException * if a validation failed, and the message must be rejected. */ private EmailProcessor(Mail message) throws AcmeInvalidMessageException { if (!message.isAutoSubmitted()) { throw new AcmeInvalidMessageException("Message is not auto-generated"); } var subject = message.getSubject(); var m = SUBJECT_PATTERN.matcher(subject); if (!m.matches()) { throw new AcmeProtocolException("Invalid subject: " + subject); } // white spaces within the token part must be ignored this.token1 = m.group(1).replaceAll("\\s+", ""); this.sender = message.getFrom(); this.recipient = message.getTo(); this.messageId = message.getMessageId().orElse(null); this.replyTo = message.getReplyTo(); } /** * The expected sender of the "challenge" email. *

* The sender is usually checked when the {@link EmailReply00Challenge} is passed into * the processor, but you can also manually check the sender here. * * @param expectedSender * The expected sender of the "challenge" email. * @return itself * @throws AcmeProtocolException * if the expected sender does not match */ public EmailProcessor expectedFrom(InternetAddress expectedSender) { requireNonNull(expectedSender, "expectedSender"); if (!sender.equals(expectedSender)) { throw new AcmeProtocolException("Message is not sent by the expected sender"); } return this; } /** * The expected recipient of the "challenge" email. *

* This must be the email address of the entity that requested the S/MIME certificate. * The check is not performed by the processor, but should be performed by * the client. * * @param expectedRecipient * The expected recipient of the "challenge" email. * @return itself * @throws AcmeProtocolException * if the expected recipient does not match */ public EmailProcessor expectedTo(InternetAddress expectedRecipient) { requireNonNull(expectedRecipient, "expectedRecipient"); if (!recipient.equals(expectedRecipient)) { throw new AcmeProtocolException("Message is not addressed to expected recipient"); } return this; } /** * The expected identifier. *

* This must be the email address of the entity that requested the S/MIME certificate. * The check is not performed by the processor, but should be performed by * the client. * * @param expectedIdentifier * The expected identifier for the S/MIME certificate. Usually this is an * {@link org.shredzone.acme4j.smime.EmailIdentifier} instance. * @return itself * @throws AcmeProtocolException * if the expected identifier is not an email identifier, or does not match */ public EmailProcessor expectedIdentifier(Identifier expectedIdentifier) { requireNonNull(expectedIdentifier, "expectedIdentifier"); if (!"email".equals(expectedIdentifier.getType())) { throw new AcmeProtocolException("Wrong identifier type: " + expectedIdentifier.getType()); } try { expectedTo(new InternetAddress(expectedIdentifier.getValue())); } catch (MessagingException ex) { throw new AcmeProtocolException("Invalid email address", ex); } return this; } /** * Returns the sender of the "challenge" email. */ public InternetAddress getSender() { return (InternetAddress) sender.clone(); } /** * Returns the recipient of the "challenge" email. */ public InternetAddress getRecipient() { return (InternetAddress) recipient.clone(); } /** * Returns all "reply-to" email addresses found in the "challenge" email. *

* Empty if there was no reply-to header, but never {@code null}. */ public Collection getReplyTo() { return unmodifiableCollection(replyTo); } /** * Returns the message-id of the "challenge" email. *

* Empty if the challenge email has no message-id. */ public Optional getMessageId() { return Optional.ofNullable(messageId); } /** * Returns the "token 1" found in the subject of the "challenge" email. */ public String getToken1() { return token1; } /** * Sets the corresponding {@link EmailReply00Challenge} that was received from the CA * for validation. * * @param challenge * {@link EmailReply00Challenge} that corresponds to this email * @return itself * @throws AcmeProtocolException * if the challenge does not match this "challenge" email. */ public EmailProcessor withChallenge(EmailReply00Challenge challenge) { requireNonNull(challenge, "challenge"); expectedFrom(challenge.getExpectedSender()); if (challengeRef.get() != null) { throw new IllegalStateException("A challenge has already been set"); } challengeRef.set(challenge); return this; } /** * Sets the corresponding {@link EmailReply00Challenge} that was received from the CA * for validation. *

* This is a convenience call in case that only the challenge location URL is * available. * * @param login * A valid {@link Login} * @param challengeLocation * The location URL of the corresponding challenge. * @return itself * @throws AcmeProtocolException * if the challenge does not match this "challenge" email. */ public EmailProcessor withChallenge(Login login, URL challengeLocation) { return withChallenge(login.bindChallenge(challengeLocation, EmailReply00Challenge.class)); } /** * Returns the full token of this challenge. *

* The corresponding email-reply-00 challenge must be set before. */ public String getToken() { checkChallengePresent(); return challengeRef.get().getToken(getToken1()); } /** * Returns the key-authorization of this challenge. This is the response to be used in * the response email. *

* The corresponding email-reply-00 challenge must be set before. */ public String getAuthorization() { checkChallengePresent(); return challengeRef.get().getAuthorization(getToken1()); } /** * Returns a {@link ResponseGenerator} for generating a response email. *

* The corresponding email-reply-00 challenge must be set before. */ public ResponseGenerator respond() { checkChallengePresent(); return new ResponseGenerator(this); } /** * Checks if a challenge has been set. Throws an exception if not. */ private void checkChallengePresent() { if (challengeRef.get() == null) { throw new IllegalStateException("No challenge has been set yet"); } } /** * A builder for {@link EmailProcessor}. *

* Use {@link EmailProcessor#builder()} to generate an instance. * * @since 2.16 */ public static class Builder { private boolean unsigned = false; private final SignedMailBuilder builder = new SignedMailBuilder(); private Builder() { // Private constructor } /** * Skips signature and header verification. Use only if the message has already * been verified in a previous stage (e.g. by the MTA) or for testing purposes. */ public Builder skipVerification() { this.unsigned = true; return this; } /** * Uses the standard cacerts truststore for signature verification. This is the * default. */ public Builder caCerts() { builder.withCaCertsTrustStore(); return this; } /** * 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. */ public Builder trustStore(KeyStore trustStore) { try { builder.withTrustStore(trustStore); } catch (KeyStoreException | InvalidAlgorithmParameterException ex) { throw new IllegalArgumentException("Cannot use trustStore", ex); } return this; } /** * 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 */ public Builder certificate(X509Certificate certificate) { builder.withSignCert(certificate); return this; } /** * Uses the given {@link PKIXParameters}. * * @param param * {@link PKIXParameters} to be used for signature verification. */ public Builder pkixParameters(PKIXParameters param) { builder.withPKIXParameters(param); return this; } /** * Uses the given mail {@link Session} for accessing the signed message body. A * simple default session is used otherwise, which is usually sufficient. * * @param session * {@link Session} to be used for accessing the message body. */ public Builder mailSession(Session session) { builder.withMailSession(session); return this; } /** * Performs strict checks. Secured headers must exactly match their unsecured * counterparts. This is the default. */ public Builder strict() { builder.relaxed(false); return this; } /** * Performs relaxed checks. Secured headers might differ in whitespaces or case of * the field names. Use this if your MTA has mangled the envelope header. */ public Builder relaxed() { builder.relaxed(true); return this; } /** * Builds an {@link EmailProcessor} for the given {@link Message} using the * current configuration. * * @param message * {@link Message} to create an {@link EmailProcessor} for. * @return The generated {@link EmailProcessor} * @throws AcmeInvalidMessageException * if the message fails to be verified. If this exception is thrown, the * message MUST be rejected, and MUST NOT be used for certification. */ public EmailProcessor build(Message message) throws AcmeInvalidMessageException { if (unsigned) { return new EmailProcessor(new SimpleMail(message)); } else { return new EmailProcessor(builder.build(message)); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy