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 <e...@pinter.dev>
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: server-dev-unsubscr...@james.apache.org
For additional commands, e-mail: server-dev-h...@james.apache.org

Reply via email to