From 6c983d66e0dcf4f87258a7a35f6b78d748bb972a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 22 Apr 2022 22:45:39 +0200 Subject: [PATCH] Take hash algorithm usage date into account when checking algorithm acceptance --- .../java/org/pgpainless/policy/Policy.java | 151 +++++++++++++++++- .../consumer/SignatureValidator.java | 5 +- .../org/pgpainless/policy/PolicyTest.java | 31 +++- 3 files changed, 175 insertions(+), 12 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java index e64899c0..17804e5e 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java @@ -6,7 +6,9 @@ package org.pgpainless.policy; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.EnumMap; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; @@ -18,6 +20,7 @@ import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.DateUtil; import org.pgpainless.util.NotationRegistry; /** @@ -301,11 +304,39 @@ public final class Policy { public static final class HashAlgorithmPolicy { private final HashAlgorithm defaultHashAlgorithm; - private final List acceptableHashAlgorithms; + private final Map acceptableHashAlgorithmsAndTerminationDates; - public HashAlgorithmPolicy(HashAlgorithm defaultHashAlgorithm, List acceptableHashAlgorithms) { + /** + * Create a {@link HashAlgorithmPolicy} which accepts all {@link HashAlgorithm HashAlgorithms} from the + * given map, if the queried usage date is BEFORE the respective termination date. + * A termination date value of
null
means no termination, resulting in the algorithm being + * acceptable, regardless of usage date. + * + * @param defaultHashAlgorithm default hash algorithm + * @param algorithmTerminationDates map of acceptable algorithms and their termination dates + */ + public HashAlgorithmPolicy(@Nonnull HashAlgorithm defaultHashAlgorithm, @Nonnull Map algorithmTerminationDates) { this.defaultHashAlgorithm = defaultHashAlgorithm; - this.acceptableHashAlgorithms = Collections.unmodifiableList(acceptableHashAlgorithms); + this.acceptableHashAlgorithmsAndTerminationDates = algorithmTerminationDates; + } + + /** + * Create a {@link HashAlgorithmPolicy} which accepts all {@link HashAlgorithm HashAlgorithms} listed in + * the given list, regardless of usage date. + * + * @param defaultHashAlgorithm default hash algorithm (e.g. used as fallback if negotiation fails) + * @param acceptableHashAlgorithms list of acceptable hash algorithms + */ + public HashAlgorithmPolicy(@Nonnull HashAlgorithm defaultHashAlgorithm, @Nonnull List acceptableHashAlgorithms) { + this(defaultHashAlgorithm, Collections.unmodifiableMap(listToMap(acceptableHashAlgorithms))); + } + + private static Map listToMap(@Nonnull List algorithms) { + Map algorithmsAndTerminationDates = new HashMap<>(); + for (HashAlgorithm algorithm : algorithms) { + algorithmsAndTerminationDates.put(algorithm, null); + } + return algorithmsAndTerminationDates; } /** @@ -319,17 +350,17 @@ public final class Policy { } /** - * Return true if the given hash algorithm is acceptable by this policy. + * Return true if the given hash algorithm is currently acceptable by this policy. * * @param hashAlgorithm hash algorithm * @return true if the hash algorithm is acceptable, false otherwise */ - public boolean isAcceptable(HashAlgorithm hashAlgorithm) { - return acceptableHashAlgorithms.contains(hashAlgorithm); + public boolean isAcceptable(@Nonnull HashAlgorithm hashAlgorithm) { + return isAcceptable(hashAlgorithm, new Date()); } /** - * Return true if the given hash algorithm is acceptable by this policy. + * Return true if the given hash algorithm is currently acceptable by this policy. * * @param algorithmId hash algorithm * @return true if the hash algorithm is acceptable, false otherwise @@ -344,6 +375,39 @@ public final class Policy { } } + /** + * Return true, if the given algorithm is acceptable for the given usage date. + * + * @param hashAlgorithm algorithm + * @param usageDate usage date (e.g. signature creation time) + * + * @return acceptance + */ + public boolean isAcceptable(@Nonnull HashAlgorithm hashAlgorithm, @Nonnull Date usageDate) { + if (!acceptableHashAlgorithmsAndTerminationDates.containsKey(hashAlgorithm)) { + return false; + } + + // Check termination date + Date terminationDate = acceptableHashAlgorithmsAndTerminationDates.get(hashAlgorithm); + if (terminationDate == null) { + return true; + } + + // Reject if usage date is past termination date + return terminationDate.after(usageDate); + } + + public boolean isAcceptable(int algorithmId, @Nonnull Date usageDate) { + try { + HashAlgorithm algorithm = HashAlgorithm.requireFromId(algorithmId); + return isAcceptable(algorithm, usageDate); + } catch (NoSuchElementException e) { + // Unknown algorithm is not acceptable + return false; + } + } + /** * The default signature hash algorithm policy of PGPainless. * Note that this policy is only used for non-revocation signatures. @@ -352,6 +416,44 @@ public final class Policy { * @return default signature hash algorithm policy */ public static HashAlgorithmPolicy defaultSignatureAlgorithmPolicy() { + return smartSignatureHashAlgorithmPolicy(); + } + + /** + * {@link HashAlgorithmPolicy} which takes the date of the algorithm usage into consideration. + * If the policy has a termination date for a given algorithm, and the usage date is after that termination + * date, the algorithm is rejected. + * + * This policy is inspired by Sequoia-PGP's collision resistant algorithm policy. + * + * @see + * Sequoia-PGP's Collision Resistant Algorithm Policy + * + * @return smart signature algorithm policy + */ + public static HashAlgorithmPolicy smartSignatureHashAlgorithmPolicy() { + Map algorithmDateMap = new HashMap<>(); + + algorithmDateMap.put(HashAlgorithm.MD5, DateUtil.parseUTCDate("1997-02-01 00:00:00 UTC")); + algorithmDateMap.put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); + algorithmDateMap.put(HashAlgorithm.RIPEMD160, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); + algorithmDateMap.put(HashAlgorithm.SHA224, null); + algorithmDateMap.put(HashAlgorithm.SHA256, null); + algorithmDateMap.put(HashAlgorithm.SHA384, null); + algorithmDateMap.put(HashAlgorithm.SHA512, null); + + return new HashAlgorithmPolicy(HashAlgorithm.SHA512, algorithmDateMap); + } + + /** + * {@link HashAlgorithmPolicy} which only accepts signatures made using algorithms which are acceptable + * according to 2022 standards. + * + * Particularly this policy only accepts algorithms from the SHA2 family. + * + * @return static signature algorithm policy + */ + public static HashAlgorithmPolicy static2022SignatureAlgorithmPolicy() { return new HashAlgorithmPolicy(HashAlgorithm.SHA512, Arrays.asList( HashAlgorithm.SHA224, HashAlgorithm.SHA256, @@ -366,6 +468,41 @@ public final class Policy { * @return default revocation signature hash algorithm policy */ public static HashAlgorithmPolicy defaultRevocationSignatureHashAlgorithmPolicy() { + return smartRevocationSignatureHashAlgorithmPolicy(); + } + + /** + * Revocation Signature {@link HashAlgorithmPolicy} which takes the date of the algorithm usage + * into consideration. + * If the policy has a termination date for a given algorithm, and the usage date is after that termination + * date, the algorithm is rejected. + * + * This policy is inspired by Sequoia-PGP's collision resistant algorithm policy. + * + * @see + * Sequoia-PGP's Collision Resistant Algorithm Policy + * + * @return smart signature revocation algorithm policy + */ + public static HashAlgorithmPolicy smartRevocationSignatureHashAlgorithmPolicy() { + Map algorithmDateMap = new HashMap<>(); + + algorithmDateMap.put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); + algorithmDateMap.put(HashAlgorithm.RIPEMD160, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); + algorithmDateMap.put(HashAlgorithm.SHA224, null); + algorithmDateMap.put(HashAlgorithm.SHA256, null); + algorithmDateMap.put(HashAlgorithm.SHA384, null); + algorithmDateMap.put(HashAlgorithm.SHA512, null); + + return new HashAlgorithmPolicy(HashAlgorithm.SHA512, algorithmDateMap); + } + + /** + * Hash algorithm policy for revocation signatures, which accepts SHA1 & SHA2 algorithms, as well as RIPEMD160. + * + * @return static revocation signature hash algorithm policy + */ + public static HashAlgorithmPolicy static2022RevocationSignatureHashAlgorithmPolicy() { return new HashAlgorithmPolicy(HashAlgorithm.SHA512, Arrays.asList( HashAlgorithm.RIPEMD160, HashAlgorithm.SHA1, diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java index 51bfa7c3..6a10e1f3 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/SignatureValidator.java @@ -195,8 +195,9 @@ public abstract class SignatureValidator { try { HashAlgorithm hashAlgorithm = HashAlgorithm.requireFromId(signature.getHashAlgorithm()); Policy.HashAlgorithmPolicy hashAlgorithmPolicy = getHashAlgorithmPolicyForSignature(signature, policy); - if (!hashAlgorithmPolicy.isAcceptable(signature.getHashAlgorithm())) { - throw new SignatureValidationException("Signature uses unacceptable hash algorithm " + hashAlgorithm); + if (!hashAlgorithmPolicy.isAcceptable(signature.getHashAlgorithm(), signature.getCreationTime())) { + throw new SignatureValidationException("Signature uses unacceptable hash algorithm " + hashAlgorithm + + " (Signature creation time: " + DateUtil.formatUTCDate(signature.getCreationTime()) + ")"); } } catch (NoSuchElementException e) { throw new SignatureValidationException("Signature uses unknown hash algorithm " + signature.getHashAlgorithm()); diff --git a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java index 6d86f8d9..9959be68 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java @@ -9,6 +9,9 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -16,6 +19,7 @@ import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.util.DateUtil; public class PolicyTest { @@ -33,11 +37,23 @@ public class PolicyTest { policy.setSymmetricKeyDecryptionAlgorithmPolicy(new Policy.SymmetricKeyAlgorithmPolicy(SymmetricKeyAlgorithm.AES_256, Arrays.asList(SymmetricKeyAlgorithm.AES_256, SymmetricKeyAlgorithm.AES_192, SymmetricKeyAlgorithm.AES_128, SymmetricKeyAlgorithm.BLOWFISH))); - policy.setSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, - Arrays.asList(HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA256))); + Map sigHashAlgoMap = new HashMap<>(); + sigHashAlgoMap.put(HashAlgorithm.SHA512, null); + sigHashAlgoMap.put(HashAlgorithm.SHA384, null); + sigHashAlgoMap.put(HashAlgorithm.SHA256, null); + sigHashAlgoMap.put(HashAlgorithm.SHA224, null); + sigHashAlgoMap.put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); + policy.setSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, sigHashAlgoMap)); + Map revHashAlgoMap = new HashMap<>(); + revHashAlgoMap.put(HashAlgorithm.SHA512, null); + revHashAlgoMap.put(HashAlgorithm.SHA384, null); + revHashAlgoMap.put(HashAlgorithm.SHA256, null); + revHashAlgoMap.put(HashAlgorithm.SHA224, null); + revHashAlgoMap.put(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); + revHashAlgoMap.put(HashAlgorithm.RIPEMD160, DateUtil.parseUTCDate("2013-02-01 00:00:00 UTC")); policy.setRevocationSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, - Arrays.asList(HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA256, HashAlgorithm.SHA224, HashAlgorithm.SHA1))); + revHashAlgoMap)); policy.setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); } @@ -92,12 +108,17 @@ public class PolicyTest { public void testAcceptableSignatureHashAlgorithm() { assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA512)); assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA512.getAlgorithmId())); + // Usage date before termination date -> acceptable + assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); + assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); } @Test public void testUnacceptableSignatureHashAlgorithm() { assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1)); assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId())); + assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); + assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); } @Test @@ -109,12 +130,16 @@ public class PolicyTest { public void testAcceptableRevocationSignatureHashAlgorithm() { assertTrue(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA384)); assertTrue(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA384.getAlgorithmId())); + assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); + assertTrue(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2000-01-01 00:00:00 UTC"))); } @Test public void testUnacceptableRevocationSignatureHashAlgorithm() { assertFalse(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.RIPEMD160)); assertFalse(policy.getRevocationSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.RIPEMD160.getAlgorithmId())); + assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1, DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); + assertFalse(policy.getSignatureHashAlgorithmPolicy().isAcceptable(HashAlgorithm.SHA1.getAlgorithmId(), DateUtil.parseUTCDate("2020-01-01 00:00:00 UTC"))); } @Test