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 9ce2bd1de05c83a760f200759b85bf0dae7cd515
Author: Benoit TELLIER <[email protected]>
AuthorDate: Sun Nov 1 19:55:48 2026 +0000

    [FIX] DKIM-Signature: reject a malformed i=
    
    The only check on i= was a suffix match against d=: an i= domain with
    an empty or hyphen-edged label ([email protected], @-a.example.com)
    or a malformed local part ("a"b, a..b) was accepted and reported as
    pass, although RFC 6376 section 3.5 requires [ Local-part ] "@"
    domain-name.
    
    With strict validation (-Djames.jdkim.strict.validation, default true),
    the decoded i= is now split on its last "@", its local part checked as
    an RFC 5321 Dot-string or Quoted-string (or empty), with the non-ASCII
    characters RFC 6531 allows, and its domain with the d= check, before
    the existing "d= or a subdomain" check.
---
 docs/modules/ROOT/pages/usage.adoc                 |  5 ++--
 .../james/jdkim/tagvalue/SignatureRecordImpl.java  | 31 ++++++++++++++++++++
 .../jdkim/tagvalue/SignatureRecordTemplate.java    |  2 ++
 .../james/jdkim/SignatureRecordImplTest.java       | 34 ++++++++++++++++++++++
 4 files changed, 70 insertions(+), 2 deletions(-)

diff --git a/docs/modules/ROOT/pages/usage.adoc 
b/docs/modules/ROOT/pages/usage.adoc
index 8ed5523..7b902bc 100644
--- a/docs/modules/ROOT/pages/usage.adoc
+++ b/docs/modules/ROOT/pages/usage.adoc
@@ -277,8 +277,9 @@ DKIMVerifier verifier = new DKIMVerifier(new 
VerifierOptions.Builder()
 jDKIM rejects, as a syntax error (RFC 6376 sections 3.5, 3.6.1 and 6.1), tag
 values that do not follow the RFC grammar, such as an empty entry or a
 trailing `:` in the `h=`, `k=`, `s=` or `t=` lists of a key record, an empty
-`t=`, or a DKIM-Signature `d=` or `s=` that is not a domain name (a `_`, an
-empty label, a label over 63 octets). A verifier stricter than the others may 
however reject real-world,
+`t=`, a DKIM-Signature `d=` or `s=` that is not a domain name (a `_`, an
+empty label, a label over 63 octets), or an `i=` whose local part or domain is
+malformed. A verifier stricter than the others may however reject real-world,
 slightly malformed signatures or keys. Start the JVM with
 `-Djames.jdkim.strict.validation=false` to disable these syntax checks. Checks
 with a semantic effect -- a key for another service (`s=chat`), an expired
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 076ce94..caf94de 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
@@ -108,6 +108,35 @@ public class SignatureRecordImpl extends TagValue 
implements SignatureRecord {
         }
     }
 
+    // SPEC (RFC 5321): Dot-string = Atom *("." Atom) ; Quoted-string = DQUOTE 
*QcontentSMTP DQUOTE,
+    // with the non-ASCII characters RFC 6531 adds to atext and qtextSMTP
+    private static final String ATEXT = 
"[A-Za-z0-9!#$%&'*+/=?^_`{|}~\\u0080-\\uFFFF-]+";
+    private static final Pattern LOCAL_PART = Pattern.compile(
+            "|" + ATEXT + "(?:\\." + ATEXT + 
")*|\"(?:[\\x20\\x21\\x23-\\x5B\\x5D-\\x7E\\u0080-\\uFFFF]|\\\\[\\x20-\\x7E])*\"");
+
+    /**
+     * RFC 6376 section 3.5: checks the decoded i= value as [ Local-part ] "@" 
domain-name.
+     * Only with strict validation, see {@link TagValue#isStrictValidation()}.
+     *
+     * @throws IllegalStateException if it is not
+     */
+    static void checkIdentity(CharSequence identity) {
+        if (!isStrictValidation()) {
+            return;
+        }
+        String value = identity.toString();
+        int at = value.lastIndexOf('@');
+        if (at == -1) {
+            throw new IllegalStateException("Invalid i= [" + value + "]: no 
@");
+        }
+        String localPart = value.substring(0, at);
+        if (!LOCAL_PART.matcher(localPart).matches()) {
+            throw new IllegalStateException("Invalid i= [" + value + "]: the 
local part is neither a Dot-string"
+                    + " nor a Quoted-string (RFC 5321)");
+        }
+        checkDomainName("i= domain", value.substring(at + 1), 2);
+    }
+
     /**
      * Checks the a= value of a signature record, see {@link 
#checkAlgorithm(CharSequence)}.
      *
@@ -182,6 +211,8 @@ public class SignatureRecordImpl extends TagValue 
implements SignatureRecord {
                 && Long.parseLong(getValue("x").toString()) <= 
Long.parseLong(getValue("t").toString()))
             throw new IllegalStateException("Signature expiration (x=) must be 
greater than its timestamp (t=)");
 
+        checkIdentity(identity);
+
         // when "x=" exists and signature expired then return PERMFAIL
         // (signature expired)
         if (getValue("x") != null) {
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 39a09df..7ac6ff8 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
@@ -104,6 +104,8 @@ public class SignatureRecordTemplate extends TagValue 
implements SignatureRecord
                 && Long.parseLong(getValue("x").toString()) <= 
Long.parseLong(getValue("t").toString()))
             throw new IllegalStateException("Signature expiration (x=) must be 
greater than its timestamp (t=)");
 
+        SignatureRecordImpl.checkIdentity(identity);
+
         // when "x=" exists and signature expired then return PERMFAIL
         // (signature expired)
         if (getValue("x") != null) {
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 376fbf6..000f7e3 100644
--- a/main/src/test/java/org/apache/james/jdkim/SignatureRecordImplTest.java
+++ b/main/src/test/java/org/apache/james/jdkim/SignatureRecordImplTest.java
@@ -352,4 +352,38 @@ public class SignatureRecordImplTest {
             System.clearProperty(TagValue.STRICT_VALIDATION_PROPERTY);
         }
     }
+
+    private static SignatureRecordImpl recordWithIdentity(String i) {
+        return new SignatureRecordImpl("v=1; a=rsa-sha256; c=relaxed/relaxed; 
d=example.com; s=sel;"
+                + " h=from:subject; i=" + i + "; bh=AAAA; b=AAAA");
+    }
+
+    @Test
+    public void malformedIdentityIsASyntaxError() {
+        for (String i : new String[] {
+                "\"j=20oe\"@-a.example.com", "[email protected]", 
"[email protected]",
+                "\"a\"[email protected]", "[email protected]", 
"[email protected]", "[email protected]"}) {
+            assertThatThrownBy(() -> recordWithIdentity(i)).as("i=%s", i)
+                    .isInstanceOf(IllegalStateException.class);
+        }
+    }
+
+    @Test
+    public void wellFormedIdentityIsAccepted() {
+        recordWithIdentity("@example.com");
+        recordWithIdentity("[email protected]");
+        recordWithIdentity("[email protected]");
+        recordWithIdentity("\"j=20oe\"@sub.example.com");
+        new SignatureRecordImpl("v=1; a=rsa-sha256; d=example.com; s=sel; 
h=from; bh=AAAA; b=AAAA");
+    }
+
+    @Test
+    public void malformedIdentityIsAcceptedWithoutStrictValidation() {
+        System.setProperty(TagValue.STRICT_VALIDATION_PROPERTY, "false");
+        try {
+            recordWithIdentity("[email protected]");
+        } finally {
+            System.clearProperty(TagValue.STRICT_VALIDATION_PROPERTY);
+        }
+    }
 }


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

Reply via email to