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 e6c7c062a8dbaf95aac63b56d4e1dbea83c67569
Author: Benoit TELLIER <[email protected]>
AuthorDate: Fri Oct 30 08:52:37 2026 +0100

    [FIX] Relaxed body canonicalization: an empty body stays empty
    
    RelaxedBodyCanonicalizer delegates to SimpleBodyCanonicalizer, which
    always ends its output with CRLF. That is right for simple, where an
    empty body becomes CRLF, but RFC 6376 section 3.4.4 only adds the CRLF
    to a non-empty relaxed body: an empty or blank-lines-only body hashed
    "\r\n" instead of the empty string, so such signatures from other
    implementations failed in jDKIM and jDKIM's failed elsewhere.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../jdkim/canon/RelaxedBodyCanonicalizer.java      |  2 +-
 .../james/jdkim/canon/SimpleBodyCanonicalizer.java | 23 ++++++++++++++++++++--
 .../jdkim/canon/RelaxedBodyCanonicalizerTest.java  | 18 +++++++++++++++++
 3 files changed, 40 insertions(+), 3 deletions(-)

diff --git 
a/main/src/main/java/org/apache/james/jdkim/canon/RelaxedBodyCanonicalizer.java 
b/main/src/main/java/org/apache/james/jdkim/canon/RelaxedBodyCanonicalizer.java
index 2716028..cbecbb9 100644
--- 
a/main/src/main/java/org/apache/james/jdkim/canon/RelaxedBodyCanonicalizer.java
+++ 
b/main/src/main/java/org/apache/james/jdkim/canon/RelaxedBodyCanonicalizer.java
@@ -32,7 +32,7 @@ public class RelaxedBodyCanonicalizer extends 
FilterOutputStream {
     private boolean pendingSpaces;
 
     public RelaxedBodyCanonicalizer(OutputStream out) {
-        super(new SimpleBodyCanonicalizer(out));
+        super(new SimpleBodyCanonicalizer(out, true));
         pendingSpaces = false;
     }
 
diff --git 
a/main/src/main/java/org/apache/james/jdkim/canon/SimpleBodyCanonicalizer.java 
b/main/src/main/java/org/apache/james/jdkim/canon/SimpleBodyCanonicalizer.java
index 90b14bf..359cbfe 100644
--- 
a/main/src/main/java/org/apache/james/jdkim/canon/SimpleBodyCanonicalizer.java
+++ 
b/main/src/main/java/org/apache/james/jdkim/canon/SimpleBodyCanonicalizer.java
@@ -31,11 +31,23 @@ public class SimpleBodyCanonicalizer extends 
FilterOutputStream {
 
     private static final boolean DEEP_DEBUG = false;
 
+    private final boolean emptyBodyIsEmpty;
     private boolean lastWasCR;
     private int countCRLF;
+    private boolean hasContent;
 
     public SimpleBodyCanonicalizer(OutputStream arg0) {
+        this(arg0, false);
+    }
+
+    /**
+     * @param emptyBodyIsEmpty true to output nothing for a body made only of
+     *                         CRLFs, as relaxed canonicalization requires (RFC
+     *                         6376 section 3.4.4); simple outputs a single 
CRLF.
+     */
+    SimpleBodyCanonicalizer(OutputStream arg0, boolean emptyBodyIsEmpty) {
         super(arg0);
+        this.emptyBodyIsEmpty = emptyBodyIsEmpty;
     }
 
     public void write(byte[] b, int off, int len) throws IOException {
@@ -54,6 +66,7 @@ public class SimpleBodyCanonicalizer extends 
FilterOutputStream {
                 // TODO output the lone \r ? (this condition should never 
happen
                 // as we expect only CRLF in a compliant 7bit email.
                 out.write('\r');
+                hasContent = true;
                 lastWasCR = false;
             }
         }
@@ -70,6 +83,7 @@ public class SimpleBodyCanonicalizer extends 
FilterOutputStream {
         if (len > 0) {
             dumpCRLF();
             out.write(b, off, len);
+            hasContent = true;
         }
         countCRLF += newCountCRLF;
         lastWasCR = newLastWasCR;
@@ -86,10 +100,12 @@ public class SimpleBodyCanonicalizer extends 
FilterOutputStream {
                 lastWasCR = true;
             } else {
                 dumpCRLF();
-                if ('\r' == b)
+                if ('\r' == b) {
                     lastWasCR = true;
-                else
+                } else {
                     out.write(b);
+                    hasContent = true;
+                }
             }
         }
     }
@@ -107,6 +123,9 @@ public class SimpleBodyCanonicalizer extends 
FilterOutputStream {
             // to output the missing \n
             lastWasCR = false;
         }
+        if (emptyBodyIsEmpty && !hasContent) {
+            return;
+        }
         countCRLF = 1;
         dumpCRLF();
     }
diff --git 
a/main/src/test/java/org/apache/james/jdkim/canon/RelaxedBodyCanonicalizerTest.java
 
b/main/src/test/java/org/apache/james/jdkim/canon/RelaxedBodyCanonicalizerTest.java
index e5ad724..d571747 100644
--- 
a/main/src/test/java/org/apache/james/jdkim/canon/RelaxedBodyCanonicalizerTest.java
+++ 
b/main/src/test/java/org/apache/james/jdkim/canon/RelaxedBodyCanonicalizerTest.java
@@ -50,4 +50,22 @@ public class RelaxedBodyCanonicalizerTest extends 
AbstractOutputStreamTestCase {
         chunker(testData, os);
         assertArrayEquals(expectedData, bos.toByteArray());
     }
+
+    public void testEmptyAndBlankBodiesCanonicalizeToNothing() throws 
IOException {
+        for (String body : new String[] {"", " \r\n", "\r\n\r\n", "\t \r\n 
\r\n"}) {
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+            RelaxedBodyCanonicalizer os = new RelaxedBodyCanonicalizer(bos);
+            os.write(body.getBytes());
+            os.close();
+            assertArrayEquals("[" + body + "]", new byte[0], 
bos.toByteArray());
+        }
+    }
+
+    public void testNonEmptyBodyGetsAFinalCRLF() throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        RelaxedBodyCanonicalizer os = new RelaxedBodyCanonicalizer(bos);
+        os.write("\r\nx".getBytes());
+        os.close();
+        assertArrayEquals("\r\nx\r\n".getBytes(), bos.toByteArray());
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to