This is an automated email from the ASF dual-hosted git repository. quantranhong1999 pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-jdkim.git
commit 60ae74eb2149a45d72760ec417bd994ec1fd6768 Author: Benoit TELLIER <[email protected]> AuthorDate: Thu Oct 29 12:28:52 2026 +0100 [FIX] Result: emit RFC 8601 pvalues in Authentication-Results text getHeaderText copied the sender-controlled d=, s=, b= values and the exception messages into the resinfo without quoting, so a quote, a backslash, a control character or a folding CRLF could break the quoted-string and add properties to the result. Every property value is now unfolded and stripped of control characters, then emitted as an RFC 2045 token when it is one and as an escaped quoted-string otherwise. reason is always a quoted-string, and header.b is truncated after FWS removal. --- .../java/org/apache/james/jdkim/api/Result.java | 58 ++++++++++++++++--- .../org/apache/james/jdkim/api/ResultTest.java | 66 ++++++++++++++++++++++ 2 files changed, 117 insertions(+), 7 deletions(-) diff --git a/main/src/main/java/org/apache/james/jdkim/api/Result.java b/main/src/main/java/org/apache/james/jdkim/api/Result.java index a383a48..443b6af 100644 --- a/main/src/main/java/org/apache/james/jdkim/api/Result.java +++ b/main/src/main/java/org/apache/james/jdkim/api/Result.java @@ -23,6 +23,8 @@ package org.apache.james.jdkim.api; * Class to hold results of DKIMVerifier */ public class Result { + private static final String TSPECIALS = "()<>@,;:\\\"/[]?="; + private final String errorMessage; private final String dkimRawField; private final SignatureRecord record; @@ -98,11 +100,9 @@ public class Result { String partialSig = ""; String reasonProp = ""; if (record.getRawSignature() != null) { - if (record.getRawSignature().length() >= 12) { - partialSig = " header.b=" + record.getRawSignature().subSequence(0, 12); - } else { - partialSig = " header.b=" + record.getRawSignature(); - } + // base64 ignores FWS: truncate after removing it so the prefix holds 12 base64 characters. + String signature = sanitize(record.getRawSignature()).replaceAll("[\t ]", ""); + partialSig = " header.b=" + pvalue(signature.substring(0, Math.min(12, signature.length()))); } if (withReason) { @@ -118,11 +118,55 @@ public class Result { reasonMsg = errorMessage != null ? errorMessage : ""; break; } - reasonProp = reasonMsg.isEmpty() ? "" : String.format(" reason=\"%s\"", reasonMsg); + reasonProp = reasonMsg.isEmpty() ? "" : " reason=" + quotedString(sanitize(reasonMsg)); } return String.format("dkim=%s header.d=%s header.s=%s%s%s", - type.toString().toLowerCase(), record.getDToken(), record.getSelector(), partialSig, reasonProp); + type.toString().toLowerCase(), pvalue(sanitize(record.getDToken())), pvalue(sanitize(record.getSelector())), + partialSig, reasonProp); + } + + /** + * Unfolds the value and drops control characters, which a quoted-string can not hold. Tabs become spaces. + */ + private static String sanitize(CharSequence value) { + if (value == null) { + return ""; + } + StringBuilder res = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '\t') { + res.append(' '); + } else if (c >= 0x20 && c != 0x7f) { + res.append(c); + } + } + return res.toString(); + } + + /** + * Formats a property value as a RFC 8601 pvalue: an RFC 2045 token as is, a quoted-string otherwise. + */ + private static String pvalue(String value) { + return isToken(value) ? value : quotedString(value); + } + + private static boolean isToken(String value) { + if (value.isEmpty()) { + return false; + } + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c <= 0x20 || c >= 0x7f || TSPECIALS.indexOf(c) >= 0) { + return false; + } + } + return true; + } + + private static String quotedString(String value) { + return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; } /** diff --git a/main/src/test/java/org/apache/james/jdkim/api/ResultTest.java b/main/src/test/java/org/apache/james/jdkim/api/ResultTest.java new file mode 100644 index 0000000..3115d4c --- /dev/null +++ b/main/src/test/java/org/apache/james/jdkim/api/ResultTest.java @@ -0,0 +1,66 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.jdkim.api; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.james.jdkim.tagvalue.SignatureRecordImpl; +import org.junit.Test; + +public class ResultTest { + + @Test + public void headerTextShouldKeepTokensUnquoted() { + SignatureRecord record = new SignatureRecordImpl( + "v=1; a=rsa-sha256; d=example.com; s=sel; h=from; bh=AAAA; b=keocS8z7y+utXXXX"); + Result result = new Result(record); + + assertThat(result.getHeaderTextWithReason()) + .isEqualTo("dkim=pass header.d=example.com header.s=sel header.b=keocS8z7y+ut reason=\"valid signature\""); + } + + @Test + public void headerTextShouldQuoteSenderValues() { + SignatureRecord record = new SignatureRecordImpl( + "v=1; a=rsa-sha256; d=example.com; s=s\"el; h=from; bh=AAAA; b=AA\"A"); + Result result = new Result("Invalid DKIM-Signature version (expected '1'): 1\" x=\"", "", record, Result.Type.FAIL); + + assertThat(result.getHeaderText()).isEqualTo("dkim=fail header.d=example.com header.s=\"s\\\"el\" header.b=\"AA\\\"A\""); + assertThat(result.getHeaderTextWithReason()).endsWith("reason=\"Invalid DKIM-Signature version (expected '1'): 1\\\" x=\\\"\""); + } + + @Test + public void headerTextShouldQuoteEmptyValues() { + SignatureRecord record = new SignatureRecordImpl("v=1; a=rsa-sha256; d=; s=; h=from; bh=AAAA; b="); + Result result = new Result("error", "", record, Result.Type.FAIL); + + assertThat(result.getHeaderText()).isEqualTo("dkim=fail header.d=\"\" header.s=\"\" header.b=\"\""); + } + + @Test + public void headerTextShouldDropControlCharactersAndFolding() { + SignatureRecord record = new SignatureRecordImpl( + "v=1; a=rsa-sha256; d=(\u0001.example; s=se\r\n l\u007f; h=from; bh=AAAA; b=AAAA\r\n BBBB\r\n\tCCCC\u0001DDDD"); + Result result = new Result("bad \\ value\r\n \u007f\"", "", record, Result.Type.FAIL); + + assertThat(result.getHeaderTextWithReason()).isEqualTo( + "dkim=fail header.d=\"(.example\" header.s=\"se l\" header.b=AAAABBBBCCCC reason=\"bad \\\\ value \\\"\""); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
