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 51dea22d51edb431bab6108dfd4f789023b242ad
Author: Benoit TELLIER <[email protected]>
AuthorDate: Wed Nov 4 18:19:19 2026 +0100

    [FIX] SignatureRecordImpl: require well-formed UTF-8 in i= and z=
    
    RFC 8616 section 4 adds UTF8-non-ascii to the i= local part and to the
    z= copies, that is well-formed UTF-8. The checks are per character
    class: any octet above 0x7F passed, so a lone continuation octet, a
    truncated or overlong sequence, an encoded surrogate or F5-FF made a
    signature that verifies, raw or as =XX.
    
    checkIdentity and checkCopiedHeaderFields now decode the octets of the
    decoded value with a reporting UTF-8 decoder, so both forms are checked
    alike.
---
 .../james/jdkim/tagvalue/SignatureRecordImpl.java  | 39 ++++++++++++++++++++++
 .../james/jdkim/SignatureRecordImplTest.java       | 33 ++++++++++++++++++
 2 files changed, 72 insertions(+)

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 1154357..d2ee419 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
@@ -24,6 +24,10 @@ import static 
org.apache.james.jdkim.parser.DKIMQuotedPrintable.dkimQuotedPrinta
 import org.apache.james.jdkim.api.HashMethod;
 import org.apache.james.jdkim.api.SignatureRecord;
 
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
 import java.util.Base64;
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -135,6 +139,10 @@ public class SignatureRecordImpl extends TagValue 
implements SignatureRecord {
             throw new IllegalStateException("Invalid i= [" + value + "]: the 
local part is neither a Dot-string"
                     + " nor a Quoted-string (RFC 5321)");
         }
+        if (!isWellFormedUtf8(localPart)) {
+            throw new IllegalStateException("Invalid i= [" + value + "]: the 
local part is not well-formed UTF-8"
+                    + " (RFC 8616 section 4)");
+        }
         checkDomainName("i= domain", value.substring(at + 1), 2);
     }
 
@@ -165,6 +173,37 @@ public class SignatureRecordImpl extends TagValue 
implements SignatureRecord {
                 throw new IllegalStateException("Invalid z= [" + z + "]: copy 
[" + copy
                         + "] is not hdr-name [FWS] \":\" qp-hdr-value (RFC 
6376 section 3.5)");
             }
+            if (!isWellFormedUtf8(dkimQuotedPrintableDecode(trimmed))) {
+                throw new IllegalStateException("Invalid z= [" + z + "]: copy 
[" + copy
+                        + "] is not well-formed UTF-8 (RFC 8616 section 4)");
+            }
+        }
+    }
+
+    /**
+     * RFC 8616 section 4 admits UTF8-non-ascii (RFC 6532 section 3.1): the 
octets above 0x7F must form
+     * well-formed UTF-8. The header octets reach the tag values as ISO-8859-1 
chars.
+     *
+     * @param octets a decoded tag value, one char per octet
+     * @return whether it is well-formed UTF-8
+     */
+    static boolean isWellFormedUtf8(CharSequence octets) {
+        byte[] bytes = new byte[octets.length()];
+        for (int k = 0; k < bytes.length; k++) {
+            char c = octets.charAt(k);
+            if (c > 0xFF) {
+                return false;
+            }
+            bytes[k] = (byte) c;
+        }
+        try {
+            StandardCharsets.UTF_8.newDecoder()
+                    .onMalformedInput(CodingErrorAction.REPORT)
+                    .onUnmappableCharacter(CodingErrorAction.REPORT)
+                    .decode(ByteBuffer.wrap(bytes));
+            return true;
+        } catch (CharacterCodingException e) {
+            return false;
         }
     }
 
diff --git 
a/main/src/test/java/org/apache/james/jdkim/SignatureRecordImplTest.java 
b/main/src/test/java/org/apache/james/jdkim/SignatureRecordImplTest.java
index a36ff08..d213ee0 100644
--- a/main/src/test/java/org/apache/james/jdkim/SignatureRecordImplTest.java
+++ b/main/src/test/java/org/apache/james/jdkim/SignatureRecordImplTest.java
@@ -475,4 +475,37 @@ public class SignatureRecordImplTest {
         recordWithIdentity(repeat("a.", 32768) + "[email protected]");
         recordWithIdentity("\"" + repeat("a=20", 16384) + "\"@example.com");
     }
+
+    @Test
+    public void illFormedUtf8IdentityIsASyntaxError() {
+        // header octets arrive as ISO-8859-1 chars, as Message produces them
+        for (String i : new String[] {
+                "\"\u0080\"@example.com",       // lone continuation octet, raw
+                "\"=80\"@example.com",          // the same, 
DKIM-Quoted-Printable
+                "\[email protected]",           // truncated 2-octet sequence
+                "\u00c0\[email protected]",     // overlong "/"
+                "\u00ed\u00a0\[email protected]", // encoded surrogate U+D800
+                "\[email protected]"}) {        // never valid in UTF-8
+            assertThatThrownBy(() -> recordWithIdentity(i)).as("i=%s", i)
+                    .isInstanceOf(IllegalStateException.class);
+        }
+    }
+
+    @Test
+    public void illFormedUtf8CopiedFieldIsASyntaxError() {
+        for (String z : new String[] {"Subject:\u0080", "Subject:=80", 
"From:[email protected]|Subject:\u00c3"}) {
+            assertThatThrownBy(() -> recordWithCopiedFields(z)).as("z=%s", z)
+                    .isInstanceOf(IllegalStateException.class);
+        }
+    }
+
+    @Test
+    public void wellFormedUtf8IdentityIsStillAccepted() {
+        // "\u03b4" = CE B4
+        
assertThat(recordWithIdentity("\u00ce\[email protected]").getIdentity().toString())
+                .isEqualTo("\u00ce\[email protected]");
+        
assertThat(recordWithIdentity("[email protected]").getIdentity().toString())
+                .isEqualTo("\u00ce\[email protected]");
+        recordWithCopiedFields("Subject:=CE=B4|To:\u00ce\[email protected]");
+    }
 }


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

Reply via email to