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 05c23ed9270f94bf104431bc73f806c2f2364c29
Author: Benoit TELLIER <[email protected]>
AuthorDate: Sun Nov 1 19:53:36 2026 +0000

    [FIX] DKIMSigner: fold the DKIM-Signature field it returns
    
    TagValue#updateStringRepresentation built the field on one line
    ("TODO add folding"). With a 2048-bit key, an h= of about 515
    characters gave a line over the 998-octet limit of RFC 5322, which
    MTAs split or reject, breaking the signature at every receiver.
    
    Signature records now fold their generated representation at 78
    columns with CRLF + SP, between tags and after a ":" of a long h=, as
    RFC 6376 section 3.5 allows. The b= value goes on continuation lines
    of its own and the next tag on a new line, so that removing the b=
    value gives back the representation generated with an empty b=, over
    which the signature is computed, with simple and relaxed header
    canonicalization alike. The expected signatures of DKIMTest change
    accordingly.
---
 .../james/jdkim/tagvalue/SignatureRecordImpl.java  |  5 ++
 .../jdkim/tagvalue/SignatureRecordTemplate.java    |  5 ++
 .../org/apache/james/jdkim/tagvalue/TagValue.java  | 75 +++++++++++++++++++++-
 .../test/java/org/apache/james/jdkim/DKIMTest.java | 60 +++++++++++++++--
 4 files changed, 140 insertions(+), 5 deletions(-)

diff --git 
a/main/src/main/java/org/apache/james/jdkim/tagvalue/SignatureRecordImpl.java 
b/main/src/main/java/org/apache/james/jdkim/tagvalue/SignatureRecordImpl.java
index e8806ee..897b220 100644
--- 
a/main/src/main/java/org/apache/james/jdkim/tagvalue/SignatureRecordImpl.java
+++ 
b/main/src/main/java/org/apache/james/jdkim/tagvalue/SignatureRecordImpl.java
@@ -384,6 +384,11 @@ public class SignatureRecordImpl extends TagValue 
implements SignatureRecord {
         }
     }
 
+    @Override
+    protected boolean isFoldedRepresentation() {
+        return true;
+    }
+
     public String toUnsignedString() {
         return emptyTagValue(toString(), "b");
     }
diff --git 
a/main/src/main/java/org/apache/james/jdkim/tagvalue/SignatureRecordTemplate.java
 
b/main/src/main/java/org/apache/james/jdkim/tagvalue/SignatureRecordTemplate.java
index 017c5ba..b9c001f 100644
--- 
a/main/src/main/java/org/apache/james/jdkim/tagvalue/SignatureRecordTemplate.java
+++ 
b/main/src/main/java/org/apache/james/jdkim/tagvalue/SignatureRecordTemplate.java
@@ -332,6 +332,11 @@ public class SignatureRecordTemplate extends TagValue 
implements SignatureRecord
         return new SignatureRecordImpl(this.toString());
     }
 
+    @Override
+    protected boolean isFoldedRepresentation() {
+        return true;
+    }
+
     public String toUnsignedString() {
         return emptyTagValue(toString(), "b");
     }
diff --git a/main/src/main/java/org/apache/james/jdkim/tagvalue/TagValue.java 
b/main/src/main/java/org/apache/james/jdkim/tagvalue/TagValue.java
index 393e704..34dd7be 100644
--- a/main/src/main/java/org/apache/james/jdkim/tagvalue/TagValue.java
+++ b/main/src/main/java/org/apache/james/jdkim/tagvalue/TagValue.java
@@ -326,7 +326,18 @@ public class TagValue {
         return stringRepresentation;
     }
     
+    /**
+     * @return true to fold the generated string representation, see {@link 
#foldedRepresentation()}
+     */
+    protected boolean isFoldedRepresentation() {
+        return false;
+    }
+
     private void updateStringRepresentation() {
+        if (isFoldedRepresentation()) {
+            stringRepresentation = foldedRepresentation();
+            return;
+        }
         // calculate a new string representation
         StringBuilder res = new StringBuilder();
         Set<String> s = getTags();
@@ -337,8 +348,70 @@ public class TagValue {
             res.append(getValue(tag));
             res.append(";");
         }
-        // TODO add folding
         stringRepresentation = res.toString();
     }
 
+    // RFC 5322 section 2.1.1: lines SHOULD be at most 78 characters, MUST be 
at most 998.
+    private static final int FOLDING_WIDTH = 78;
+    private static final int B64_CHUNK = 72;
+    // the field name the representation is appended to, e.g. "DKIM-Signature:"
+    private static final int FIELD_NAME_LENGTH = "DKIM-Signature:".length();
+
+    /**
+     * Folds the representation with CRLF + SP, as RFC 6376 section 3.5 
allows. The b= value goes on
+     * continuation lines of its own and the next tag on a new line, so that 
the folding of the other
+     * tags does not depend on b=: removing the b= value (with its FWS) gives 
back the representation
+     * generated with an empty b=, which is the one the signature is computed 
over.
+     */
+    private String foldedRepresentation() {
+        StringBuilder res = new StringBuilder();
+        int column = FIELD_NAME_LENGTH;
+        boolean breakBeforeNextTag = false;
+        for (String tag : getTags()) {
+            String value = getValue(tag).toString();
+            if ("b".equals(tag)) {
+                String prefix = " b=";
+                if (breakBeforeNextTag || column + prefix.length() + 1 > 
FOLDING_WIDTH) {
+                    res.append("\r\n");
+                    column = 0;
+                }
+                res.append(prefix);
+                column += prefix.length();
+                for (int i = 0; i < value.length(); i += B64_CHUNK) {
+                    String chunk = value.substring(i, Math.min(value.length(), 
i + B64_CHUNK));
+                    res.append("\r\n ").append(chunk);
+                    column = 1 + chunk.length();
+                }
+                res.append(";");
+                breakBeforeNextTag = true;
+                continue;
+            }
+            String piece = " " + tag + "=" + value + ";";
+            if (breakBeforeNextTag || (column > 0 && column + piece.length() > 
FOLDING_WIDTH)) {
+                res.append("\r\n");
+                column = 0;
+            }
+            breakBeforeNextTag = false;
+            if (column + piece.length() <= FOLDING_WIDTH || !"h".equals(tag)) {
+                res.append(piece);
+                column += piece.length();
+                continue;
+            }
+            // SPEC: sig-h-tag = hdr-name *( [FWS] ":" [FWS] hdr-name ): fold 
after a ":"
+            res.append(" h=");
+            column += 3;
+            String[] names = value.split(":", -1);
+            for (int i = 0; i < names.length; i++) {
+                String name = names[i] + (i < names.length - 1 ? ":" : ";");
+                if (column + name.length() > FOLDING_WIDTH) {
+                    res.append("\r\n ");
+                    column = 1;
+                }
+                res.append(name);
+                column += name.length();
+            }
+        }
+        return res.toString();
+    }
+
 }
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 c4564da..c97abde 100644
--- a/main/src/test/java/org/apache/james/jdkim/DKIMTest.java
+++ b/main/src/test/java/org/apache/james/jdkim/DKIMTest.java
@@ -85,7 +85,14 @@ public class DKIMTest {
 
     @Test
     public void should_verify_generated_signature_single_key() throws 
Exception {
-        String expectedSignature = "DKIM-Signature: a=rsa-sha256; q=dns/txt; 
b=Axa8s/gTnnJ8em45KV/AQw33hQ4uYtBKiQp3dLq7oRFt+WmDZ5ZErPq4lBVXfP+IAvP+Au91J8270ivn1J/6E0YqKntn4s1hjcNBPPRVohvmlcQ1mEMd6DuYDtWjDFwG2GWZwtilaPY2afhlTuAbHkn8nHm7MVtAGETO8QQ2zfD1NSGzKbNYP9I+hrDJq5ajka6PZn1d+mDhUH5Px8yScYqo5i8Z8GXaejSIu7RsLDuxtOO2cuClRi8MKGxc7MiGndMufXB8xbS1L80IFlyunOVY5eBaqnnhF2YrDDQfZ6DTqorzX6D5dNjpjOG6AbsqkW83Drx0TTV/5M0raU1SIw==;
 c=simple; s=selector2; d=messiah.edu; v=1; bh=6pQY5V6Dw8mCYWq017gfb [...]
+        String expectedSignature = "DKIM-Signature: a=rsa-sha256; q=dns/txt; 
b=\r\n"
+                + " 
C+1++2C84m11IB3g95xLH9fMGUC/eDf8hzv267Htlr010KmPOa+ljSTVGzWUnsor9JXIDXwl\r\n"
+                + " 
a7JEMixM5GRDhmZX/bkHki3BBkyjQxOq7g8YNfMIOE0igpM2j6kItCP6fA9jAG7mBfHFOwiu\r\n"
+                + " 
a2pxXzejH3mxefUK2RfQ93aFG3Mm0BWMTkkEr0km6+M4crjuDj63+od4lmXMu8PEkirSAERH\r\n"
+                + " 
CzpIfluJpl/TfEhQ9kMljPaAFApt1iLzaIPyOTIUQx+pWRbuVGE52+Z+Sbco1m/1+9gKWyk7\r\n"
+                + " 
S9flEbozdtNIadQSOfhcl+wMaG/R8Y7166HilWhzz9uF6u7gv/VIMQ==;\r\n"
+                + " c=simple; s=selector2; d=messiah.edu; v=1;\r\n"
+                + " bh=6pQY5V6Dw8mCYWq017gfbpv+x2X4GvOhIIZtKw6iU6g=; 
h=date:from:subject;";
 
         ByteArrayInputStream inputStream = 
readFileToByteArrayInputStream("/org/apache/james/jdkim/Mail-DKIM/corpus/multiple_2.txt");
         String actualSignature = dkimSigner.sign(inputStream);
@@ -112,9 +119,30 @@ public class DKIMTest {
         // You might want to sign with multiple keys to support several 
hashmethods on the same selector
         // or several signing algorithm on the same selector ( ECDSA is a 
proposed alternative to rsa )
         // or when upgrading keys for any reason to add several signatures 
using different selectors.
-        String expectedSignature = "DKIM-Signature: a=rsa-sha256; q=dns/txt; 
b=Axa8s/gTnnJ8em45KV/AQw33hQ4uYtBKiQp3dLq7oRFt+WmDZ5ZErPq4lBVXfP+IAvP+Au91J8270ivn1J/6E0YqKntn4s1hjcNBPPRVohvmlcQ1mEMd6DuYDtWjDFwG2GWZwtilaPY2afhlTuAbHkn8nHm7MVtAGETO8QQ2zfD1NSGzKbNYP9I+hrDJq5ajka6PZn1d+mDhUH5Px8yScYqo5i8Z8GXaejSIu7RsLDuxtOO2cuClRi8MKGxc7MiGndMufXB8xbS1L80IFlyunOVY5eBaqnnhF2YrDDQfZ6DTqorzX6D5dNjpjOG6AbsqkW83Drx0TTV/5M0raU1SIw==;
 c=simple; s=selector2; d=messiah.edu; v=1; bh=6pQY5V6Dw8mCYWq017gfb [...]
-        String expectedSignature2 = "DKIM-Signature: a=rsa-sha1; q=dns/txt; 
b=YiwcfjqM7myZ/OENExlyGVzy+rg/779R6pF7bPl79aL6e7yGYeN0XdLRcJEqhg+/uNFwcC7zrbWUwPBVFpFN8pKdQT7TgTr+ydoN65QiBa/rXH4m8Ga+oKx8652dXAHm9oMvG166VdMRsEKTJq2bFpM9RR4mW0KtHPte2JWiOtCYO6MPTlWA2JnIgQp+3+03rnOcKdQ+sn/bi9OwanwE4jgIBcPeHkHVr1fVsV53nvDlbk1DiDX+uOXvuk6bjPVBN4srZiSIvFKsmco0tGZx8cgs5OKyjmtIWVOvjxgupXWvEJJ1nMi1UQ1AXh6jDqWrMDCioRCMG9TeGy8fjcjcfw==;
 c=simple; s=selector2; d=messiah.edu; v=1; bh=q6DWKdHUzNbVPt6YBbD1KO [...]
-        String expectedSignature3 = "DKIM-Signature: a=rsa-sha256; q=dns/txt; 
b=crkBsqVuTJJmjZyNuJtmGXBsHIT7tJq0ONWLvNfO29sl1kNm9UzTZ4mOYR+akNJqonkaFFaVM9MZ/6QUd5NbGaIytxXnxv+NPNu6ZzUunlcRyOPhEQ/znemg3WjibRs24gWubBZZXApkqQ9kFh/atatoaJhTls/lnbP8ZV3XlWVN12UuESU3qdieRvrhKWX5/Od7LqZS04ZTeToAabOtDmm6hYl2R6wxizdHrOkGiNERfbB8Iaws5f3Qnt0S94wQ5FVTPzgRiO9OW8hYbAijS4Bh8/NXV5xauMXjCETfxX3pQYUuxc4QVhnoMbmuqgEulzuJUzapjotLLFxQRaSsjw==;
 c=simple; s=selector3; d=messiah.edu; v=1; bh=6pQY5V6Dw8mCYWq017gf [...]
+        String expectedSignature = "DKIM-Signature: a=rsa-sha256; q=dns/txt; 
b=\r\n"
+                + " 
C+1++2C84m11IB3g95xLH9fMGUC/eDf8hzv267Htlr010KmPOa+ljSTVGzWUnsor9JXIDXwl\r\n"
+                + " 
a7JEMixM5GRDhmZX/bkHki3BBkyjQxOq7g8YNfMIOE0igpM2j6kItCP6fA9jAG7mBfHFOwiu\r\n"
+                + " 
a2pxXzejH3mxefUK2RfQ93aFG3Mm0BWMTkkEr0km6+M4crjuDj63+od4lmXMu8PEkirSAERH\r\n"
+                + " 
CzpIfluJpl/TfEhQ9kMljPaAFApt1iLzaIPyOTIUQx+pWRbuVGE52+Z+Sbco1m/1+9gKWyk7\r\n"
+                + " 
S9flEbozdtNIadQSOfhcl+wMaG/R8Y7166HilWhzz9uF6u7gv/VIMQ==;\r\n"
+                + " c=simple; s=selector2; d=messiah.edu; v=1;\r\n"
+                + " bh=6pQY5V6Dw8mCYWq017gfbpv+x2X4GvOhIIZtKw6iU6g=; 
h=date:from:subject;";
+        String expectedSignature2 = "DKIM-Signature: a=rsa-sha1; q=dns/txt; 
b=\r\n"
+                + " 
KZAV2MxR3rpgiMusBSzFlMX2AdiloPGzz1nwSFN3XCZf3negA5PePSbHzB+49ZI0guESW1tJ\r\n"
+                + " 
qhs5ke3lSTq/06B6y6AIreAVneZMetOlZQT6iKYWSkKJLrXB/lQMDWjsy73X8aiaVXCpnAY0\r\n"
+                + " 
IKy5YHNZe0deFEsiVf6ooXGxxm3ZkDNJx4kFM+LO+Cc4SXSj+XbN9U32WPMb6NwMYGt/3JqC\r\n"
+                + " 
Efp33mQSM8kktisvNiNOVMLAcDH7JsxL+ZvMhcyR9ysoyxZYbA3YjiGplrawANQhOBMmskjt\r\n"
+                + " 
dbtMy80J1vYK/7QHqr/k1jvSexSXtqENKdsdhSRvFYn0zZVE5RF64w==;\r\n"
+                + " c=simple; s=selector2; d=messiah.edu; v=1; 
bh=q6DWKdHUzNbVPt6YBbD1KOai/b8=;\r\n"
+                + " h=date:from:subject;";
+        String expectedSignature3 = "DKIM-Signature: a=rsa-sha256; q=dns/txt; 
b=\r\n"
+                + " 
UtvF5klyckyu+x/qq5QWauomFRNMBYjt5ozWB4k7wWAolnhbgWZ2pwfdnkjygJxqwWpb8YQr\r\n"
+                + " 
M9oM1umMc+Lh1icGMzRERkADy1EMHFrhxMfjLsu8tnU2A23Z+Pjc9ZSuLJIelPoPZt8cZNX6\r\n"
+                + " 
aEkLBm/PFy8mct0FMsZGTJEQKAOOLwpKk2XLEaGdgsCY8C11BHIyKP5ahx53neAQFot299qx\r\n"
+                + " 
/4274+v/0ydPtHDJO007s3QTbHJbhcIsWzGwTCY/Ukx4lqSg+9COTFqVlTcF20HvB9Vm6DUi\r\n"
+                + " 
xFSaocxZmja2yewLh4+QPSqU2m3j7W6v7BkPYOqpQCeBy39SOU3rDw==;\r\n"
+                + " c=simple; s=selector3; d=messiah.edu; v=1;\r\n"
+                + " bh=6pQY5V6Dw8mCYWq017gfbpv+x2X4GvOhIIZtKw6iU6g=; 
h=date:from:subject;";
 
         ByteArrayInputStream inputStream1 = 
readFileToByteArrayInputStream("/org/apache/james/jdkim/Mail-DKIM/corpus/multiple_2.txt");
         String actualSignature1 = dkimSigner.sign(inputStream1);
@@ -681,4 +709,28 @@ public class DKIMTest {
         
assertThat(verifier.getResults()).extracting(Result::getResultType).containsExactly(Result.Type.PERMERROR);
     }
 
+    @Test
+    public void signedFieldLinesShouldBeFoldedAndStillVerify() throws 
Exception {
+        StringBuilder h = new StringBuilder("from");
+        for (int i = 0; i < 100; i++) {
+            h.append(":from");
+        }
+        String message = "From: [email protected]\r\nSubject: 
hi\r\n\r\nbody\r\n";
+        for (String c : new String[] {"simple/simple", "relaxed/relaxed"}) {
+            String template = "v=1; a=rsa-sha256; c=" + c + "; d=messiah.edu; 
s=selector2; q=dns/txt; h=" + h
+                    + "; z=From:[email protected];";
+            String field = new DKIMSigner(template, TestKeys.privateKey)
+                    .sign(new 
ByteArrayInputStream(message.getBytes(StandardCharsets.US_ASCII)));
+
+            for (String line : field.split("\r\n")) {
+                assertThat(line.length()).as("line [%s] with c=%s", line, 
c).isLessThanOrEqualTo(78);
+            }
+            verifier.resetResults();
+            String signed = field + "\r\n" + message;
+            assertThat(verifier.verify(new 
ByteArrayInputStream(signed.getBytes(StandardCharsets.US_ASCII))))
+                    .as("c=%s", c)
+                    .hasSize(1);
+        }
+    }
+
 }


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

Reply via email to