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]
