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 c0235cdcdb2657394766d3474b0b81c8d889d392 Author: Benoit TELLIER <[email protected]> AuthorDate: Fri Oct 30 08:33:04 2026 +0100 [FIX] SignatureRecord: require exactly one '@' in i= validate() only checked that the identity ended with "@d" or ".d", so an i= without any '@' passed and getIdentityLocalPart() then threw StringIndexOutOfBoundsException, while one with several '@' was accepted as is. The identity is now split on its single '@', the domain part must be d= or one of its subdomains, and getIdentityLocalPart() uses the same split. Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .../james/jdkim/tagvalue/SignatureRecordImpl.java | 34 +++++++++++++++++----- .../jdkim/tagvalue/SignatureRecordTemplate.java | 9 ++---- .../james/jdkim/SignatureRecordImplTest.java | 20 +++++++++++++ .../james/jdkim/SignatureRecordTemplateTest.java | 6 ++++ 4 files changed, 55 insertions(+), 14 deletions(-) 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 cb7ab22..2836359 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 @@ -76,11 +76,7 @@ public class SignatureRecordImpl extends TagValue implements SignatureRecord { throw new IllegalStateException("Identity (i=) declaration cannot be parsed. Probably due to missing quoted printable encoding", e); } - if (!identity.toString().toLowerCase().endsWith( - ("@" + getValue("d")).toLowerCase()) - && !getIdentity().toString().toLowerCase().endsWith( - ("." + getValue("d")).toLowerCase())) - throw new IllegalStateException("Identity (i=) domain mismatch: expected [optional]@[optional.]domain-from-d-attribute"); + checkIdentity(identity.toString(), getValue("d").toString()); // RFC 6376 section 3.5: sig-t-tag = %x74 [FWS] "=" [FWS] 1*12DIGIT if (getValue("t") != null && !timestampPattern.matcher(getValue("t")).matches()) @@ -112,6 +108,31 @@ public class SignatureRecordImpl extends TagValue implements SignatureRecord { // this class). } + /** + * RFC 6376 section 3.5: sig-i-tag = [ Local-part ] "@" domain-name, the + * domain being d= or one of its subdomains. + */ + static void checkIdentity(String identity, String d) { + String domain = identity.substring(identityAt(identity) + 1); + // regionMatches compares case-insensitively without the default locale + boolean sameDomain = domain.length() == d.length() && domain.regionMatches(true, 0, d, 0, d.length()); + boolean subDomain = domain.length() > d.length() + && domain.charAt(domain.length() - d.length() - 1) == '.' + && domain.regionMatches(true, domain.length() - d.length(), d, 0, d.length()); + if (!sameDomain && !subDomain) + throw new IllegalStateException("Identity (i=) domain mismatch: expected [optional]@[optional.]domain-from-d-attribute"); + } + + /** + * @return the position of the single '@' of the identity + */ + static int identityAt(String identity) { + int pAt = identity.indexOf('@'); + if (pAt == -1 || identity.indexOf('@', pAt + 1) != -1) + throw new IllegalStateException("Identity (i=) must contain exactly one '@': " + identity); + return pAt; + } + private String getTimeMeasure(long lifetime) { String measure = "s"; lifetime = -lifetime; @@ -159,8 +180,7 @@ public class SignatureRecordImpl extends TagValue implements SignatureRecord { */ public CharSequence getIdentityLocalPart() { String identity = getIdentity().toString(); - int pAt = identity.indexOf('@'); - return identity.subSequence(0, pAt); + return identity.substring(0, identityAt(identity)); } /** 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 0701e30..32f294a 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 @@ -84,11 +84,7 @@ public class SignatureRecordTemplate extends TagValue implements SignatureRecord throw new IllegalStateException("Identity (i=) declaration cannot be parsed. Probably due to missing quoted printable encoding", e); } - if (!identity.toString().toLowerCase().endsWith( - ("@" + getValue("d")).toLowerCase()) - && !getIdentity().toString().toLowerCase().endsWith( - ("." + getValue("d")).toLowerCase())) - throw new IllegalStateException("Identity (i=) domain mismatch: expected [optional]@[optional.]domain-from-d-attribute"); + SignatureRecordImpl.checkIdentity(identity.toString(), getValue("d").toString()); // An empty t= asks the signer to fill in the signing time. if (getValue("t") != null && getValue("t").length() > 0 @@ -168,8 +164,7 @@ public class SignatureRecordTemplate extends TagValue implements SignatureRecord */ public CharSequence getIdentityLocalPart() { String identity = getIdentity().toString(); - int pAt = identity.indexOf('@'); - return identity.subSequence(0, pAt); + return identity.substring(0, SignatureRecordImpl.identityAt(identity)); } /** 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 533160d..ff209a4 100644 --- a/main/src/test/java/org/apache/james/jdkim/SignatureRecordImplTest.java +++ b/main/src/test/java/org/apache/james/jdkim/SignatureRecordImplTest.java @@ -218,4 +218,24 @@ public class SignatureRecordImplTest { assertThat(new SignatureRecordImpl(base + "l=0000000000000000042").getBodyHashLimit()).isEqualTo(42); assertThat(new SignatureRecordImpl(base + "l=0").getBodyHashLimit()).isEqualTo(0); } + + @Test + public void identityWithoutExactlyOneAtShouldBeRejected() { + String base = "v=1; a=rsa-sha256; d=example.com; s=s; h=from; bh=AA==; b=AA==; "; + + assertThatThrownBy(() -> new SignatureRecordImpl(base + "i=.example.com")) + .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> new SignatureRecordImpl(base + "[email protected]")) + .isInstanceOf(IllegalStateException.class); + assertThatThrownBy(() -> new SignatureRecordImpl(base + "[email protected]")) + .isInstanceOf(IllegalStateException.class); + } + + @Test + public void identityInDomainOrSubdomainShouldBeAccepted() { + String base = "v=1; a=rsa-sha256; d=example.com; s=s; h=from; bh=AA==; b=AA==; "; + + assertThat(new SignatureRecordImpl(base + "[email protected]").getIdentityLocalPart()).isEqualTo("joe"); + assertThat(new SignatureRecordImpl(base + "[email protected]").getIdentityLocalPart()).isEqualTo(""); + } } diff --git a/main/src/test/java/org/apache/james/jdkim/SignatureRecordTemplateTest.java b/main/src/test/java/org/apache/james/jdkim/SignatureRecordTemplateTest.java index 00f76f5..009eba2 100644 --- a/main/src/test/java/org/apache/james/jdkim/SignatureRecordTemplateTest.java +++ b/main/src/test/java/org/apache/james/jdkim/SignatureRecordTemplateTest.java @@ -182,4 +182,10 @@ public class SignatureRecordTemplateTest { assertThatThrownBy(() -> new SignatureRecordTemplate("v=1; a=rsa-sha256; d=example.com; s=s; h=from; l=-1")) .isInstanceOf(IllegalStateException.class); } + + @Test + public void identityWithoutAtShouldBeRejected() { + assertThatThrownBy(() -> new SignatureRecordTemplate("v=1; a=rsa-sha256; d=example.com; s=s; h=from; i=.example.com")) + .isInstanceOf(IllegalStateException.class); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
