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}