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 bae47c8e151e9ea21255c0c9c0433d6e687c1c1d Author: Benoit TELLIER <[email protected]> AuthorDate: Fri Oct 30 13:46:54 2026 +0100 [FIX] Header hashing: reject chars above U+00FF instead of hashing '?' Headers carries one octet per char, but a Headers implementation that decodes the header bytes otherwise (e.g. as UTF-8) can hand over chars above U+00FF. octets() used String#getBytes(ISO_8859_1), which silently turns them into '?': signing covered '?' instead of the real octets, and on verification such chars could be swapped without breaking the signature. They are now reported: sign() and verify() fail with a PermFailException for that signature. Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .../java/org/apache/james/jdkim/DKIMCommon.java | 20 +++++++++++++ .../test/java/org/apache/james/jdkim/DKIMTest.java | 33 ++++++++++++++++++++++ 2 files changed, 53 insertions(+) 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 c504d43..c5315d7 100644 --- a/main/src/main/java/org/apache/james/jdkim/DKIMCommon.java +++ b/main/src/main/java/org/apache/james/jdkim/DKIMCommon.java @@ -41,8 +41,17 @@ public abstract class DKIMCommon { /** * Header fields are carried as Strings where each char is one raw octet * (see {@link Headers}): ISO-8859-1 gives them back unchanged. + * + * @throws IllegalArgumentException on a char above U+00FF, which getBytes + * would silently turn into '?' */ private static byte[] octets(String s) { + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) > 0xFF) { + throw new IllegalArgumentException("Header field holds a char above U+00FF at " + i + + ": header fields must carry one octet per char"); + } + } return s.getBytes(StandardCharsets.ISO_8859_1); } @@ -86,6 +95,17 @@ public abstract class DKIMCommon { List<CharSequence> headers, Signature signature, String signatureFieldName) throws SignatureException, PermFailException { + try { + hashHeaders(h, sign, headers, signature, signatureFieldName); + } catch (IllegalArgumentException e) { + throw new PermFailException(e.getMessage(), sign, e); + } + } + + private static void hashHeaders(Headers h, SignatureRecord sign, + List<CharSequence> headers, Signature signature, + String signatureFieldName) + throws SignatureException, PermFailException { boolean relaxedHeaders = SignatureRecord.RELAXED.equals(sign.getHeaderCanonicalisationMethod()); if (!relaxedHeaders 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 cc8252d..4ad6d80 100644 --- a/main/src/test/java/org/apache/james/jdkim/DKIMTest.java +++ b/main/src/test/java/org/apache/james/jdkim/DKIMTest.java @@ -19,6 +19,8 @@ package org.apache.james.jdkim; +import org.apache.james.jdkim.api.BodyHasher; +import java.util.stream.Collectors; import org.apache.james.mime4j.MimeException; import java.security.MessageDigest; import static org.assertj.core.api.Assertions.assertThat; @@ -443,4 +445,35 @@ public class DKIMTest { assertThatThrownBy(() -> verifier.verify(new ByteArrayInputStream(tampered))) .isInstanceOf(CompositeFailException.class); } + + @Test + public void headerCharsAboveU00FFShouldNotBeHashedAsQuestionMarks() throws Exception { + String template = "v=1; d=messiah.edu; s=selector2; a=rsa-sha256; c=simple/simple; h=from;"; + DKIMSigner signer = new DKIMSigner(template, TestKeys.privateKey); + BodyHasher bodyHasher = signer.newBodyHasher(signer.newSignatureRecordTemplate(template)); + bodyHasher.getOutputStream().close(); + // getBytes(ISO_8859_1) used to turn any char above U+00FF into this '?' + String signature = signer.sign(headers("From: [email protected]?"), bodyHasher); + + assertThatThrownBy(() -> signer.sign(headers("From: " + (char) 0xA9C3), bodyHasher)) + .isInstanceOf(PermFailException.class); + assertThatThrownBy(() -> verifier.verify(headers(signature, "From: [email protected]" + (char) 0x100), + new ByteArrayInputStream(new byte[0]))) + .isInstanceOf(PermFailException.class); + } + + private static Headers headers(String... fields) { + List<String> all = Arrays.asList(fields); + return new Headers() { + public List<String> getFields() { + return all; + } + + public List<String> getFields(String name) { + return all.stream() + .filter(f -> f.substring(0, f.indexOf(':')).equalsIgnoreCase(name)) + .collect(Collectors.toList()); + } + }; + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
