001// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
002//
003// SPDX-License-Identifier: Apache-2.0
004
005package org.pgpainless.key.util;
006
007import java.io.ByteArrayOutputStream;
008import java.io.IOException;
009import java.io.InputStream;
010import java.io.OutputStream;
011import java.math.BigInteger;
012import java.security.SecureRandom;
013
014import org.bouncycastle.bcpg.BCPGKey;
015import org.bouncycastle.bcpg.DSAPublicBCPGKey;
016import org.bouncycastle.bcpg.DSASecretBCPGKey;
017import org.bouncycastle.bcpg.EdDSAPublicBCPGKey;
018import org.bouncycastle.bcpg.EdSecretBCPGKey;
019import org.bouncycastle.bcpg.ElGamalPublicBCPGKey;
020import org.bouncycastle.bcpg.ElGamalSecretBCPGKey;
021import org.bouncycastle.bcpg.RSAPublicBCPGKey;
022import org.bouncycastle.bcpg.RSASecretBCPGKey;
023import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
024import org.bouncycastle.openpgp.PGPEncryptedDataList;
025import org.bouncycastle.openpgp.PGPException;
026import org.bouncycastle.openpgp.PGPPrivateKey;
027import org.bouncycastle.openpgp.PGPPublicKey;
028import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
029import org.bouncycastle.openpgp.PGPSignature;
030import org.bouncycastle.openpgp.PGPSignatureGenerator;
031import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
032import org.bouncycastle.util.Arrays;
033import org.bouncycastle.util.io.Streams;
034import org.pgpainless.algorithm.HashAlgorithm;
035import org.pgpainless.algorithm.PublicKeyAlgorithm;
036import org.pgpainless.algorithm.SignatureType;
037import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
038import org.pgpainless.exception.KeyIntegrityException;
039import org.pgpainless.implementation.ImplementationFactory;
040
041public class PublicKeyParameterValidationUtil {
042
043    public static void verifyPublicKeyParameterIntegrity(PGPPrivateKey privateKey, PGPPublicKey publicKey)
044            throws KeyIntegrityException {
045        PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.fromId(publicKey.getAlgorithm());
046        boolean valid = true;
047
048        // Algorithm specific validations
049        BCPGKey key = privateKey.getPrivateKeyDataPacket();
050        if (key instanceof RSASecretBCPGKey) {
051            valid = verifyRSAKeyIntegrity(
052                    (RSASecretBCPGKey) key,
053                    (RSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey())
054                    && valid;
055        } else if (key instanceof EdSecretBCPGKey) {
056            valid = verifyEdDsaKeyIntegrity(
057                    (EdSecretBCPGKey) key,
058                    (EdDSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey())
059                    && valid;
060        } else if (key instanceof DSASecretBCPGKey) {
061            valid = verifyDsaKeyIntegrity(
062                    (DSASecretBCPGKey) key,
063                    (DSAPublicBCPGKey) publicKey.getPublicKeyPacket().getKey())
064                    && valid;
065        } else if (key instanceof ElGamalSecretBCPGKey) {
066            valid = verifyElGamalKeyIntegrity(
067                    (ElGamalSecretBCPGKey) key,
068                    (ElGamalPublicBCPGKey) publicKey.getPublicKeyPacket().getKey())
069                    && valid;
070        }
071
072        if (!valid) {
073            throw new KeyIntegrityException();
074        }
075
076        // Additional to the algorithm-specific tests further above, we also perform
077        // generic functionality tests with the key, such as whether it is able to decrypt encrypted data
078        // or verify signatures.
079        // These tests should be more or less constant time.
080        if (publicKeyAlgorithm.isSigningCapable()) {
081            valid = verifyCanSign(privateKey, publicKey);
082        }
083        if (publicKeyAlgorithm.isEncryptionCapable()) {
084            valid = verifyCanDecrypt(privateKey, publicKey) && valid;
085        }
086
087        if (!valid) {
088            throw new KeyIntegrityException();
089        }
090    }
091
092    /**
093     * Verify that the public key can be used to successfully verify a signature made by the private key.
094     * @param privateKey private key
095     * @param publicKey public key
096     * @return false if signature verification fails
097     */
098    private static boolean verifyCanSign(PGPPrivateKey privateKey, PGPPublicKey publicKey) {
099        SecureRandom random = new SecureRandom();
100        PublicKeyAlgorithm publicKeyAlgorithm = PublicKeyAlgorithm.fromId(publicKey.getAlgorithm());
101        PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
102                ImplementationFactory.getInstance().getPGPContentSignerBuilder(publicKeyAlgorithm, HashAlgorithm.SHA256)
103        );
104
105        try {
106            signatureGenerator.init(SignatureType.TIMESTAMP.getCode(), privateKey);
107
108            byte[] data = new byte[512];
109            random.nextBytes(data);
110
111            signatureGenerator.update(data);
112            PGPSignature sig = signatureGenerator.generate();
113
114            sig.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), publicKey);
115            sig.update(data);
116            return sig.verify();
117        } catch (PGPException e) {
118            return false;
119        }
120    }
121
122    /**
123     * Verify that the public key can be used to encrypt a message which can successfully be
124     * decrypted using the private key.
125     * @param privateKey private key
126     * @param publicKey public key
127     * @return false if decryption of a message encrypted with the public key fails
128     */
129    private static boolean verifyCanDecrypt(PGPPrivateKey privateKey, PGPPublicKey publicKey) {
130        SecureRandom random = new SecureRandom();
131        PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(
132                ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(SymmetricKeyAlgorithm.AES_256)
133        );
134        encryptedDataGenerator.addMethod(
135                ImplementationFactory.getInstance().getPublicKeyKeyEncryptionMethodGenerator(publicKey));
136
137        byte[] data = new byte[1024];
138        random.nextBytes(data);
139        ByteArrayOutputStream out = new ByteArrayOutputStream();
140        try {
141            OutputStream outputStream = encryptedDataGenerator.open(out, new byte[1024]);
142            outputStream.write(data);
143            encryptedDataGenerator.close();
144            PGPEncryptedDataList encryptedDataList = new PGPEncryptedDataList(out.toByteArray());
145            PublicKeyDataDecryptorFactory decryptorFactory =
146                    ImplementationFactory.getInstance().getPublicKeyDataDecryptorFactory(privateKey);
147            PGPPublicKeyEncryptedData encryptedData =
148                    (PGPPublicKeyEncryptedData) encryptedDataList.getEncryptedDataObjects().next();
149            InputStream decrypted = encryptedData.getDataStream(decryptorFactory);
150            out = new ByteArrayOutputStream();
151            Streams.pipeAll(decrypted, out);
152            decrypted.close();
153        } catch (IOException | PGPException e) {
154            return false;
155        }
156
157        return Arrays.constantTimeAreEqual(data, out.toByteArray());
158    }
159
160    private static boolean verifyEdDsaKeyIntegrity(EdSecretBCPGKey privateKey, EdDSAPublicBCPGKey publicKey)
161            throws KeyIntegrityException {
162        // TODO: Implement
163        return true;
164    }
165
166    private static boolean verifyDsaKeyIntegrity(DSASecretBCPGKey privateKey, DSAPublicBCPGKey publicKey)
167            throws KeyIntegrityException {
168        // Not sure what value to put here in order to have a "robust" primality check
169        // I went with 40, since that's what SO recommends:
170        // https://stackoverflow.com/a/6330138
171        final int certainty = 40;
172        BigInteger pG = publicKey.getG();
173        BigInteger pP = publicKey.getP();
174        BigInteger pQ = publicKey.getQ();
175        BigInteger pY = publicKey.getY();
176        BigInteger sX = privateKey.getX();
177
178        boolean pPrime = pP.isProbablePrime(certainty);
179        if (!pPrime) {
180            return false;
181        }
182
183        boolean qPrime = pQ.isProbablePrime(certainty);
184        if (!qPrime) {
185            return false;
186        }
187
188        // q > 160 bits
189        boolean qLarge = pQ.getLowestSetBit() > 160;
190        if (!qLarge) {
191            return false;
192        }
193
194        // q divides p - 1
195        boolean qDividesPminus1 = pP.subtract(BigInteger.ONE).mod(pQ).equals(BigInteger.ZERO);
196        if (!qDividesPminus1) {
197            return false;
198        }
199
200        // 1 < g < p
201        boolean gInBounds = BigInteger.ONE.max(pG).equals(pG) && pG.max(pP).equals(pP);
202        if (!gInBounds) {
203            return false;
204        }
205
206        // g^q = 1 mod p
207        boolean gPowXModPEquals1 = pG.modPow(pQ, pP).equals(BigInteger.ONE);
208        if (!gPowXModPEquals1) {
209            return false;
210        }
211
212        // y = g^x mod p
213        boolean yEqualsGPowXModP = pY.equals(pG.modPow(sX, pP));
214        if (!yEqualsGPowXModP) {
215            return false;
216        }
217
218        return true;
219    }
220
221    private static boolean verifyRSAKeyIntegrity(RSASecretBCPGKey secretKey, RSAPublicBCPGKey publicKey)
222            throws KeyIntegrityException {
223        // Verify that the public keys N is equal to private keys p*q
224        return publicKey.getModulus().equals(secretKey.getPrimeP().multiply(secretKey.getPrimeQ()));
225    }
226
227    /**
228     * Validate ElGamal public key parameters.
229     *
230     * Original implementation by the openpgpjs authors:
231     * https://github.com/openpgpjs/openpgpjs/blob/main/src/crypto/public_key/elgamal.js#L76-L143
232     * @param secretKey secret key
233     * @param publicKey public key
234     * @return true if supposedly valid, false if invalid
235     */
236    private static boolean verifyElGamalKeyIntegrity(ElGamalSecretBCPGKey secretKey, ElGamalPublicBCPGKey publicKey) {
237        BigInteger p = publicKey.getP();
238        BigInteger g = publicKey.getG();
239        BigInteger y = publicKey.getY();
240        BigInteger one = BigInteger.ONE;
241
242        // 1 < g < p
243        if (g.min(one).equals(g) || g.max(p).equals(g)) {
244            return false;
245        }
246
247        // p-1 is large
248        if (p.bitLength() < 1023) {
249            return false;
250        }
251
252        // g^(p-1) mod p = 1
253        if (!g.modPow(p.subtract(one), p).equals(one)) {
254            return false;
255        }
256
257        // check g^i mod p != 1 for i < threshold
258        BigInteger res = g;
259        BigInteger i = BigInteger.valueOf(1);
260        BigInteger threshold = BigInteger.valueOf(2).shiftLeft(17);
261        while (i.compareTo(threshold) < 0) {
262            res = res.multiply(g).mod(p);
263            if (res.equals(one)) {
264                return false;
265            }
266            i = i.add(one);
267        }
268
269        // blinded exponentiation to check y = g^(r*(p-1)+x) mod p
270        SecureRandom random = new SecureRandom();
271        BigInteger x = secretKey.getX();
272        BigInteger r = new BigInteger(p.bitLength(), random);
273        BigInteger rqx = p.subtract(one).multiply(r).add(x);
274        if (!y.equals(g.modPow(rqx, p))) {
275            return false;
276        }
277
278        return true;
279    }
280
281}