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