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 014cf0c0..669dbfc5 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java +++ b/pgpainless-core/src/main/java/org/pgpainless/policy/Policy.java @@ -17,10 +17,13 @@ package org.pgpainless.policy; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.util.NotationRegistry; @@ -41,6 +44,8 @@ public final class Policy { SymmetricKeyAlgorithmPolicy.defaultSymmetricKeyDecryptionAlgorithmPolicy(); private CompressionAlgorithmPolicy compressionAlgorithmPolicy = CompressionAlgorithmPolicy.defaultCompressionAlgorithmPolicy(); + private PublicKeyAlgorithmPolicy publicKeyAlgorithmPolicy = + PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy(); private final NotationRegistry notationRegistry = new NotationRegistry(); Policy() { @@ -156,6 +161,27 @@ public final class Policy { this.compressionAlgorithmPolicy = policy; } + /** + * Return the current public key algorithm policy. + * + * @return public key algorithm policy + */ + public PublicKeyAlgorithmPolicy getPublicKeyAlgorithmPolicy() { + return publicKeyAlgorithmPolicy; + } + + /** + * Set a custom public key algorithm policy. + * + * @param publicKeyAlgorithmPolicy custom policy + */ + public void setPublicKeyAlgorithmPolicy(PublicKeyAlgorithmPolicy publicKeyAlgorithmPolicy) { + if (publicKeyAlgorithmPolicy == null) { + throw new NullPointerException("Public key algorithm policy cannot be null."); + } + this.publicKeyAlgorithmPolicy = publicKeyAlgorithmPolicy; + } + public static final class SymmetricKeyAlgorithmPolicy { private final SymmetricKeyAlgorithm defaultSymmetricKeyAlgorithm; @@ -344,6 +370,66 @@ public final class Policy { } } + public static final class PublicKeyAlgorithmPolicy { + + private final Map algorithmStrengths = new HashMap<>(); + + public PublicKeyAlgorithmPolicy(Map minimalAlgorithmBitStrengths) { + this.algorithmStrengths.putAll(minimalAlgorithmBitStrengths); + } + + public boolean isAcceptable(int algorithmId, int bitStrength) { + return isAcceptable(PublicKeyAlgorithm.fromId(algorithmId), bitStrength); + } + + public boolean isAcceptable(PublicKeyAlgorithm algorithm, int bitStrength) { + if (!algorithmStrengths.containsKey(algorithm)) { + return false; + } + + int minStrength = algorithmStrengths.get(algorithm); + return bitStrength >= minStrength; + } + + /** + * Return PGPainless' default public key algorithm policy. + * This policy is based upon recommendations made by the German Federal Office for Information Security (BSI). + * + * Basically this policy requires keys based on elliptic curves to have a bit strength of at least 250, + * and keys based on prime number factorization / discrete logarithm problems to have a strength of at least 2000 bits. + * + * @see + * BSI - Technical Guideline - Cryptographic Mechanisms: Recommendations and Key Lengths (2021-01) + * + * @return default algorithm policy + */ + public static PublicKeyAlgorithmPolicy defaultPublicKeyAlgorithmPolicy() { + Map minimalBitStrengths = new HashMap<>(); + // §5.4.1 + minimalBitStrengths.put(PublicKeyAlgorithm.RSA_GENERAL, 2000); + minimalBitStrengths.put(PublicKeyAlgorithm.RSA_SIGN, 2000); + minimalBitStrengths.put(PublicKeyAlgorithm.RSA_ENCRYPT, 2000); + // TODO: ElGamal is not mentioned in the BSI document. + // We assume that the requirements are similar to other DH algorithms + minimalBitStrengths.put(PublicKeyAlgorithm.ELGAMAL_ENCRYPT, 2000); + minimalBitStrengths.put(PublicKeyAlgorithm.ELGAMAL_GENERAL, 2000); + // §5.4.2 + minimalBitStrengths.put(PublicKeyAlgorithm.DSA, 2000); + // §5.4.3 + minimalBitStrengths.put(PublicKeyAlgorithm.ECDSA, 250); + // TODO: EdDSA is not mentioned in the BSI document. + // We assume that the requirements are similar to other EC algorithms. + minimalBitStrengths.put(PublicKeyAlgorithm.EDDSA, 250); + // §7.2.1 + minimalBitStrengths.put(PublicKeyAlgorithm.DIFFIE_HELLMAN, 2000); + // §7.2.2 + minimalBitStrengths.put(PublicKeyAlgorithm.ECDH, 250); + minimalBitStrengths.put(PublicKeyAlgorithm.EC, 250); + + return new PublicKeyAlgorithmPolicy(minimalBitStrengths); + } + } + /** * Return the {@link NotationRegistry} of PGPainless. * The notation registry is used to decide, whether or not a Notation is known or not. diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureValidator.java index 61000219..ff6237f2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/SignatureValidator.java @@ -40,6 +40,7 @@ import org.pgpainless.exception.SignatureValidationException; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.policy.Policy; import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; +import org.pgpainless.util.BCUtil; import org.pgpainless.util.NotationRegistry; public abstract class SignatureValidator { @@ -265,6 +266,21 @@ public abstract class SignatureValidator { signatureDoesNotHaveCriticalUnknownNotations(policy.getNotationRegistry()).verify(signature); signatureDoesNotHaveCriticalUnknownSubpackets().verify(signature); signatureUsesAcceptableHashAlgorithm(policy).verify(signature); + signatureUsesAcceptablePublicKeyAlgorithm(policy, signingKey).verify(signature); + } + }; + } + + private static SignatureValidator signatureUsesAcceptablePublicKeyAlgorithm(Policy policy, PGPPublicKey signingKey) { + return new SignatureValidator() { + @Override + public void verify(PGPSignature signature) throws SignatureValidationException { + PublicKeyAlgorithm algorithm = PublicKeyAlgorithm.fromId(signingKey.getAlgorithm()); + int bitStrength = BCUtil.getBitStrenght(signingKey); + if (!policy.getPublicKeyAlgorithmPolicy().isAcceptable(algorithm, bitStrength)) { + throw new SignatureValidationException("Signature was made using unacceptable key. " + + algorithm + " (" + bitStrength + " bits) is not acceptable according to the public key algorithm policy."); + } } }; } diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java index 66853f48..72b24385 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/BCUtil.java @@ -20,6 +20,9 @@ import java.io.IOException; import java.io.InputStream; import javax.annotation.Nonnull; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.bcpg.ECPublicBCPGKey; +import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPUtil; public class BCUtil { @@ -34,4 +37,26 @@ public class BCUtil { return PGPUtil.getDecoderStream(inputStream); } + public static int getBitStrenght(PGPPublicKey key) { + int bitStrength = key.getBitStrength(); + + if (bitStrength == -1) { + // TODO: BC's PGPPublicKey.getBitStrength() does fail for some keys (EdDSA, X25519) + // Manually set the bit strength. + + ASN1ObjectIdentifier oid = ((ECPublicBCPGKey) key.getPublicKeyPacket().getKey()).getCurveOID(); + if (oid.getId().equals("1.3.6.1.4.1.11591.15.1")) { + // ed25519 is 256 bits + bitStrength = 256; + } else if (oid.getId().equals("1.3.6.1.4.1.3029.1.5.1")) { + // curvey25519 is 256 bits + bitStrength = 256; + } else { + throw new RuntimeException("Unknown curve: " + oid.getId()); + } + + } + return bitStrength; + } + } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java index a9dce1c5..be574c1f 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/BrainpoolKeyGenerationTest.java @@ -41,6 +41,7 @@ import org.pgpainless.key.generation.type.rsa.RsaLength; import org.pgpainless.key.generation.type.xdh.XDHSpec; import org.pgpainless.key.info.KeyInfo; import org.pgpainless.key.util.UserId; +import org.pgpainless.util.BCUtil; import org.pgpainless.util.Passphrase; public class BrainpoolKeyGenerationTest { @@ -115,18 +116,22 @@ public class BrainpoolKeyGenerationTest { PGPSecretKey ecdsaPrim = iterator.next(); KeyInfo ecdsaInfo = new KeyInfo(ecdsaPrim); assertEquals(EllipticCurve._BRAINPOOLP384R1.getName(), ecdsaInfo.getCurveName()); + assertEquals(384, BCUtil.getBitStrenght(ecdsaPrim.getPublicKey())); PGPSecretKey eddsaSub = iterator.next(); KeyInfo eddsaInfo = new KeyInfo(eddsaSub); assertEquals(EdDSACurve._Ed25519.getName(), eddsaInfo.getCurveName()); + assertEquals(256, BCUtil.getBitStrenght(eddsaSub.getPublicKey())); PGPSecretKey xdhSub = iterator.next(); KeyInfo xdhInfo = new KeyInfo(xdhSub); assertEquals(XDHSpec._X25519.getCurveName(), xdhInfo.getCurveName()); + assertEquals(256, BCUtil.getBitStrenght(xdhSub.getPublicKey())); PGPSecretKey rsaSub = iterator.next(); KeyInfo rsaInfo = new KeyInfo(rsaSub); assertThrows(IllegalArgumentException.class, rsaInfo::getCurveName, "RSA is not a curve-based encryption system"); + assertEquals(3072, BCUtil.getBitStrenght(rsaSub.getPublicKey())); } public PGPSecretKeyRing generateKey(KeySpec primaryKey, KeySpec subKey, String userId) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException { 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 f8750581..1957cd1e 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/policy/PolicyTest.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; public class PolicyTest { @@ -48,6 +49,8 @@ public class PolicyTest { policy.setRevocationSignatureHashAlgorithmPolicy(new Policy.HashAlgorithmPolicy(HashAlgorithm.SHA512, Arrays.asList(HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA256, HashAlgorithm.SHA224, HashAlgorithm.SHA1))); + + policy.setPublicKeyAlgorithmPolicy(Policy.PublicKeyAlgorithmPolicy.defaultPublicKeyAlgorithmPolicy()); } @Test @@ -130,6 +133,17 @@ public class PolicyTest { assertEquals(HashAlgorithm.SHA512, policy.getRevocationSignatureHashAlgorithmPolicy().defaultHashAlgorithm()); } + @Test + public void testAcceptablePublicKeyAlgorithm() { + assertTrue(policy.getPublicKeyAlgorithmPolicy().isAcceptable(PublicKeyAlgorithm.ECDSA, 256)); + assertTrue(policy.getPublicKeyAlgorithmPolicy().isAcceptable(PublicKeyAlgorithm.RSA_GENERAL, 3072)); + } + + @Test + public void testUnacceptablePublicKeyAlgorithm() { + assertFalse(policy.getPublicKeyAlgorithmPolicy().isAcceptable(PublicKeyAlgorithm.RSA_GENERAL, 1024)); + } + @Test public void testNotationRegistry() { assertFalse(policy.getNotationRegistry().isKnownNotation("notation@pgpainless.org"));