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 bae47c8e151e9ea21255c0c9c0433d6e687c1c1d
Author: Benoit TELLIER <[email protected]>
AuthorDate: Fri Oct 30 13:46:54 2026 +0100

    [FIX] Header hashing: reject chars above U+00FF instead of hashing '?'
    
    Headers carries one octet per char, but a Headers implementation that
    decodes the header bytes otherwise (e.g. as UTF-8) can hand over chars
    above U+00FF. octets() used String#getBytes(ISO_8859_1), which silently
    turns them into '?': signing covered '?' instead of the real octets, and
    on verification such chars could be swapped without breaking the
    signature.
    
    They are now reported: sign() and verify() fail with a PermFailException
    for that signature.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../java/org/apache/james/jdkim/DKIMCommon.java    | 20 +++++++++++++
 .../test/java/org/apache/james/jdkim/DKIMTest.java | 33 ++++++++++++++++++++++
 2 files changed, 53 insertions(+)

diff --git a/main/src/main/java/org/apache/james/jdkim/DKIMCommon.java 
b/main/src/main/java/org/apache/james/jdkim/DKIMCommon.java
index c504d43..c5315d7 100644
--- a/main/src/main/java/org/apache/james/jdkim/DKIMCommon.java
+++ b/main/src/main/java/org/apache/james/jdkim/DKIMCommon.java
@@ -41,8 +41,17 @@ public abstract class DKIMCommon {
     /**
      * Header fields are carried as Strings where each char is one raw octet
      * (see {@link Headers}): ISO-8859-1 gives them back unchanged.
+     *
+     * @throws IllegalArgumentException on a char above U+00FF, which getBytes
+     *                                  would silently turn into '?'
      */
     private static byte[] octets(String s) {
+        for (int i = 0; i < s.length(); i++) {
+            if (s.charAt(i) > 0xFF) {
+                throw new IllegalArgumentException("Header field holds a char 
above U+00FF at " + i
+                        + ": header fields must carry one octet per char");
+            }
+        }
         return s.getBytes(StandardCharsets.ISO_8859_1);
     }
 
@@ -86,6 +95,17 @@ public abstract class DKIMCommon {
                                          List<CharSequence> headers, Signature 
signature,
                                          String signatureFieldName)
             throws SignatureException, PermFailException {
+        try {
+            hashHeaders(h, sign, headers, signature, signatureFieldName);
+        } catch (IllegalArgumentException e) {
+            throw new PermFailException(e.getMessage(), sign, e);
+        }
+    }
+
+    private static void hashHeaders(Headers h, SignatureRecord sign,
+                                    List<CharSequence> headers, Signature 
signature,
+                                    String signatureFieldName)
+            throws SignatureException, PermFailException {
 
         boolean relaxedHeaders = 
SignatureRecord.RELAXED.equals(sign.getHeaderCanonicalisationMethod());
         if (!relaxedHeaders
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 cc8252d..4ad6d80 100644
--- a/main/src/test/java/org/apache/james/jdkim/DKIMTest.java
+++ b/main/src/test/java/org/apache/james/jdkim/DKIMTest.java
@@ -19,6 +19,8 @@
 
 package org.apache.james.jdkim;
 
+import org.apache.james.jdkim.api.BodyHasher;
+import java.util.stream.Collectors;
 import org.apache.james.mime4j.MimeException;
 import java.security.MessageDigest;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -443,4 +445,35 @@ public class DKIMTest {
         assertThatThrownBy(() -> verifier.verify(new 
ByteArrayInputStream(tampered)))
                 .isInstanceOf(CompositeFailException.class);
     }
+
+    @Test
+    public void headerCharsAboveU00FFShouldNotBeHashedAsQuestionMarks() throws 
Exception {
+        String template = "v=1; d=messiah.edu; s=selector2; a=rsa-sha256; 
c=simple/simple; h=from;";
+        DKIMSigner signer = new DKIMSigner(template, TestKeys.privateKey);
+        BodyHasher bodyHasher = 
signer.newBodyHasher(signer.newSignatureRecordTemplate(template));
+        bodyHasher.getOutputStream().close();
+        // getBytes(ISO_8859_1) used to turn any char above U+00FF into this 
'?'
+        String signature = signer.sign(headers("From: [email protected]?"), 
bodyHasher);
+
+        assertThatThrownBy(() -> signer.sign(headers("From: " + (char) 
0xA9C3), bodyHasher))
+                .isInstanceOf(PermFailException.class);
+        assertThatThrownBy(() -> verifier.verify(headers(signature, "From: 
[email protected]" + (char) 0x100),
+                new ByteArrayInputStream(new byte[0])))
+                .isInstanceOf(PermFailException.class);
+    }
+
+    private static Headers headers(String... fields) {
+        List<String> all = Arrays.asList(fields);
+        return new Headers() {
+            public List<String> getFields() {
+                return all;
+            }
+
+            public List<String> getFields(String name) {
+                return all.stream()
+                        .filter(f -> f.substring(0, 
f.indexOf(':')).equalsIgnoreCase(name))
+                        .collect(Collectors.toList());
+            }
+        };
+    }
 }


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

Reply via email to