package com.mailserver.service; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyPairGenerator; import java.security.interfaces.RSAPrivateKey; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; class DkimServiceTest { private static final String MESSAGE = "From: Alice \r\nTo: bob@theirs.test\r\nSubject: Hello\r\n" + "Date: Mon, 2 Nov 2026 10:00:00 +0000\r\nMessage-ID: <1@ours.test>\r\nMIME-Version: 1.0\r\n" + "Content-Type: text/plain; charset=UTF-8\r\n\r\nHello Bob,\r\nsee you.\r\n"; @TempDir Path keys; /** Signer and a verifier that finds the signer's DNS record — like the receiving side would. */ private DkimService[] pair() { DkimService signer = new DkimService("ours.test", "mail", keys, true, (method, selector, token) -> List.of()); signer.init(); DkimService verifier = new DkimService("other.test", "mail", keys.resolve("verifier"), false, (method, selector, token) -> "mail".contentEquals(selector) && "ours.test".contentEquals(token) ? List.of(signer.dnsRecord()) : List.of()); return new DkimService[]{signer, verifier}; } @Test void signatureFromOurDomainVerifies() throws Exception { DkimService[] p = pair(); byte[] signed = p[0].sign(MESSAGE.getBytes(StandardCharsets.UTF_8)); String text = new String(signed, StandardCharsets.UTF_8); assertThat(text).startsWith("DKIM-Signature:").contains("c=relaxed/relaxed").contains("a=rsa-sha256") .contains("d=ours.test").contains("s=mail"); DkimService.Verification result = p[1].verify(signed); assertThat(result.signed()).isTrue(); assertThat(result.passed()).isTrue(); assertThat(result.domain()).isEqualTo("ours.test"); } @Test void changedBodyFailsVerification() throws Exception { DkimService[] p = pair(); String signed = new String(p[0].sign(MESSAGE.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8); byte[] tampered = signed.replace("see you.", "send money.").getBytes(StandardCharsets.UTF_8); DkimService.Verification result = p[1].verify(tampered); assertThat(result.signed()).isTrue(); assertThat(result.passed()).isFalse(); } @Test void unsignedMessageIsReportedAsNone() { DkimService.Verification result = pair()[1].verify(MESSAGE.getBytes(StandardCharsets.UTF_8)); assertThat(result.signed()).isFalse(); } @Test void mailFromAnotherDomainIsNotSigned() throws Exception { byte[] foreign = MESSAGE.replace("alice@ours.test", "eve@elsewhere.test").getBytes(StandardCharsets.UTF_8); assertThat(pair()[0].sign(foreign)).isEqualTo(foreign); } @Test void keyIsKeptAcrossRestartsAndOldPkcs1FilesLoad() throws Exception { DkimService first = new DkimService("ours.test", "mail", keys, true, (m, s, t) -> List.of()); first.init(); DkimService second = new DkimService("ours.test", "mail", keys, true, (m, s, t) -> List.of()); second.init(); assertThat(second.dnsRecord()).isEqualTo(first.dnsRecord()).startsWith("v=DKIM1; k=rsa; p="); // the previous version wrote "BEGIN RSA PRIVATE KEY" (PKCS#1) and could not read it back Path legacy = keys.resolve("legacy"); Files.createDirectories(legacy); KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); generator.initialize(2048); RSAPrivateKey key = (RSAPrivateKey) generator.generateKeyPair().getPrivate(); try (var writer = Files.newBufferedWriter(legacy.resolve("dkim-private.pem")); var pem = new org.bouncycastle.openssl.jcajce.JcaPEMWriter(writer)) { pem.writeObject(key); } assertThat(Files.readString(legacy.resolve("dkim-private.pem"))).contains("BEGIN RSA PRIVATE KEY"); DkimService loaded = new DkimService("ours.test", "mail", legacy, true, (m, s, t) -> List.of()); loaded.init(); assertThat(loaded.dnsRecord()).isNotNull(); } }