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]
