This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-jdkim.git
The following commit(s) were added to refs/heads/master by this push:
new a9886ee Add a list of results to verifier (#23)
a9886ee is described below
commit a9886ee5b12d02b2605ee2c986e778e7977a5089
Author: Emerson Pinter <[email protected]>
AuthorDate: Tue Mar 25 04:42:07 2025 -0300
Add a list of results to verifier (#23)
With this, the user can get the results of verification also when it
fails partially. This commit also adds a method to return a header text
representing the result.
---
.../java/org/apache/james/jdkim/DKIMVerifier.java | 98 +++++++----
.../java/org/apache/james/jdkim/api/Result.java | 187 +++++++++++++++++++++
.../apache/james/jdkim/api/SignatureRecord.java | 2 +
.../jdkim/exceptions/CompositeFailException.java | 18 ++
.../james/jdkim/tagvalue/SignatureRecordImpl.java | 4 +
.../java/org/apache/james/jdkim/FileBasedTest.java | 4 +-
.../java/org/apache/james/jdkim/PerlDKIMTest.java | 22 ++-
7 files changed, 299 insertions(+), 36 deletions(-)
diff --git a/main/src/main/java/org/apache/james/jdkim/DKIMVerifier.java
b/main/src/main/java/org/apache/james/jdkim/DKIMVerifier.java
index 2b1966d..73478c5 100644
--- a/main/src/main/java/org/apache/james/jdkim/DKIMVerifier.java
+++ b/main/src/main/java/org/apache/james/jdkim/DKIMVerifier.java
@@ -23,7 +23,9 @@ import org.apache.james.jdkim.api.BodyHasher;
import org.apache.james.jdkim.api.Headers;
import org.apache.james.jdkim.api.PublicKeyRecord;
import org.apache.james.jdkim.api.PublicKeyRecordRetriever;
+import org.apache.james.jdkim.api.Result;
import org.apache.james.jdkim.api.SignatureRecord;
+import org.apache.james.jdkim.exceptions.CompositeFailException;
import org.apache.james.jdkim.exceptions.FailException;
import org.apache.james.jdkim.exceptions.PermFailException;
import org.apache.james.jdkim.exceptions.TempFailException;
@@ -42,6 +44,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Hashtable;
@@ -53,6 +56,7 @@ import java.util.Map;
public class DKIMVerifier extends DKIMCommon {
private final PublicKeyRecordRetriever publicKeyRecordRetriever;
+ private final List<Result> result = new ArrayList<>();
public DKIMVerifier() {
this.publicKeyRecordRetriever = new
MultiplexingPublicKeyRecordRetriever(
@@ -209,9 +213,7 @@ public class DKIMVerifier extends DKIMCommon {
try {
try {
message = new Message(is);
- } catch (RuntimeException e) {
- throw e;
- } catch (IOException e) {
+ } catch (RuntimeException | IOException e) {
throw e;
} catch (Exception e1) {
// This can only be a MimeException but we don't declare to
allow usage of
@@ -244,8 +246,7 @@ public class DKIMVerifier extends DKIMCommon {
try {
int pos = signatureField.indexOf(':');
if (pos > 0) {
- String v = signatureField.substring(pos + 1, signatureField
- .length());
+ String v = signatureField.substring(pos + 1);
SignatureRecord signatureRecord = null;
try {
signatureRecord = newSignatureRecord(v);
@@ -303,9 +304,7 @@ public class DKIMVerifier extends DKIMCommon {
throw new PermFailException(
"unexpected bad signature field");
}
- } catch (TempFailException e) {
- signatureExceptions.put(signatureField, e);
- } catch (PermFailException e) {
+ } catch (TempFailException | PermFailException e) {
signatureExceptions.put(signatureField, e);
} catch (RuntimeException e) {
signatureExceptions.put(signatureField, new PermFailException(
@@ -313,12 +312,8 @@ public class DKIMVerifier extends DKIMCommon {
}
}
- if (bodyHashJobs.isEmpty()) {
- if (signatureExceptions.size() > 0) {
- throw prepareException(signatureExceptions);
- } else {
- throw new PermFailException("Unexpected condition with " +
fields);
- }
+ if (bodyHashJobs.isEmpty() && signatureExceptions.isEmpty()) {
+ throw new PermFailException("Unexpected condition with " + fields);
}
return new CompoundBodyHasher(bodyHashJobs, signatureExceptions);
@@ -409,28 +404,66 @@ public class DKIMVerifier extends DKIMCommon {
}
}
+ for(SignatureRecord s: verifiedSignatures) {
+ result.add(new Result(s));
+ }
+
result.addAll(resultsFromExceptions(compoundBodyHasher.getSignatureExceptions()));
+
if (verifiedSignatures.isEmpty()) {
throw
prepareException(compoundBodyHasher.getSignatureExceptions());
} else {
- // There is no access to the signatureExceptions when
- // there is at least one valid signature (JDKIM-14)
- /*
- for (Iterator i = signatureExceptions.keySet().iterator(); i
- .hasNext();) {
- String f = (String) i.next();
- System.out.println("DKIM-Error:"
- + ((FailException) signatureExceptions.get(f))
- .getMessage() + " FIELD: " + f);
+ return verifiedSignatures;
+ }
+ }
+
+ /**
+ * Return the results of all signature checks, success and fail.
+ *
+ * @return List of {@link Result} object.
+ */
+ public List<Result> getResults() {
+ return result;
+ }
+
+ /**
+ * Returns true when all signature verification are successful. A message
without dkim-signature is considered a success.
+ *
+ * @return true when success
+ */
+ public boolean isSuccess() {
+ return result.stream().allMatch(Result::isSuccess);
+ }
+
+ /**
+ * Clears results list for the DKIMVerifier instance
+ */
+ public void resetResults() {
+ result.clear();
+ }
+
+ private List<Result> resultsFromExceptions(Map<String, FailException>
exceptions) {
+ List<Result> results = new ArrayList<>();
+ for (Map.Entry<String, FailException> e : exceptions.entrySet()) {
+ SignatureRecord rec = e.getValue().getRelatedRecord();
+ if (rec == null) {
+ rec = new SignatureRecordImpl("v=1; d=invalid; h=from;
s=invalid; b=invalidsig");
}
- */
- /*
- for (Iterator i = verifiedSignatures.iterator(); i.hasNext();) {
- SignatureRecord sr = (SignatureRecord) i.next();
- System.out.println("DKIM-Pass:" + sr);
+
+ Result.Type resultType = Result.Type.NONE;
+ if (e.getValue() instanceof TempFailException) {
+ resultType = Result.Type.TEMPERROR;
+ } else if (e.getValue() instanceof PermFailException) {
+ if (e.getValue().getRelatedRecord() == null) {
+ //FailException without the SignatureRecord
+ resultType = Result.Type.PERMERROR;
+ } else {
+ resultType = Result.Type.FAIL;
+ }
}
- */
- return verifiedSignatures;
+ results.add(new Result(e.getValue().getMessage(), e.getKey() !=
null ? e.getKey() : "", rec, resultType));
}
+
+ return results;
}
/**
@@ -446,10 +479,7 @@ public class DKIMVerifier extends DKIMCommon {
return signatureExceptions.values().iterator()
.next();
} else {
- // TODO loops signatureExceptions to give a more complete
- // response, using nested exception or a compound exception.
- // System.out.println(signatureExceptions);
- return new PermFailException("found " + signatureExceptions.size()
+ return new CompositeFailException(signatureExceptions.values(),
"found " + signatureExceptions.size()
+ " invalid signatures");
}
}
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
new file mode 100644
index 0000000..065fe81
--- /dev/null
+++ b/main/src/main/java/org/apache/james/jdkim/api/Result.java
@@ -0,0 +1,187 @@
+/****************************************************************
+ * 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;
+
+/**
+ * Class to hold results of DKIMVerifier
+ */
+public class Result {
+ private final String errorMessage;
+ private final String dkimRawField;
+ private final SignatureRecord record;
+ private final Type type;
+
+ /**
+ * Result type
+ *
+ * @see <a
href="https://datatracker.ietf.org/doc/html/rfc8601#section-2.7.1">RFC8601
2.7.1</a>
+ */
+ public enum Type {
+ NONE,
+ PASS,
+ FAIL,
+ POLICY,
+ NEUTRAL,
+ TEMPERROR,
+ PERMERROR
+ }
+
+ /**
+ * Constructor to create a Result instance with error message
+ *
+ * @param errorMessage Error message, from exception
+ * @param dkimRawField The DKIM-Signature field
+ * @param record SignatureRecord
+ * @param type Result type
+ */
+ public Result(String errorMessage, String dkimRawField, SignatureRecord
record, Type type) {
+ this.errorMessage = errorMessage;
+ this.dkimRawField = dkimRawField;
+ this.record = record;
+ this.type = type;
+ }
+
+ /**
+ * Constructor to create a Result instance of a successful verification
+ *
+ * @param record SignatureRecord
+ */
+ public Result(SignatureRecord record) {
+ this.errorMessage = null;
+ this.dkimRawField = null;
+ this.record = record;
+ this.type = Type.PASS;
+ }
+
+ /**
+ * Returns a string representing the result, with a reason field
+ */
+ public String getHeaderTextWithReason() {
+ return getHeaderText(true);
+ }
+
+ /**
+ * Returns a string representing the result
+ */
+ public String getHeaderText() {
+ return getHeaderText(false);
+ }
+
+ /**
+ * Returns the header text for usage with authentication results header,
like defined in RFC7601
+ *
+ * @param withReason If true, add reason field with error/success message
+ * @return String
+ */
+ private String getHeaderText(boolean withReason) {
+ if (record == null) {
+ return "";
+ }
+
+ 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();
+ }
+ }
+
+ if (withReason) {
+ String reasonMsg;
+ switch (type) {
+ case PASS:
+ reasonMsg = "valid signature";
+ break;
+ case NONE:
+ reasonMsg = "not signed";
+ break;
+ default:
+ reasonMsg = errorMessage != null ? errorMessage : "";
+ break;
+ }
+ reasonProp = reasonMsg.isEmpty() ? "" : String.format("
reason=\"%s\"", reasonMsg);
+ }
+
+ return String.format("dkim=%s header.d=%s header.s=%s%s%s",
+ type.toString().toLowerCase(), record.getDToken(),
record.getSelector(), partialSig, reasonProp);
+ }
+
+ /**
+ * Get ErrorMessage
+ *
+ * @return The error message produced when the exception was thrown
+ */
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ /**
+ * Get dkim field
+ *
+ * @return The DKIM-Signature field that was verified
+ */
+ public String getDkimRawField() {
+ return dkimRawField;
+ }
+
+ /**
+ * @return Returns true if success
+ */
+ public boolean isSuccess() {
+ return type == Type.PASS || type == Type.NONE || type == Type.NEUTRAL;
+ }
+
+ /**
+ * @return Returns true if fail
+ */
+ public boolean isFail() {
+ return !isSuccess();
+ }
+
+ /**
+ * The resulting SignatureRecord
+ *
+ * @return SignatureRecord
+ */
+ public SignatureRecord getRecord() {
+ return record;
+ }
+
+ /**
+ * Result Type
+ *
+ * @return The result type
+ */
+ public Type getResultType() {
+ return type;
+ }
+
+ @Override
+ public String toString() {
+ return "Result{" +
+ "headerText='" + getHeaderText() + '\'' +
+ ", errorMessage='" + errorMessage + '\'' +
+ ", dkimRawField='" + dkimRawField + '\'' +
+ ", type=" + type +
+ '}';
+ }
+}
diff --git a/main/src/main/java/org/apache/james/jdkim/api/SignatureRecord.java
b/main/src/main/java/org/apache/james/jdkim/api/SignatureRecord.java
index 3281e23..ca6b9a6 100644
--- a/main/src/main/java/org/apache/james/jdkim/api/SignatureRecord.java
+++ b/main/src/main/java/org/apache/james/jdkim/api/SignatureRecord.java
@@ -60,6 +60,8 @@ public interface SignatureRecord {
public abstract void validate();
public abstract byte[] getSignature();
+
+ public abstract CharSequence getRawSignature();
public abstract void setSignature(byte[] newSignature);
diff --git
a/main/src/main/java/org/apache/james/jdkim/exceptions/CompositeFailException.java
b/main/src/main/java/org/apache/james/jdkim/exceptions/CompositeFailException.java
new file mode 100644
index 0000000..36bbdee
--- /dev/null
+++
b/main/src/main/java/org/apache/james/jdkim/exceptions/CompositeFailException.java
@@ -0,0 +1,18 @@
+package org.apache.james.jdkim.exceptions;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+public class CompositeFailException extends FailException {
+ private final List<FailException> exceptions = new ArrayList<>();
+
+ public CompositeFailException(Collection<FailException> exceptions, String
message) {
+ super(message);
+ this.exceptions.addAll(exceptions);
+ }
+
+ public List<FailException> getExceptions() {
+ return exceptions;
+ }
+}
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 1cee2b9..fc30b10 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
@@ -284,6 +284,10 @@ public class SignatureRecordImpl extends TagValue
implements SignatureRecord {
return Base64.decodeBase64(getValue("b").toString().getBytes());
}
+ public CharSequence getRawSignature() {
+ return getValue("b");
+ }
+
public int getBodyHashLimit() {
String limit = getValue("l").toString();
if (ALL.equals(limit))
diff --git a/main/src/test/java/org/apache/james/jdkim/FileBasedTest.java
b/main/src/test/java/org/apache/james/jdkim/FileBasedTest.java
index f7cdd36..956f98a 100644
--- a/main/src/test/java/org/apache/james/jdkim/FileBasedTest.java
+++ b/main/src/test/java/org/apache/james/jdkim/FileBasedTest.java
@@ -209,7 +209,9 @@ public class FileBasedTest extends TestCase {
"k=rsa;
p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1CTqmkuRWkxlHcv1peAz3c0RuXHthVO1xx1Hy4HryZUJwSJo/R3cnEwKorQvlRuDSMgXSLLxI8u6n7h6mzRmHdsS/A+pKc7nx/6WS4N6U57PSNqOclxfwa27m/EIL6KTk9KDhaKsXxquQUBkP1CQEUZHPhQ/t7s4dmU/kvGFgNQIDAQAB");
try {
- List<SignatureRecord> res = new DKIMVerifier(pkr).verify(is);
+ DKIMVerifier verifier = new DKIMVerifier(pkr);
+ List<SignatureRecord> res = verifier.verify(is);
+ assertEquals(1, verifier.getResults().size());
if (getName().startsWith("NONE_"))
assertNull(res);
if (getName().startsWith("FAIL_"))
diff --git a/main/src/test/java/org/apache/james/jdkim/PerlDKIMTest.java
b/main/src/test/java/org/apache/james/jdkim/PerlDKIMTest.java
index 88504e7..e24ee7f 100644
--- a/main/src/test/java/org/apache/james/jdkim/PerlDKIMTest.java
+++ b/main/src/test/java/org/apache/james/jdkim/PerlDKIMTest.java
@@ -22,6 +22,7 @@ package org.apache.james.jdkim;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
+import org.apache.james.jdkim.api.Result;
import org.apache.james.jdkim.api.SignatureRecord;
import org.apache.james.jdkim.exceptions.FailException;
@@ -111,7 +112,26 @@ public class PerlDKIMTest extends TestCase {
expectFailure = true;
try {
- List<SignatureRecord> res = new DKIMVerifier(pkr).verify(is);
+ DKIMVerifier verifier = new DKIMVerifier(pkr);
+ List<SignatureRecord> res = verifier.verify(is);
+
+ if (getName().matches("good_dk_7|good_dk_6|dk_headers_2|good_dk_3")
+ ||
getName().matches("|good_dk_gmail|dk_headers_1|good_dk_5|good_dk_4")
+ ||
getName().matches("good_dk_2|good_dk_yahoo|bad_dk_1|bad_dk_2|good_dk_1|dk_multiple_1"))
{
+ assertEquals(0, verifier.getResults().size());
+ } else if (getName().equals("multiple_2")) {
+ assertEquals(4, verifier.getResults().size());
+ assertEquals(1, verifier.getResults().stream().filter(r ->
r.getResultType() == Result.Type.PASS).count());
+ assertEquals(1, verifier.getResults().stream().filter(r ->
r.getResultType() == Result.Type.FAIL).count());
+ assertEquals(2, verifier.getResults().stream().filter(r ->
r.getResultType() == Result.Type.PERMERROR).count());
+ assertEquals(1, verifier.getResults().stream().filter(r ->
r.getResultType() == Result.Type.PASS
+ && r.getHeaderText().equals("dkim=pass
header.d=messiah.edu header.s=selector1 header.b=keocS8z7y+ut")).count());
+ assertEquals(1, verifier.getResults().stream().filter(r ->
r.getResultType() == Result.Type.FAIL
+ && r.getHeaderText().equals("dkim=fail
header.d=messiah.edu header.s=selector1 header.b=shouldfailut")).count());
+ } else {
+ assertEquals(1, verifier.getResults().size());
+ }
+ assertTrue(verifier.getResults().stream().allMatch(f ->
f.getRecord().getRawSignature() != null));
if (expectNull)
assertNull(res);
if (expectFailure)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]