001/* 002 * acme4j - Java ACME client 003 * 004 * Copyright (C) 2021 Richard "Shred" Körber 005 * http://acme4j.shredzone.org 006 * 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 013 */ 014package org.shredzone.acme4j.smime.email; 015 016import static java.util.Collections.unmodifiableCollection; 017import static java.util.Objects.requireNonNull; 018 019import java.net.URL; 020import java.security.InvalidAlgorithmParameterException; 021import java.security.KeyStore; 022import java.security.KeyStoreException; 023import java.security.cert.PKIXParameters; 024import java.security.cert.X509Certificate; 025import java.util.Collection; 026import java.util.Optional; 027import java.util.concurrent.atomic.AtomicReference; 028import java.util.regex.Pattern; 029 030import edu.umd.cs.findbugs.annotations.Nullable; 031import jakarta.mail.Message; 032import jakarta.mail.MessagingException; 033import jakarta.mail.Session; 034import jakarta.mail.internet.InternetAddress; 035import org.shredzone.acme4j.Identifier; 036import org.shredzone.acme4j.Login; 037import org.shredzone.acme4j.exception.AcmeProtocolException; 038import org.shredzone.acme4j.smime.challenge.EmailReply00Challenge; 039import org.shredzone.acme4j.smime.exception.AcmeInvalidMessageException; 040import org.shredzone.acme4j.smime.wrapper.Mail; 041import org.shredzone.acme4j.smime.wrapper.SignedMailBuilder; 042import org.shredzone.acme4j.smime.wrapper.SimpleMail; 043 044/** 045 * A processor for incoming "Challenge" emails. 046 * 047 * @see <a href="https://datatracker.ietf.org/doc/html/rfc8823">RFC 8823</a> 048 * @since 2.12 049 */ 050public final class EmailProcessor { 051 private static final Pattern SUBJECT_PATTERN = Pattern.compile("ACME:\\s+([0-9A-Za-z_\\s-]+=?)\\s*"); 052 053 private final InternetAddress sender; 054 private final InternetAddress recipient; 055 private final @Nullable String messageId; 056 private final Collection<InternetAddress> replyTo; 057 private final String token1; 058 private final AtomicReference<EmailReply00Challenge> challengeRef = new AtomicReference<>(); 059 060 /** 061 * Processes the given plain e-mail message. 062 * <p> 063 * Note that according to RFC-8823, the challenge message must be signed using either 064 * DKIM or S/MIME. This method does not do any DKIM or S/MIME validation, and assumes 065 * that this has already been done in a previous stage. 066 * 067 * @param message 068 * E-mail that was received from the CA. The inbound MTA has already taken 069 * care of DKIM and/or S/MIME validation. 070 * @return EmailProcessor for this e-mail 071 * @throws AcmeInvalidMessageException 072 * if a validation failed, and the message <em>must</em> be rejected. 073 * @since 2.15 074 */ 075 public static EmailProcessor plainMessage(Message message) 076 throws AcmeInvalidMessageException { 077 return builder().skipVerification().build(message); 078 } 079 080 /** 081 * Processes the given signed e-mail message. 082 * <p> 083 * This method expects an S/MIME signed message. The signature must use a certificate 084 * that can be validated using Java's cacert truststore. Strict validation rules are 085 * applied. 086 * <p> 087 * Use the {@link #builder()} method if you need to configure the validation process. 088 * 089 * @param message 090 * S/MIME signed e-mail that was received from the CA. 091 * @return EmailProcessor for this e-mail 092 * @throws AcmeInvalidMessageException 093 * if a validation failed, and the message <em>must</em> be rejected. 094 * @since 2.16 095 */ 096 public static EmailProcessor signedMessage(Message message) 097 throws AcmeInvalidMessageException { 098 return builder().build(message); 099 } 100 101 /** 102 * Creates a {@link Builder} for building an {@link EmailProcessor} with individual 103 * configuration. 104 * 105 * @since 2.16 106 */ 107 public static Builder builder() { 108 return new Builder(); 109 } 110 111 /** 112 * Creates a new {@link EmailProcessor} for the incoming "Challenge" message. 113 * <p> 114 * The incoming message is validated against the requirements of RFC-8823. 115 * 116 * @param message 117 * "Challenge" message as it was sent by the CA. 118 * @throws AcmeInvalidMessageException 119 * if a validation failed, and the message <em>must</em> be rejected. 120 */ 121 private EmailProcessor(Mail message) throws AcmeInvalidMessageException { 122 if (!message.isAutoSubmitted()) { 123 throw new AcmeInvalidMessageException("Message is not auto-generated"); 124 } 125 126 var subject = message.getSubject(); 127 var m = SUBJECT_PATTERN.matcher(subject); 128 if (!m.matches()) { 129 throw new AcmeProtocolException("Invalid subject: " + subject); 130 } 131 // white spaces within the token part must be ignored 132 this.token1 = m.group(1).replaceAll("\\s+", ""); 133 134 this.sender = message.getFrom(); 135 this.recipient = message.getTo(); 136 this.messageId = message.getMessageId().orElse(null); 137 this.replyTo = message.getReplyTo(); 138 } 139 140 /** 141 * The expected sender of the "challenge" email. 142 * <p> 143 * The sender is usually checked when the {@link EmailReply00Challenge} is passed into 144 * the processor, but you can also manually check the sender here. 145 * 146 * @param expectedSender 147 * The expected sender of the "challenge" email. 148 * @return itself 149 * @throws AcmeProtocolException 150 * if the expected sender does not match 151 */ 152 public EmailProcessor expectedFrom(InternetAddress expectedSender) { 153 requireNonNull(expectedSender, "expectedSender"); 154 if (!sender.equals(expectedSender)) { 155 throw new AcmeProtocolException("Message is not sent by the expected sender"); 156 } 157 return this; 158 } 159 160 /** 161 * The expected recipient of the "challenge" email. 162 * <p> 163 * This must be the email address of the entity that requested the S/MIME certificate. 164 * The check is not performed by the processor, but <em>should</em> be performed by 165 * the client. 166 * 167 * @param expectedRecipient 168 * The expected recipient of the "challenge" email. 169 * @return itself 170 * @throws AcmeProtocolException 171 * if the expected recipient does not match 172 */ 173 public EmailProcessor expectedTo(InternetAddress expectedRecipient) { 174 requireNonNull(expectedRecipient, "expectedRecipient"); 175 if (!recipient.equals(expectedRecipient)) { 176 throw new AcmeProtocolException("Message is not addressed to expected recipient"); 177 } 178 return this; 179 } 180 181 /** 182 * The expected identifier. 183 * <p> 184 * This must be the email address of the entity that requested the S/MIME certificate. 185 * The check is not performed by the processor, but <em>should</em> be performed by 186 * the client. 187 * 188 * @param expectedIdentifier 189 * The expected identifier for the S/MIME certificate. Usually this is an 190 * {@link org.shredzone.acme4j.smime.EmailIdentifier} instance. 191 * @return itself 192 * @throws AcmeProtocolException 193 * if the expected identifier is not an email identifier, or does not match 194 */ 195 public EmailProcessor expectedIdentifier(Identifier expectedIdentifier) { 196 requireNonNull(expectedIdentifier, "expectedIdentifier"); 197 if (!"email".equals(expectedIdentifier.getType())) { 198 throw new AcmeProtocolException("Wrong identifier type: " + expectedIdentifier.getType()); 199 } 200 try { 201 expectedTo(new InternetAddress(expectedIdentifier.getValue())); 202 } catch (MessagingException ex) { 203 throw new AcmeProtocolException("Invalid email address", ex); 204 } 205 return this; 206 } 207 208 /** 209 * Returns the sender of the "challenge" email. 210 */ 211 public InternetAddress getSender() { 212 return (InternetAddress) sender.clone(); 213 } 214 215 /** 216 * Returns the recipient of the "challenge" email. 217 */ 218 public InternetAddress getRecipient() { 219 return (InternetAddress) recipient.clone(); 220 } 221 222 /** 223 * Returns all "reply-to" email addresses found in the "challenge" email. 224 * <p> 225 * Empty if there was no reply-to header, but never {@code null}. 226 */ 227 public Collection<InternetAddress> getReplyTo() { 228 return unmodifiableCollection(replyTo); 229 } 230 231 /** 232 * Returns the message-id of the "challenge" email. 233 * <p> 234 * Empty if the challenge email has no message-id. 235 */ 236 public Optional<String> getMessageId() { 237 return Optional.ofNullable(messageId); 238 } 239 240 /** 241 * Returns the "token 1" found in the subject of the "challenge" email. 242 */ 243 public String getToken1() { 244 return token1; 245 } 246 247 /** 248 * Sets the corresponding {@link EmailReply00Challenge} that was received from the CA 249 * for validation. 250 * 251 * @param challenge 252 * {@link EmailReply00Challenge} that corresponds to this email 253 * @return itself 254 * @throws AcmeProtocolException 255 * if the challenge does not match this "challenge" email. 256 */ 257 public EmailProcessor withChallenge(EmailReply00Challenge challenge) { 258 requireNonNull(challenge, "challenge"); 259 expectedFrom(challenge.getExpectedSender()); 260 if (challengeRef.get() != null) { 261 throw new IllegalStateException("A challenge has already been set"); 262 } 263 challengeRef.set(challenge); 264 return this; 265 } 266 267 /** 268 * Sets the corresponding {@link EmailReply00Challenge} that was received from the CA 269 * for validation. 270 * <p> 271 * This is a convenience call in case that only the challenge location URL is 272 * available. 273 * 274 * @param login 275 * A valid {@link Login} 276 * @param challengeLocation 277 * The location URL of the corresponding challenge. 278 * @return itself 279 * @throws AcmeProtocolException 280 * if the challenge does not match this "challenge" email. 281 */ 282 public EmailProcessor withChallenge(Login login, URL challengeLocation) { 283 return withChallenge(login.bindChallenge(challengeLocation, EmailReply00Challenge.class)); 284 } 285 286 /** 287 * Returns the full token of this challenge. 288 * <p> 289 * The corresponding email-reply-00 challenge must be set before. 290 */ 291 public String getToken() { 292 checkChallengePresent(); 293 return challengeRef.get().getToken(getToken1()); 294 } 295 296 /** 297 * Returns the key-authorization of this challenge. This is the response to be used in 298 * the response email. 299 * <p> 300 * The corresponding email-reply-00 challenge must be set before. 301 */ 302 public String getAuthorization() { 303 checkChallengePresent(); 304 return challengeRef.get().getAuthorization(getToken1()); 305 } 306 307 /** 308 * Returns a {@link ResponseGenerator} for generating a response email. 309 * <p> 310 * The corresponding email-reply-00 challenge must be set before. 311 */ 312 public ResponseGenerator respond() { 313 checkChallengePresent(); 314 return new ResponseGenerator(this); 315 } 316 317 /** 318 * Checks if a challenge has been set. Throws an exception if not. 319 */ 320 private void checkChallengePresent() { 321 if (challengeRef.get() == null) { 322 throw new IllegalStateException("No challenge has been set yet"); 323 } 324 } 325 326 /** 327 * A builder for {@link EmailProcessor}. 328 * <p> 329 * Use {@link EmailProcessor#builder()} to generate an instance. 330 * 331 * @since 2.16 332 */ 333 public static class Builder { 334 private boolean unsigned = false; 335 private final SignedMailBuilder builder = new SignedMailBuilder(); 336 337 private Builder() { 338 // Private constructor 339 } 340 341 /** 342 * Skips signature and header verification. Use only if the message has already 343 * been verified in a previous stage (e.g. by the MTA) or for testing purposes. 344 */ 345 public Builder skipVerification() { 346 this.unsigned = true; 347 return this; 348 } 349 350 /** 351 * Uses the standard cacerts truststore for signature verification. This is the 352 * default. 353 */ 354 public Builder caCerts() { 355 builder.withCaCertsTrustStore(); 356 return this; 357 } 358 359 /** 360 * Uses the given truststore for signature verification. 361 * <p> 362 * This is for self-signed certificates. No revocation checks will take place. 363 * 364 * @param trustStore 365 * {@link KeyStore} of the truststore to be used. 366 */ 367 public Builder trustStore(KeyStore trustStore) { 368 try { 369 builder.withTrustStore(trustStore); 370 } catch (KeyStoreException | InvalidAlgorithmParameterException ex) { 371 throw new IllegalArgumentException("Cannot use trustStore", ex); 372 } 373 return this; 374 } 375 376 /** 377 * Uses the given certificate for signature verification. 378 * <p> 379 * This is for self-signed certificates. No revocation checks will take place. 380 * 381 * @param certificate 382 * {@link X509Certificate} of the CA 383 */ 384 public Builder certificate(X509Certificate certificate) { 385 builder.withSignCert(certificate); 386 return this; 387 } 388 389 /** 390 * Uses the given {@link PKIXParameters}. 391 * 392 * @param param 393 * {@link PKIXParameters} to be used for signature verification. 394 */ 395 public Builder pkixParameters(PKIXParameters param) { 396 builder.withPKIXParameters(param); 397 return this; 398 } 399 400 /** 401 * Uses the given mail {@link Session} for accessing the signed message body. A 402 * simple default session is used otherwise, which is usually sufficient. 403 * 404 * @param session 405 * {@link Session} to be used for accessing the message body. 406 */ 407 public Builder mailSession(Session session) { 408 builder.withMailSession(session); 409 return this; 410 } 411 412 /** 413 * Performs strict checks. Secured headers must exactly match their unsecured 414 * counterparts. This is the default. 415 */ 416 public Builder strict() { 417 builder.relaxed(false); 418 return this; 419 } 420 421 /** 422 * Performs relaxed checks. Secured headers might differ in whitespaces or case of 423 * the field names. Use this if your MTA has mangled the envelope header. 424 */ 425 public Builder relaxed() { 426 builder.relaxed(true); 427 return this; 428 } 429 430 /** 431 * Builds an {@link EmailProcessor} for the given {@link Message} using the 432 * current configuration. 433 * 434 * @param message 435 * {@link Message} to create an {@link EmailProcessor} for. 436 * @return The generated {@link EmailProcessor} 437 * @throws AcmeInvalidMessageException 438 * if the message fails to be verified. If this exception is thrown, the 439 * message MUST be rejected, and MUST NOT be used for certification. 440 */ 441 public EmailProcessor build(Message message) throws AcmeInvalidMessageException { 442 if (unsigned) { 443 return new EmailProcessor(new SimpleMail(message)); 444 } else { 445 return new EmailProcessor(builder.build(message)); 446 } 447 } 448 } 449 450}