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)
lmpx.com only provides a reader for public news (NNTP) servers. It is not
affiliated with the servers or forums shown here and is not responsible for
the content of articles, which is written by their respective authors.