package com.mailserver.smtp; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.GeneralNames; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermissions; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.SecureRandom; import java.security.cert.X509Certificate; import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.Base64; import java.util.Date; /** * Certificate for SMTP TLS (STARTTLS and implicit TLS): * */ public final class SmtpTls { private static final Logger log = LoggerFactory.getLogger(SmtpTls.class); static final String CERT_FILE = "smtp-cert.pem"; static final String KEY_FILE = "smtp-key.pem"; static final Duration RELOAD_CHECK = Duration.ofMinutes(1); private SmtpTls() { } public static SslContext serverContext(String certFile, String keyFile, Path dir, String hostname) throws Exception { boolean givenCert = certFile != null && !certFile.isBlank(); boolean givenKey = keyFile != null && !keyFile.isBlank(); if (givenCert != givenKey) { throw new IllegalStateException("TLS: set both the certificate and the key file, or neither"); } Path cert; Path key; if (givenCert) { cert = Path.of(certFile); key = Path.of(keyFile); log.info("SMTP TLS: certificate {}", cert); } else { cert = dir.resolve(CERT_FILE); key = dir.resolve(KEY_FILE); if (!Files.exists(cert) || !Files.exists(key)) { selfSigned(hostname, cert, key); log.warn("SMTP TLS: created a self-signed certificate for {} in {}", hostname, dir); } else { log.info("SMTP TLS: self-signed certificate {}", cert); } } // Read again when the files change (a renewed Let's Encrypt certificate) — no restart needed return SslContextBuilder.forServer(new ReloadingKeyManager(cert, key, RELOAD_CHECK, Clock.systemUTC())) .protocols("TLSv1.3", "TLSv1.2") .build(); } static void selfSigned(String hostname, Path certFile, Path keyFile) throws Exception { KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); generator.initialize(2048, new SecureRandom()); KeyPair pair = generator.generateKeyPair(); X500Name name = new X500Name("CN=" + hostname); Instant now = Instant.now(); JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(name, new BigInteger(64, new SecureRandom()), Date.from(now.minus(Duration.ofDays(1))), Date.from(now.plus(Duration.ofDays(3650))), name, pair.getPublic()); builder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(new GeneralName(GeneralName.dNSName, hostname))); X509Certificate certificate = new JcaX509CertificateConverter() .getCertificate(builder.build(new JcaContentSignerBuilder("SHA256withRSA").build(pair.getPrivate()))); Files.createDirectories(keyFile.toAbsolutePath().getParent()); write(keyFile, pem("PRIVATE KEY", pair.getPrivate().getEncoded()), true); write(certFile, pem("CERTIFICATE", certificate.getEncoded()), false); } private static String pem(String type, byte[] der) { return "-----BEGIN " + type + "-----\n" + Base64.getMimeEncoder(64, new byte[]{'\n'}).encodeToString(der) + "\n-----END " + type + "-----\n"; } private static void write(Path file, String text, boolean secret) throws IOException { Files.writeString(file, text, StandardCharsets.US_ASCII); if (secret) { try { Files.setPosixFilePermissions(file, PosixFilePermissions.fromString("rw-------")); } catch (UnsupportedOperationException ignored) { // not a POSIX file system } } } }