mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-11-26 22:32:07 +01:00
WiP implementation of public key parameter validation
This commit is contained in:
parent
3b49840c9c
commit
5364e21b5e
4 changed files with 199 additions and 3 deletions
|
@ -0,0 +1,12 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.exception;
|
||||||
|
|
||||||
|
public class KeyIntegrityException extends AssertionError {
|
||||||
|
|
||||||
|
public KeyIntegrityException() {
|
||||||
|
super("Key Integrity Exception");
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,8 +9,10 @@ import org.bouncycastle.openpgp.PGPException;
|
||||||
import org.bouncycastle.openpgp.PGPPrivateKey;
|
import org.bouncycastle.openpgp.PGPPrivateKey;
|
||||||
import org.bouncycastle.openpgp.PGPSecretKey;
|
import org.bouncycastle.openpgp.PGPSecretKey;
|
||||||
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
|
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
|
||||||
|
import org.pgpainless.exception.KeyIntegrityException;
|
||||||
import org.pgpainless.exception.WrongPassphraseException;
|
import org.pgpainless.exception.WrongPassphraseException;
|
||||||
import org.pgpainless.key.info.KeyInfo;
|
import org.pgpainless.key.info.KeyInfo;
|
||||||
|
import org.pgpainless.key.util.PublicKeyParameterValidationUtil;
|
||||||
import org.pgpainless.util.Passphrase;
|
import org.pgpainless.util.Passphrase;
|
||||||
|
|
||||||
public final class UnlockSecretKey {
|
public final class UnlockSecretKey {
|
||||||
|
@ -20,13 +22,20 @@ public final class UnlockSecretKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, SecretKeyRingProtector protector)
|
public static PGPPrivateKey unlockSecretKey(PGPSecretKey secretKey, SecretKeyRingProtector protector)
|
||||||
throws WrongPassphraseException {
|
throws WrongPassphraseException, KeyIntegrityException {
|
||||||
try {
|
try {
|
||||||
PBESecretKeyDecryptor decryptor = null;
|
PBESecretKeyDecryptor decryptor = null;
|
||||||
if (KeyInfo.isEncrypted(secretKey)) {
|
if (KeyInfo.isEncrypted(secretKey)) {
|
||||||
decryptor = protector.getDecryptor(secretKey.getKeyID());
|
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) {
|
} catch (PGPException e) {
|
||||||
throw new WrongPassphraseException(secretKey.getKeyID(), 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));
|
return unlockSecretKey(secretKey, SecretKeyRingProtector.unlockSingleKeyWith(passphrase, secretKey));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// 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()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,8 @@ import org.pgpainless.implementation.ImplementationFactory;
|
||||||
import org.pgpainless.key.generation.type.KeyType;
|
import org.pgpainless.key.generation.type.KeyType;
|
||||||
import org.pgpainless.key.generation.type.eddsa.EdDSACurve;
|
import org.pgpainless.key.generation.type.eddsa.EdDSACurve;
|
||||||
import org.pgpainless.key.generation.type.xdh.XDHSpec;
|
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;
|
import org.pgpainless.key.util.UserId;
|
||||||
|
|
||||||
public class GenerateEllipticCurveKeyTest {
|
public class GenerateEllipticCurveKeyTest {
|
||||||
|
@ -38,5 +40,6 @@ public class GenerateEllipticCurveKeyTest {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
assertEquals(PublicKeyAlgorithm.EDDSA.getAlgorithmId(), keyRing.getPublicKey().getAlgorithm());
|
assertEquals(PublicKeyAlgorithm.EDDSA.getAlgorithmId(), keyRing.getPublicKey().getAlgorithm());
|
||||||
|
UnlockSecretKey.unlockSecretKey(keyRing.getSecretKey(), SecretKeyRingProtector.unprotectedKeys());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue