From 5364e21b5e2fc96c8a2708e88ba29e2e63dc43a8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 24 Nov 2021 18:46:29 +0100 Subject: [PATCH] WiP implementation of public key parameter validation --- .../exception/KeyIntegrityException.java | 12 ++ .../key/protection/UnlockSecretKey.java | 15 +- .../PublicKeyParameterValidationUtil.java | 172 ++++++++++++++++++ .../GenerateEllipticCurveKeyTest.java | 3 + 4 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/exception/KeyIntegrityException.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/exception/KeyIntegrityException.java b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyIntegrityException.java new file mode 100644 index 00000000..65ed3dea --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/exception/KeyIntegrityException.java @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.exception; + +public class KeyIntegrityException extends AssertionError { + + public KeyIntegrityException() { + super("Key Integrity Exception"); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java index c68e4914..ce7bb5b6 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/UnlockSecretKey.java @@ -9,8 +9,10 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.pgpainless.exception.KeyIntegrityException; import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.key.info.KeyInfo; +import org.pgpainless.key.util.PublicKeyParameterValidationUtil; import org.pgpainless.util.Passphrase; public final class UnlockSecretKey { @@ -20,13 +22,20 @@ public final class UnlockSecretKey { } public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, SecretKeyRingProtector protector) - throws WrongPassphraseException { + throws WrongPassphraseException, KeyIntegrityException { try { PBESecretKeyDecryptor decryptor = null; if (KeyInfo.isEncrypted(secretKey)) { decryptor = protector.getDecryptor(secretKey.getKeyID()); } - return secretKey.extractPrivateKey(decryptor); + PGPPrivateKey privateKey = secretKey.extractPrivateKey(decryptor); + + if (secretKey.getPublicKey() != null) { + PublicKeyParameterValidationUtil.verifyPublicKeyParameterIntegrity(privateKey, secretKey.getPublicKey()); + } + return privateKey; + } catch (KeyIntegrityException e) { + throw e; } catch (PGPException e) { throw new WrongPassphraseException(secretKey.getKeyID(), e); } @@ -40,7 +49,7 @@ public final class UnlockSecretKey { } } - public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, Passphrase passphrase) throws WrongPassphraseException { + public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, Passphrase passphrase) throws WrongPassphraseException, KeyIntegrityException { return unlockSecretKey(secretKey, SecretKeyRingProtector.unlockSingleKeyWith(passphrase, secretKey)); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java new file mode 100644 index 00000000..e2cf058f --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/util/PublicKeyParameterValidationUtil.java @@ -0,0 +1,172 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.SecureRandom; + +import org.bouncycastle.bcpg.BCPGKey; +import org.bouncycastle.bcpg.DSAPublicBCPGKey; +import org.bouncycastle.bcpg.DSASecretBCPGKey; +import org.bouncycastle.bcpg.EdDSAPublicBCPGKey; +import org.bouncycastle.bcpg.EdSecretBCPGKey; +import org.bouncycastle.bcpg.RSAPublicBCPGKey; +import org.bouncycastle.bcpg.RSASecretBCPGKey; +import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; +import org.bouncycastle.openpgp.PGPEncryptedDataList; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.io.Streams; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.exception.KeyIntegrityException; +import org.pgpainless.implementation.ImplementationFactory; + +public class PublicKeyParameterValidationUtil { + + public static void verifyPublicKeyParameterIntegrity(PGPPrivateKey privateKey, PGPPublicKey publicKey) + throws KeyIntegrityException, PGPException { + PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.fromId(publicKey.getAlgorithm()); + boolean valid = true; + // Additional to the algorithm-specific tests further below, we also perform + // generic functionality tests with the key, such as whether it is able to decrypt encrypted data + // or verify signatures. + // These tests should be more or less constant time. + if (publicKeyAlgorithm.isSigningCapable()) { + valid = verifyCanSign(privateKey, publicKey) && valid; + } + if (publicKeyAlgorithm.isEncryptionCapable()) { + valid = verifyCanDecrypt(privateKey, publicKey) && valid; + } + + // Algorithm specific validations + BCPGKey key = privateKey.getPrivateKeyDataPacket(); + if (key instanceof RSASecretBCPGKey) { + valid = verifyRSAKeyIntegrity( + (RSASecretBCPGKey) key, + (RSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey()) + && valid; + } else if (key instanceof EdSecretBCPGKey) { + valid = verifyEdDsaKeyIntegrity( + (EdSecretBCPGKey) key, + (EdDSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey()) + && valid; + } else if (key instanceof DSASecretBCPGKey) { + valid = verifyDsaKeyIntegrity( + (DSASecretBCPGKey) key, + (DSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey()) + && valid; + } + + // TODO: ElGamal + + if (!valid) { + throw new KeyIntegrityException(); + } + } + + private static boolean verifyCanSign(PGPPrivateKey privateKey, PGPPublicKey publicKey) throws PGPException { + SecureRandom random = new SecureRandom(); + PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.fromId(publicKey.getAlgorithm()); + PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( + ImplementationFactory.getInstance().getPGPContentSignerBuilder(publicKeyAlgorithm, HashAlgorithm.SHA256) + ); + + signatureGenerator.init(SignatureType.TIMESTAMP.getCode(), privateKey); + + byte[] data = new byte[512]; + random.nextBytes(data); + + signatureGenerator.update(data); + PGPSignature sig = signatureGenerator.generate(); + + sig.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), publicKey); + sig.update(data); + return sig.verify(); + } + + private static boolean verifyCanDecrypt(PGPPrivateKey privateKey, PGPPublicKey publicKey) { + SecureRandom random = new SecureRandom(); + PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator( + ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(SymmetricKeyAlgorithm.AES_256) + ); + encryptedDataGenerator.addMethod( + ImplementationFactory.getInstance().getPublicKeyKeyEncryptionMethodGenerator(publicKey)); + + byte[] data = new byte[1024]; + random.nextBytes(data); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + OutputStream outputStream = encryptedDataGenerator.open(out, new byte[1024]); + outputStream.write(data); + encryptedDataGenerator.close(); + PGPEncryptedDataList encryptedDataList = new PGPEncryptedDataList(out.toByteArray()); + PublicKeyDataDecryptorFactory decryptorFactory = + ImplementationFactory.getInstance().getPublicKeyDataDecryptorFactory(privateKey); + PGPPublicKeyEncryptedData encryptedData = + (PGPPublicKeyEncryptedData) encryptedDataList.getEncryptedDataObjects().next(); + InputStream decrypted = encryptedData.getDataStream(decryptorFactory); + out = new ByteArrayOutputStream(); + Streams.pipeAll(decrypted, out); + decrypted.close(); + } catch (IOException | PGPException e) { + return false; + } + + return Arrays.constantTimeAreEqual(data, out.toByteArray()); + } + + private static boolean verifyEdDsaKeyIntegrity(EdSecretBCPGKey privateKey, EdDSAPublicBCPGKey publicKey) + throws KeyIntegrityException { + // TODO: Implement + return true; + } + + private static boolean verifyDsaKeyIntegrity(DSASecretBCPGKey privateKey, DSAPublicBCPGKey publicKey) + throws KeyIntegrityException { + // Not sure what value to put here in order to have a "robust" primality check + // I went with 40, since that's what SO recommends: + // https://stackoverflow.com/a/6330138 + final int certainty = 40; + BigInteger pG = publicKey.getG(); + BigInteger pP = publicKey.getP(); + BigInteger pQ = publicKey.getQ(); + BigInteger pY = publicKey.getY(); + BigInteger sX = privateKey.getX(); + + boolean pPrime = pP.isProbablePrime(certainty); + boolean qPrime = pQ.isProbablePrime(certainty); + // q > 160 bits + boolean qLarge = pQ.getLowestSetBit() > 160; + // q divides p - 1 + boolean qDividesPminus1 = pP.subtract(BigInteger.ONE).mod(pQ).equals(BigInteger.ZERO); + // 1 < g < p + boolean gInBounds = BigInteger.ONE.max(pG).equals(pG) && pG.max(pP).equals(pP); + // g^q = 1 mod p + boolean gPowXModPEquals1 = pG.modPow(pQ, pP).equals(BigInteger.ONE); + // y = g^x mod p + boolean yEqualsGPowXModP = pY.equals(pG.modPow(sX, pP)); + + return pPrime && qPrime && qLarge && qDividesPminus1 && gInBounds && gPowXModPEquals1 && yEqualsGPowXModP; + } + + private static boolean verifyRSAKeyIntegrity(RSASecretBCPGKey secretKey, RSAPublicBCPGKey publicKey) + throws KeyIntegrityException { + // Verify that the public keys N is equal to private keys p*q + return publicKey.getModulus().equals(secretKey.getPrimeP().multiply(secretKey.getPrimeQ())); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java index f1988d8f..eef7812b 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateEllipticCurveKeyTest.java @@ -21,6 +21,8 @@ import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.generation.type.KeyType; import org.pgpainless.key.generation.type.eddsa.EdDSACurve; import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; import org.pgpainless.key.util.UserId; public class GenerateEllipticCurveKeyTest { @@ -38,5 +40,6 @@ public class GenerateEllipticCurveKeyTest { .build(); assertEquals(PublicKeyAlgorithm.EDDSA.getAlgorithmId(), keyRing.getPublicKey().getAlgorithm()); + UnlockSecretKey.unlockSecretKey(keyRing.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); } }