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 aa1885c73d3fa27403db1ed2a77bce8bef40880d Author: Benoit TELLIER <[email protected]> AuthorDate: Fri Oct 30 08:43:10 2026 +0100 [FIX] Relaxed header canonicalization: only delete WSP around the value String#trim() removes every character up to U+0020, so control characters at either end of a signed header value were dropped before hashing: a NUL could be added to a signed From and the relaxed signature still verified. RFC 6376 section 3.4.2 only deletes WSP; since WSP runs are already reduced to one SP, one leading and one trailing SP are now removed. Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .../java/org/apache/james/jdkim/DKIMCommon.java | 3 ++- .../test/java/org/apache/james/jdkim/DKIMTest.java | 26 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/main/src/main/java/org/apache/james/jdkim/DKIMCommon.java b/main/src/main/java/org/apache/james/jdkim/DKIMCommon.java index 830342a..72a1386 100644 --- a/main/src/main/java/org/apache/james/jdkim/DKIMCommon.java +++ b/main/src/main/java/org/apache/james/jdkim/DKIMCommon.java @@ -59,7 +59,8 @@ public abstract class DKIMCommon { String headerValue = fv.substring(fv.indexOf(':') + 1); headerValue = headerValue.replaceAll("\r\n[\t ]", " "); headerValue = headerValue.replaceAll("[\t ]+", " "); - headerValue = headerValue.trim(); + // RFC 6376 section 3.4.2 only deletes WSP: String#trim would also drop control characters + headerValue = headerValue.replaceAll("^ | $", ""); signature.update(octets(headerValue)); if (DEEP_DEBUG) System.out.println("#" + headerValue + "#"); 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 bbf1933..ffd45c5 100644 --- a/main/src/test/java/org/apache/james/jdkim/DKIMTest.java +++ b/main/src/test/java/org/apache/james/jdkim/DKIMTest.java @@ -263,4 +263,30 @@ public class DKIMTest { assertThat(message.getFields("missing")).isEmpty(); } + @Test + public void controlCharacterAddedToARelaxedHeaderValueShouldBreakTheSignature() throws Exception { + DKIMSigner signer = new DKIMSigner("v=1; a=rsa-sha256; c=relaxed/simple; d=messiah.edu; h=from; s=selector2;", TestKeys.privateKey); + byte[] mail = "From: [email protected]\r\n\r\nbody\r\n".getBytes(StandardCharsets.US_ASCII); + String field = signer.sign(new ByteArrayInputStream(mail)); + + for (String tampered : Arrays.asList("From: [email protected]\u0001", "From: \[email protected]", "From:\u0016 [email protected]")) { + InputStream in = new SequenceInputStream(new ByteArrayInputStream((field + "\r\n").getBytes(StandardCharsets.US_ASCII)), + new ByteArrayInputStream((tampered + "\r\n\r\nbody\r\n").getBytes(StandardCharsets.US_ASCII))); + + assertThatThrownBy(() -> verifier.verify(in)).as(tampered).isInstanceOf(PermFailException.class); + } + } + + @Test + public void relaxedHeaderCanonicalizationShouldIgnoreWhitespaceAroundTheValue() throws Exception { + DKIMSigner signer = new DKIMSigner("v=1; a=rsa-sha256; c=relaxed/simple; d=messiah.edu; h=from; s=selector2;", TestKeys.privateKey); + byte[] mail = "From: [email protected]\r\n\r\nbody\r\n".getBytes(StandardCharsets.US_ASCII); + byte[] reformatted = "From:\t [email protected] \t\r\n\r\nbody\r\n".getBytes(StandardCharsets.US_ASCII); + String field = signer.sign(new ByteArrayInputStream(mail)); + + InputStream in = new SequenceInputStream( + new ByteArrayInputStream((field + "\r\n").getBytes(StandardCharsets.US_ASCII)), new ByteArrayInputStream(reformatted)); + + assertThat(verifier.verify(in)).hasSize(1); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
