This is an automated email from the ASF dual-hosted git repository. chibenwa pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-jdkim.git
commit f19a5c499299cd35921f7b92ecdc086526091bfb Author: Benoit TELLIER <[email protected]> AuthorDate: Fri Oct 30 08:51:32 2026 +0100 [FIX] DKIMVerifier: do not select the verified field for a DKIM-Signature h= entry signatureCheck picks the instances of each h= name bottom-up among the message fields. When signing, the new DKIM-Signature field is not there yet; when verifying, it is, and it was picked once the earlier DKIM-Signature fields were used up, so a signature whose h= listed DKIM-Signature more often than there were earlier signatures never verified, jDKIM's own included. The field under verification is hashed implicitly as the last field (RFC 6376 section 3.5), so it is now left out of the instances offered for DKIM-Signature. Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .../java/org/apache/james/jdkim/DKIMVerifier.java | 32 ++++++++++++++++++++-- .../test/java/org/apache/james/jdkim/DKIMTest.java | 31 +++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/main/src/main/java/org/apache/james/jdkim/DKIMVerifier.java b/main/src/main/java/org/apache/james/jdkim/DKIMVerifier.java index fdec5ae..f15e289 100644 --- a/main/src/main/java/org/apache/james/jdkim/DKIMVerifier.java +++ b/main/src/main/java/org/apache/james/jdkim/DKIMVerifier.java @@ -258,7 +258,8 @@ public class DKIMVerifier { // One job per field, even for byte-identical fields: each one gets its own result. List<CompoundBodyHasher.Job> jobs = new ArrayList<>(); int processedSignatures = 0; - for (String signatureField : fields) { + for (int signatureIndex = 0; signatureIndex < fields.size(); signatureIndex++) { + String signatureField = fields.get(signatureIndex); CompoundBodyHasher.Job job = new CompoundBodyHasher.Job(signatureField); jobs.add(job); // RFC 6376 section 6.1: verifiers MAY limit the number of signatures they try to verify. @@ -314,7 +315,7 @@ public class DKIMVerifier { // decoded now so that verify(CompoundBodyHasher) cannot fail on it signatureRecord.getBodyHash(); byte[] decoded = signatureRecord.getSignature(); - signatureVerify(messageHeaders, signatureRecord, decoded, + signatureVerify(withoutSignature(messageHeaders, signatureIndex), signatureRecord, decoded, publicKeyRecord, signedHeadersList, signatureField.substring(0, pos)); } catch (IllegalArgumentException e) { throw new PermFailException("Invalid signature record: " + e.getMessage(), signatureRecord, e); @@ -342,6 +343,33 @@ public class DKIMVerifier { return new CompoundBodyHasher(jobs); } + /** + * The field being verified is hashed implicitly, as the last field (RFC 6376 + * section 3.5): like the signer, which did not have it yet, a DKIM-Signature + * entry of h= only selects the other DKIM-Signature fields. + * + * @param headers the message headers + * @param signatureIndex the position of the verified field among the + * DKIM-Signature fields + */ + private static Headers withoutSignature(Headers headers, int signatureIndex) { + return new Headers() { + public List<String> getFields() { + return headers.getFields(); + } + + public List<String> getFields(String name) { + List<String> fields = headers.getFields(name); + if (!"DKIM-Signature".equalsIgnoreCase(name) || signatureIndex >= fields.size()) { + return fields; + } + List<String> others = new ArrayList<>(fields); + others.remove(signatureIndex); + return others; + } + }; + } + /** * Verifies all of the DKIM-Signature records declared in the Headers * object. diff --git a/main/src/test/java/org/apache/james/jdkim/DKIMTest.java b/main/src/test/java/org/apache/james/jdkim/DKIMTest.java index 3380821..cc8252d 100644 --- a/main/src/test/java/org/apache/james/jdkim/DKIMTest.java +++ b/main/src/test/java/org/apache/james/jdkim/DKIMTest.java @@ -412,4 +412,35 @@ public class DKIMTest { assertThat(verifier.getResults()).extracting(Result::isSuccess).containsExactly(false, true, false); } + + @Test + public void signatureListingDkimSignatureInHShouldVerify() throws Exception { + DKIMSigner signer = new DKIMSigner("v=1; a=rsa-sha256; c=simple; d=messiah.edu; h=from:DKIM-Signature; s=selector2;", TestKeys.privateKey); + byte[] mail = "From: [email protected]\r\n\r\nbody\r\n".getBytes(StandardCharsets.US_ASCII); + String field = signer.sign(new ByteArrayInputStream(mail)); + + List<SignatureRecord> verified = verifier.verify(new SequenceInputStream( + new ByteArrayInputStream((field + "\r\n").getBytes(StandardCharsets.US_ASCII)), new ByteArrayInputStream(mail))); + + assertThat(verified).hasSize(1); + } + + @Test + public void signatureOverAnEarlierSignatureShouldVerify() throws Exception { + byte[] mail = "From: [email protected]\r\n\r\nbody\r\n".getBytes(StandardCharsets.US_ASCII); + String first = dkimSigner.sign(new ByteArrayInputStream(mail)); + byte[] signedOnce = (first + "\r\n" + new String(mail, StandardCharsets.US_ASCII)).getBytes(StandardCharsets.US_ASCII); + String second = new DKIMSigner("v=1; a=rsa-sha256; c=relaxed; d=messiah.edu; h=from:DKIM-Signature:DKIM-Signature; s=selector3;", TestKeys.privateKey_2) + .sign(new ByteArrayInputStream(signedOnce)); + byte[] tampered = (second + "\r\n" + first.replace("h=", "h=to:") + "\r\n" + new String(mail, StandardCharsets.US_ASCII)) + .getBytes(StandardCharsets.US_ASCII); + + List<SignatureRecord> verified = verifier.verify(new SequenceInputStream( + new ByteArrayInputStream((second + "\r\n").getBytes(StandardCharsets.US_ASCII)), new ByteArrayInputStream(signedOnce))); + assertThat(verified).hasSize(2); + + verifier.resetResults(); + assertThatThrownBy(() -> verifier.verify(new ByteArrayInputStream(tampered))) + .isInstanceOf(CompositeFailException.class); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
