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 25721f9aa9f5289cb45c5111074fe92eab15ddfe
Author: Benoit TELLIER <[email protected]>
AuthorDate: Tue Nov 3 08:41:26 2026 +0100

    [FIX] Tag values: accept the UTF-8 octets RFC 8616 allows
    
    RFC 8616 section 4 adds UTF8-non-ascii to dkim-safe-char and to the i=
    local part for internationalized mail, but tval was restricted to the
    ASCII VALCHAR and the DKIM-Quoted-Printable decoder rejected 8-bit
    octets, whatever the strict validation setting: an EAI signature with
    raw UTF-8 in i= or z= became PERMERROR. Both now accept non-ASCII
    characters, as do the q= and z= syntax checks; the i= local part
    already did.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../apache/james/jdkim/parser/DKIMQuotedPrintable.java    |  4 ++--
 .../apache/james/jdkim/tagvalue/SignatureRecordImpl.java  |  7 ++++---
 .../java/org/apache/james/jdkim/tagvalue/TagValue.java    |  6 ++++--
 main/src/test/java/org/apache/james/jdkim/DKIMTest.java   | 15 +++++++++++++++
 .../org/apache/james/jdkim/SignatureRecordImplTest.java   |  9 +++++++++
 .../james/jdkim/parser/DKIMQuotedPrintableTest.java       |  8 ++++++--
 .../org/apache/james/jdkim/tagvalue/TagValueTest.java     |  8 +++++++-
 7 files changed, 47 insertions(+), 10 deletions(-)

diff --git 
a/main/src/main/java/org/apache/james/jdkim/parser/DKIMQuotedPrintable.java 
b/main/src/main/java/org/apache/james/jdkim/parser/DKIMQuotedPrintable.java
index 094aec8..07e0dae 100644
--- a/main/src/main/java/org/apache/james/jdkim/parser/DKIMQuotedPrintable.java
+++ b/main/src/main/java/org/apache/james/jdkim/parser/DKIMQuotedPrintable.java
@@ -102,8 +102,8 @@ public class DKIMQuotedPrintable {
         return sb.toString();
     }
 
-    // dkim-safe-char = %x21-3A / %x3C / %x3E-7E
+    // dkim-safe-char = %x21-3A / %x3C / %x3E-7E, RFC 8616 section 4 adding 
UTF8-non-ascii
     private static boolean isDkimSafeChar(char c) {
-        return c >= 0x21 && c <= 0x7E && c != ';' && c != '=';
+        return c >= 0x21 && c <= 0x7E && c != ';' && c != '=' || c >= 0x80;
     }
 }
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 2d62e51..1154357 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
@@ -139,14 +139,15 @@ public class SignatureRecordImpl extends TagValue 
implements SignatureRecord {
     }
 
     // SPEC: sig-q-tag-method = "dns/txt" / x-sig-q-tag-type ["/" 
x-sig-q-tag-args], the type a
-    // hyphenated-word and the args a qp-hdr-value (dkim-safe-char but "|", or 
"=" HEX HEX). Possessive, see LOCAL_PART.
+    // hyphenated-word and the args a qp-hdr-value (dkim-safe-char but "|", or 
"=" HEX HEX), dkim-safe-char
+    // including UTF8-non-ascii (RFC 8616 section 4). Possessive, see 
LOCAL_PART.
     static final Pattern QUERY_METHOD = Pattern.compile(
-            
"[A-Za-z](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:/(?:[\\x21-\\x3A\\x3C\\x3E-\\x7B\\x7D\\x7E]|=[0-9A-F]{2})*+)?");
+            
"[A-Za-z](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:/(?:[\\x21-\\x3A\\x3C\\x3E-\\x7B\\x7D\\x7E\\u0080-\\uFFFF]|=[0-9A-F]{2})*+)?");
 
     // SPEC: sig-z-tag-copy = hdr-name [FWS] ":" qp-hdr-value ; hdr-name = 
1*ftext (%d33-57 / %d59-126).
     // Possessive, see LOCAL_PART.
     private static final Pattern COPIED_HEADER_FIELD = Pattern.compile(
-            "[\\x21-\\x39\\x3B-\\x7E]++[ 
\\t\\r\\n]*+:(?:[\\x21-\\x3A\\x3C\\x3E-\\x7B\\x7D\\x7E]|=[0-9A-F]{2}|[ 
\\t\\r\\n])*+");
+            "[\\x21-\\x39\\x3B-\\x7E]++[ 
\\t\\r\\n]*+:(?:[\\x21-\\x3A\\x3C\\x3E-\\x7B\\x7D\\x7E\\u0080-\\uFFFF]|=[0-9A-F]{2}|[
 \\t\\r\\n])*+");
 
     /**
      * RFC 6376 section 3.5: checks z= as sig-z-tag-copy *( "|" [FWS] 
sig-z-tag-copy ).
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 34dd7be..7593a02 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
@@ -53,9 +53,11 @@ public class TagValue {
 
     private static final Pattern tagPattern = Pattern
             .compile("^[A-Za-z][A-Za-z0-9_]*$");
-    // RFC 6376 section 3.2: tval = 1*VALCHAR, VALCHAR = %x21-3A / %x3C-7E
+    // RFC 6376 section 3.2: tval = 1*VALCHAR, VALCHAR = %x21-3A / %x3C-7E, 
plus the UTF8-non-ascii
+    // octets RFC 8616 section 4 allows in i= and z= for internationalized 
mail (header octets are read as
+    // ISO-8859-1, so they arrive as U+0080 to U+00FF; the header hashing 
rejects anything above).
     // Use possessive matching to avoid heavy stack usage
-    private static final String tval = "[\\x21-\\x3a\\x3c-\\x7e]++";
+    private static final String tval = 
"[\\x21-\\x3a\\x3c-\\x7e\\u0080-\\uffff]++";
     // validate value chars
     // Use possessive matching to avoid heavy stack usage
     private static final Pattern valuePattern = Pattern.compile("^(?:" + tval
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 2575404..725c698 100644
--- a/main/src/test/java/org/apache/james/jdkim/DKIMTest.java
+++ b/main/src/test/java/org/apache/james/jdkim/DKIMTest.java
@@ -614,6 +614,21 @@ public class DKIMTest {
         assertThat(rfc5322Verifier.hasAnyValidSignature()).isFalse();
     }
 
+    @Test
+    public void eaiSignatureWithUtf8IdentityAndCopiedFieldsShouldPass() throws 
Exception {
+        // RFC 8616 section 4: raw UTF-8 in i= and z=, here "j\u00e9r\u00f4me" 
whose octets read as ISO-8859-1
+        String local = "j\u00c3\u00a9r\u00c3\u00b4me";
+        String message = "From: " + local + "@messiah.edu\r\nSubject: 
hello\r\n\r\nbody\r\n";
+        DKIMSigner signer = new DKIMSigner("v=1; a=rsa-sha256; c=relaxed; 
d=messiah.edu; h=from:subject; q=dns/txt; s=selector2;"
+                + " i=" + local + "@messiah.edu; z=From:" + local + 
"@messiah.edu;", TestKeys.privateKey);
+        String signature = signer.sign(new 
ByteArrayInputStream(message.getBytes(StandardCharsets.ISO_8859_1)));
+        String signed = signature + "\r\n" + message;
+
+        assertThat(verifier.verify(new 
ByteArrayInputStream(signed.getBytes(StandardCharsets.ISO_8859_1)))).hasSize(1);
+        assertThat(verifier.getResults()).singleElement()
+                .satisfies(r -> 
assertThat(r.getResultType()).isEqualTo(Result.Type.PASS));
+    }
+
     @Test
     public void duplicatedToFieldSignedOnceShouldPassByDefault() throws 
Exception {
         String message = "From: [email protected]\r\nTo: 
[email protected]\r\nTo: [email protected]\r\nSubject: hello\r\n\r\nbody\r\n";
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 8f59319..a36ff08 100644
--- a/main/src/test/java/org/apache/james/jdkim/SignatureRecordImplTest.java
+++ b/main/src/test/java/org/apache/james/jdkim/SignatureRecordImplTest.java
@@ -458,6 +458,15 @@ public class SignatureRecordImplTest {
         return sb.toString();
     }
 
+    @Test
+    public void utf8IdentityAndCopiedFieldsAreAccepted() {
+        // RFC 8616 section 4, the UTF-8 octets of "j\u00e9r\u00f4me" read as 
ISO-8859-1
+        String local = "j\u00c3\u00a9r\u00c3\u00b4me";
+        Assert.assertEquals(local + "@example.com", recordWithIdentity(local + 
"@example.com").getIdentity().toString());
+        recordWithCopiedFields("From:" + local + "@example.com|Subject:" + 
local);
+        recordWithQuery("x-foo/" + local + ":dns/txt");
+    }
+
     @Test
     public void longValuesDoNotOverflowTheStack() {
         recordWithCopiedFields("Subject:" + repeat("a=20", 16384));
diff --git 
a/main/src/test/java/org/apache/james/jdkim/parser/DKIMQuotedPrintableTest.java 
b/main/src/test/java/org/apache/james/jdkim/parser/DKIMQuotedPrintableTest.java
index 26e68ba..f510452 100644
--- 
a/main/src/test/java/org/apache/james/jdkim/parser/DKIMQuotedPrintableTest.java
+++ 
b/main/src/test/java/org/apache/james/jdkim/parser/DKIMQuotedPrintableTest.java
@@ -99,13 +99,17 @@ public class DKIMQuotedPrintableTest {
         assertEquals("smiling face ØÞ with heart eyes", 
DKIMQuotedPrintable.dkimQuotedPrintableDecode("smiling=20face=20=D8=DE=20with=20heart=20eyes"));
     }
 
-    // TODO UTF-8 see https://datatracker.ietf.org/doc/html/rfc6376#section-3.2
+    @Test
+    public void utf8OctetsShouldBeKeptAsSafeChars() {
+        // RFC 8616 section 4: dkim-safe-char =/ UTF8-non-ascii
+        assertEquals("j\u00c3\u00a9r\u00c3\u00b4me x", 
DKIMQuotedPrintable.dkimQuotedPrintableDecode("j\u00c3\u00a9r\u00c3\u00b4me=20x"));
+    }
 
     @Test
     public void unsafeCharactersAndBareLineBreaksShouldBeRejected() {
         assertThatThrownBy(() -> 
DKIMQuotedPrintable.dkimQuotedPrintableDecode("\u001d"))
                 .isInstanceOf(IllegalArgumentException.class);
-        assertThatThrownBy(() -> 
DKIMQuotedPrintable.dkimQuotedPrintableDecode("\u00e9"))
+        assertThatThrownBy(() -> 
DKIMQuotedPrintable.dkimQuotedPrintableDecode("\u007f"))
                 .isInstanceOf(IllegalArgumentException.class);
         assertThatThrownBy(() -> 
DKIMQuotedPrintable.dkimQuotedPrintableDecode("a\n b"))
                 .isInstanceOf(IllegalArgumentException.class);
diff --git 
a/main/src/test/java/org/apache/james/jdkim/tagvalue/TagValueTest.java 
b/main/src/test/java/org/apache/james/jdkim/tagvalue/TagValueTest.java
index 6617b62..8b2862e 100644
--- a/main/src/test/java/org/apache/james/jdkim/tagvalue/TagValueTest.java
+++ b/main/src/test/java/org/apache/james/jdkim/tagvalue/TagValueTest.java
@@ -22,6 +22,7 @@ package org.apache.james.jdkim.tagvalue;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import org.junit.Assert;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
 import org.junit.Test;
 
 import java.util.Set;
@@ -197,10 +198,15 @@ public class TagValueTest {
     @Test
     public void valuesOutsideValcharShouldBeRejected() {
         assertThatThrownBy(() -> new 
TagValue("a=\u000b")).isInstanceOf(IllegalStateException.class);
-        assertThatThrownBy(() -> new 
TagValue("a=\u0090")).isInstanceOf(IllegalStateException.class);
+        assertThatThrownBy(() -> new 
TagValue("a=\u007f")).isInstanceOf(IllegalStateException.class);
         assertThatThrownBy(() -> new 
TagValue("a=b\u0000c")).isInstanceOf(IllegalStateException.class);
     }
 
+    @Test
+    public void utf8OctetsShouldBeAccepted() {
+        assertEquals("j\u00c3\u00a9r\u00c3\u00b4me", new 
TagValue("a=j\u00c3\u00a9r\u00c3\u00b4me").getValue("a").toString());
+    }
+
     @Test
     public void trailingWhitespaceThatIsNotFwsShouldBeRejected() {
         assertThatThrownBy(() -> new 
TagValue("a=b;\r\n\r\n\t")).isInstanceOf(IllegalStateException.class);


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

Reply via email to