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]

Reply via email to